0.9.0 - Pipeline Investigation Tools
Pipeline investigation tools - three new MCP surfaces (get_pipeline_summary, get_job_log_smart, list_pipeline_jobs extension) for AI-agent-friendly CI failure investigation. Closes #64 (the reincarnation arc through #86 → #99). Substantial review cycle: 3 contributor rounds + 1 maintainer Path-B reincarnation + 5 codex bot review rounds + 2 pre-push agent passes closed 21 real bugs end-to-end.
Contributors: @ecthelion77 (Olivier Gintrand) authored the original three-tool design + initial implementation + three response rounds; maintainer added contract-clarity / silent-failure / perf hardening on top via Path B reincarnation (#99).
Added
get_pipeline_summarytool - single-call pipeline investigation returning
pipeline details, jobs grouped by stage, and log tails for failed jobs. Includes
failure pattern detection andmax_failed_jobs_with_logscap (default: 5) to
prevent context blowup. (#64)get_job_log_smarttool - intelligent log post-processor that strips ANSI
codes, GitLab timestamps, and section markers. Supportstail/head, section
extraction, anderror_onlyfiltering. (#64)list_pipeline_jobsextension - new optionalinclude_log_tail,
log_tail_lines, andmax_log_tail_jobsparameters. Wheninclude_log_tail=true,
failed jobs include their cleaned log tail directly in the response. (#64)truncatedfield inget_pipeline_summaryresponse - signals when pagination
cap (50 pages) was hit and job list may be incomplete.section_matchedanderror_lines_matchedfields inget_job_log_smart
response - explicit feedback on filter effectiveness.log_fetch_errorsfield in pipeline summary - surfaces per-job log fetch
failures instead of silently swallowing them.
Changed
- Renamed
log_lines→log_tail_linesinget_pipeline_summaryschema for
consistency withlist_pipeline_jobs. (#64 review) failure_patternis now an exhaustive discriminated union
(no_failures | single | shared_reason | mixed | unknown) instead of a
plain string. Bothshared_reasonandmixedvariants carry
unreasoned_countso callers can tell when some failed jobs lacked a
failure_reason(the invariantsum(reasons) + unreasoned_count === failedJobs.lengthholds for both). (#64 review + #99 codex)- Stage status derivation now mirrors GitLab's aggregation:
allow_failure
jobs no longer poison the stage; mixed success+skipped collapses to success.
(#64 review) - Pagination no longer relies on
X-Totalheader (absent in GitLab EE);
usesitems.length < per_pagewith a MAX_PAGES=50 safety cap. (#64 review)
Fixed
error_onlymode inget_job_log_smartnow correctly returns an empty log
(instead of the full log) when no error-like lines are found. (#64 review)- Silent log fetch failures -
getJobLogTailsandgetPipelineSummarynow
track and report errors per job instead of empty catch blocks. (#64 review) - E2E tests - assertions moved outside try/catch so test failures propagate
correctly; try/catch narrowed to the fetch step only. (#64 review) list_pipeline_jobs(include_log_tail=true) now always emits the
unified{jobs, log_fetch_errors?}wrapper, including the zero-failed-jobs
fallback path that previously reverted to the legacy flat-array shape.
(#64 round-3 follow-up)get_job_log_smartreturnsline_count: 0for an empty log instead of
the JS-quirk1from''.split('\n'). Faithful counts on the
section-not-found anderror_onlyno-match paths. (#64 round-3 follow-up)FailurePattern.single.reasonnormalizes empty-stringfailure_reason
tonull(was passed through verbatim by??). Aligns with.filter(Boolean)
semantics used by the multi-job variants. (#64 round-3 follow-up)FailurePattern.mixedgainsunreasoned_countfor parity with
shared_reason. Previously the mixed branch silently dropped failed jobs
whosefailure_reasonwas missing/empty from the visible reason histogram.
(#99 codex)get_job_log_smartsection extraction is now exact-match. The previous
regexsection_start:\d+:NAME[^\n]*\n?accepted a prefix match - requesting
section: "build"would silently returnbuild_extracontent with
section_matched: true. Now uses a lookahead(?=[\r\n\[]|$)after the
name to require GitLab's actual section delimiters. (#99 codex P2 round-2)stripSectionsnow consumes both CR and LF after the marker. GitLab's
section linesection_*:NNN:name\r\x1B[0K\ncollapses to
section_*:NNN:name\r\nafter ANSI stripping; the previous regex tail
[\r\n]?consumed only one of CR/LF, leaving an orphan\nper marker.
Cleaned logs had spurious blank lines andtail: Ncould shift by an
empty line. New regex tail\r?\n?consumes the full CRLF combo.
(#99 codex P2 round-3)sections_foundsurfaces the bare section name. Previously emitted
"script[collapsed=true]"verbatim, which is unusable as a follow-up
section:argument (end markers never carry the[option]suffix and
would never match). Now strips the[...]collapsed-marker tail after
ANSI removal. (#99 codex P2 round-4)pipeline_id: 0no longer silently falls through to "latest pipeline".
Schema now requiresz.number().int().positive()and the runtime path
selector uses!== undefined. Previously a JS truthy check disagreed
with the schema's XOR refine. (#99 pre-push silent-failure-hunter)section: ""no longer silently skipped. Schema now requires
z.string().min(1); empty-string requests fail at parse instead of
silently returning the full log withsection_matched: null.
(#99 pre-push silent-failure-hunter)sectionmatching is now case-SENSITIVE. Dropped the regexiflag.
GitLab section names are identifiers and case-insensitive matching
would create silent ambiguity between sibling sections likeBuild
andbuild. Aligns with the "exact-match" contract introduced in the
codex P2 round-2 fix. (#99 pre-push silent-failure-hunter LOW-2)- Section markers are stripped regardless of
strip_ansi. Previously
section-marker stripping was bound to thestrip_ansi: truepath; a
caller debugging raw ANSI output would see rawsection_start:NNN:name
bytes leak intotail/headwindows. Section markers are unconditionally
noise and are now always stripped. (#99 pre-push code-simplifier)
Added
log_fetch_cappedfield inget_pipeline_summarysummary and in
thelist_pipeline_jobs + include_log_tailwrapper. Present as
{ fetched: number; total_failed: number }when the helper intentionally
skipped log-fetching for failed jobs beyond the cap
(max_failed_jobs_with_logsdefault 5,max_log_tail_jobsdefault 10).
Closes a silent fallback where callers saw a partial set of log tails
without any signal that more failed jobs existed. (#99 pre-push
silent-failure-hunter)
Fixed (round 5)
tail: 1on a single-line log no longer returns"". The previous
logTailwalked all\nfrom the end including the trailing
terminator, sotail: 1on"ERROR\n"returned the empty string and
log_tail_lines: 50returned only 49 real lines.logTail/logHead/
countLinesnow treat a single trailing\nas the line terminator
via anendIdxbound, preserving the O(tail) memory promise.
(#99 codex P2 round-5)stripSectionsnow consumes\x1B[0Kclear-control bytes between
CR and LF. Whenstrip_ansi: false, the section-stripping path runs
on raw GitLab markerssection_*:NNN:name\r\x1B[0K\n- the previous
regex tail\r?\n?left orphan\x1B[0K\nfragments in the cleaned
log. New tail\r?(?:\x1B\[[0-9;]*[a-zA-Z])*\n?consumes any inline
ANSI escapes between CR and LF, working whether ANSI was pre-stripped
or not. (#99 codex P2 round-5)get_job_log_smart.logis shape-stable across the truncation
boundary. The same tool no longer returns"L\n"fortail: 50
(no truncation) and"L"fortail: 49(truncation kicks in) on the
same 50-line input. Unconditional strip of a single trailing\nat
the end of the helper. (#99 pre-push silent-failure-hunter)
Performance
- Job log tail/head/section/error_only extraction in
get_pipeline_summary,get_job_log_smart, andlist_pipeline_jobs + include_log_tailis now O(K) in memory instead of O(N) - the previous
log.split('\n').slice(-N).join('\n')pattern allocated an array of all
lines just to keep the last/first K. New private helpers (logTail,
logHead,countLines) walk newlines vialastIndexOf/indexOf/
charCodeAt. Section extraction usesRegExp.lastIndexinstead of
rawLog.slice(startIdx)to avoid copying multi-MB log strings.
error_onlyfiltering uses a singlegimregex match instead of
split + filter + join. (#99 gemini)
Security
- Schema parameters now have strict
.int().min().max()bounds to prevent
abuse (e.g.,log_tail_linescapped at 200,max_log_tail_jobsat 20). - XOR validation via
.refine()on mutually exclusive parameters
(pipeline_id/ref,tail/head).