tools-integration-ci

CI provider abstraction with unified API for GitHub and GitLab operations (PR, issues, CI status)

SKILL.md

GitHub

Tools Integration CI Skill

Unified CI provider abstraction using static routing - one script per provider, config stores full commands.

Enforcement

Execution mode: Run scripts exactly as documented; parse TOON output for status and route accordingly.

Prohibited actions:

  • Do not call gh or glab directly; all CI operations go through the script API
  • Do not invent script arguments not listed in the operations table
  • Do not bypass provider detection logic
  • Do not transfer gh/glab flag names from memory when invoking ci leaf subcommands — flag names diverge from the underlying tools (e.g., ci pr merge uses --strategy, not --merge-method)

Constraints:

  • All commands use python3 .plan/execute-script.py plan-marshall:tools-integration-ci:{script} {command} {args}
  • Provider routing is config-driven; do not hard-code provider names
  • Before invoking any ci leaf subcommand whose exact flags you do not already know, Read standards/leaf-command-reference.md (or the relevant group standard). Never guess

Exit-code convention for every script call

The exit-code contract for every python3 .plan/execute-script.py call in this document — of EVERY notation, not only manage-* — is stated once in tools-script-executor/standards/exit-code-convention.md; it is not restated here.

A ci verb reports failure as status: error at exit 0 — by design

This is the authoritative statement of the rule. Every other document cross-references this section rather than restating it.

A ci call's exit code carries no verdict. Branch on the payload status, never on the exit code. An exit code of 0 from any ci verb means the process ran to completion — it does not mean the operation succeeded. The failure is reported in the TOON payload on stdout as status: error, alongside operation, error, and where present error_cause and context.

This is deliberate, not a defect and not a caveat. It is the skill's three-tier model: an expected error — no such PR, an unmergeable branch, a provider refusal — is a well-formed result the caller is meant to read and route on, so it exits 0 and puts the verdict in the payload. A non-zero exit is reserved for the process failing to produce a result at all.

Two places in the source make this concrete, and both are read-only references — neither is modified by this contract:

  • ci_base.output_error (scripts/ci_base.py:883-891) builds the error dict, prints it with serialize_toon, and returns EXIT_SUCCESS. The error envelope goes to stdout; the exit code stays 0.
  • Both provider front-ends close their main() the same way — github_ops.py:1877 (closing print(serialize_toon(result, …)) / return 0 at lines 1973-1974) and gitlab_ops.py:2684 (closing at lines 2761-2762). Each ends dispatch → print → return 0 with no branch on the dispatched result's status, so a handler that returned an error dict exits exactly as a handler that succeeded.

The consequence a caller must design for: an exit-code-only reading accepts a failed ci call as a usable value, and then reads success-payload fields off an error envelope that does not carry them. That is the swallowed-rejection failure this rule exists to prevent — a create-pr step marking itself done on a PR that does not exist.

What This Skill Provides

  • Provider detection and health verification
  • PR operations (create, view, merge, auto-merge, safe-merge, merge-queue, close, ready, edit)
  • PR review operations (comments, wait-for-comments, reply, resolve-thread, thread-reply, reviews)
  • CI status, wait, rerun, and logs (with automatic failure-log download + error-extraction filtering)
  • The PR-wide pull_request-run observable (checks pull-request-runs) behind the not_triggered review-participation state — GitHub only; the GitLab arm refuses explicitly
  • Issue operations (create, comment, prepare-body, prepare-comment, view, close, wait-for-close, wait-for-label)
  • Repo operations (merge-queue probe/enable — platform merge queue / merge train; label ensure — idempotent create-if-missing; label list and file read — read-only, any repository, GitHub only: the GitLab arm refuses with error: not_supported)
  • Org-wide reads (list every repository, search code for a literal) — read-only, each reporting its own completeness; GitHub only, the GitLab arm refuses with error: not_supported
  • Unified TOON output format across providers

Consumers

This skill is a script-only library (not registered in plugin.json). It is consumed by:

  • workflow-integration-github — GitHub PR review comment workflows
  • workflow-integration-gitlab — GitLab MR review comment workflows
  • workflow-integration-git — git commit workflows
  • workflow-pr-doctor — PR diagnosis workflows
  • phase-6-finalize — plan finalization with PR creation

Architecture

Static Routing Pattern: Config stores full commands, wizard generates provider-specific paths.

marshal.json                          Scripts
ci.commands.pr-create ─────────────► ci.py pr create ──► {provider}_ops.py
ci.commands.ci-status ─────────────► ci.py checks status ──► {provider}_ops.py

ci.py is the pure passthrough router; the per-provider handler bodies live in {provider}_ops.py (github_ops.py / gitlab_ops.py) in the workflow-integration-{github,gitlab} bundles — there is no github.py / gitlab.py in this skill's scripts/.

Load Reference: For full architecture details:

Read standards/architecture.md

Skill Structure

tools-integration-ci/
├── SKILL.md                     # This file (API index)
├── standards/
│   ├── architecture.md          # Static routing, skill boundaries
│   ├── api-contract.md          # Shared TOON output formats
│   ├── blocking-wait-pattern.md # Script-side blocking wait instead of shell sleep
│   ├── github-impl.md           # GitHub-specific: gh CLI
│   ├── gitlab-impl.md           # GitLab-specific: glab CLI
│   ├── health-setup.md          # Provider detection, verification, config persistence
│   ├── leaf-command-reference.md # Consolidated cheat sheet of every leaf subcommand
│   ├── pr-operations.md         # PR create, view, merge, auto-merge, safe-merge, merge-queue, close, ready, edit
│   ├── pr-review-operations.md  # PR comments, reply, resolve-thread, thread-reply, reviews
│   ├── ci-operations.md         # CI status, wait, rerun, logs
│   └── issue-operations.md      # Issue create, comment, prepare-body, prepare-comment, view, close, waits
└── scripts/
    ├── ci_health.py             # Detection & verification
    ├── ci.py                    # Provider-agnostic passthrough router (+ router-level `barrier` verb)
    ├── ci_base.py               # Shared argparse surface (pr/checks/issue/branch/repo sub-verbs)
    ├── _ci_barrier.py           # Concurrent finalize-wait barrier coordinator (per-signal-proceed / re-settle)
    └── _ci_log_filter.py        # Failure-log error-extraction filter

Provider handler bodies are NOT in this skill — they live in github_ops.py / gitlab_ops.py under the workflow-integration-{github,gitlab} bundles (GitHub PR-merge handlers are further split into _github_pr.py; GitLab defines its pr handlers inline in gitlab_ops.py).


Scripts

Script Notation Purpose
ci_health plan-marshall:tools-integration-ci:ci_health Provider detection & verification
ci plan-marshall:tools-integration-ci:ci Provider-agnostic router
github_ops plan-marshall:workflow-integration-github:github_ops GitHub handler bodies via gh CLI (routed to by ci.py)
gitlab_ops plan-marshall:workflow-integration-gitlab:gitlab_ops GitLab handler bodies via glab CLI (routed to by ci.py)

Standards (Load On-Demand)

Load the relevant standard when performing specific operations:

Standard When to Load
standards/leaf-command-reference.md Before invoking any unfamiliar ci leaf subcommand
standards/health-setup.md Detecting provider, verifying tools, persisting config
standards/pr-operations.md Creating, viewing, merging, or managing PRs
standards/pr-review-operations.md Replying to reviews, resolving threads, checking approvals
standards/ci-operations.md Checking CI status, waiting for CI, rerunning, getting logs
standards/issue-operations.md Creating, viewing, or closing issues
standards/architecture.md Understanding static routing and skill boundaries
standards/api-contract.md Understanding shared TOON output formats

Worktree-Aware Invocation (--plan-id / --project-dir)

Every ci leaf subcommand accepts an optional top-level routing flag placed before the command/subcommand pair. When supplied, every underlying gh/glab subprocess runs with cwd=<resolved_path>, so branch-aware operations (pr view, checks status, pr create, pr merge, …) resolve HEAD against the specified checkout instead of the Python process cwd.

The router implements the canonical --plan-id / --project-dir routing contract — five cases, no others:

  • --plan-id X and --project-dir Y together — error mutually_exclusive_args. Pick one.
  • --plan-id X only — auto-resolve through file_ops.resolve_plan_context, which owns the single manage-status get-worktree-path invocation. When use_worktree=true the persisted worktree path is used; when the query succeeds and reports use_worktree=false, the main checkout is used. A resolution FAILURE is not a silent fallback: when the worktree-state query cannot be answered — metadata absent, corrupt, or never seeded, or use_worktree=true with an empty persisted path — the resolver raises WorktreeResolutionError, and the router emits a structured TOON error and exits 2. It does NOT degrade to the main checkout.
  • --plan-id NO_PLAN only — the plan-less sentinel. Binds to the main checkout, always; the sentinel never resolves to a worktree.
  • --project-dir Y only — explicit override (legacy / escape hatch).
  • Neither — no resolution happens at all. The router returns without a resolved path and never sets the process-global default cwd, so every gh/glab subprocess inherits the caller's Python process cwd.
# Preferred: bind the call to a plan's worktree by id.
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci \
  --plan-id EXAMPLE-PLAN \
  pr view --head EXAMPLE-PLAN-branch

# Escape hatch: bind to an explicit path (test fixtures, ad-hoc).
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci \
  --project-dir <worktree_path> \
  pr view --head EXAMPLE-PLAN-branch

Both flags are consumed by the ci.py router before the provider script is dispatched; provider scripts behave unchanged. The router scans the entire argument vector and strips the routing flag before the provider parser runs.

Required when invoking CI operations from a checkout whose HEAD is not the branch you want to operate on. See workflow-integration-git/standards/worktree-handling.md for the worktree-specific application of this rule (path convention, dispatch protocol, two-state contract reference).


The NO_PLAN sentinel — one plan-less convention for every --plan-id verb

--plan-id NO_PLAN is accepted uniformly wherever this skill takes a --plan-id. It is one rule for the whole surface, not a per-verb affordance, and it is the ONLY plan-less convention: there is no --body-file, no --no-plan switch, and no verb-specific escape hatch.

Two distinct places take --plan-id, and the sentinel applies to both:

Position Verbs What NO_PLAN means there
Top-level routing flag (before the command pair) any ci invocation Bind the gh/glab subprocess cwd to the main checkout
Verb-scoped body-store binding the ten verbs below Allocate/consume the body from the shared plan-less body store

The ten body-store verbs, all with identical sentinel semantics:

Group Verbs
pr prepare-body, prepare-comment, create, edit, reply, thread-reply
issue prepare-body, prepare-comment, create, comment

Semantics, once, for all of them:

  • The sentinel resolves to the shared plan-less plan directory, which is materialized on demand — directory and status.json, because the plan-existence check treats a directory without status.json as absent.
  • Its working-tree face is always the main checkout.
  • The prepare/consume pairing is unchanged: prepare-* allocates a scratch path, the caller writes the body there with its native Write tool, and the consuming verb reads it. --slot disambiguates concurrent bodies exactly as for a real plan.
  • Use it only when the caller genuinely has no plan. A --plan-id that failed to resolve returns plan_not_found carrying both a sentinel hint and a hint_caveat: a mistyped real plan id must be corrected, never replaced with the sentinel — otherwise a typo becomes a silent write into the shared directory.

The identifier-grammar carve-out that makes NO_PLAN an acceptable --plan-id value lives in tools-input-validation/SKILL.md § "The NO_PLAN sentinel (plan_id carve-out)"; the resolution contract lives in tools-file-ops/SKILL.md § "Plan-Context Resolution". Neither is restated per verb.


Automatic Failure-Log Download on checks wait / checks status

When a checks wait or checks status call observes one or more checks with result: failure, it automatically downloads and filters the failing-job log for every failing check — no separate user-callable subcommand is involved. The behavior is built into the existing checks wait and checks status verbs.

For each failing check, two files are written under the plan-scoped artifact tree artifacts/ci-runs/{run_id}/:

artifacts/ci-runs/{run_id}/{slug}.log           # raw downloaded failing-job log
artifacts/ci-runs/{run_id}/{slug}.filtered.log  # error-extraction filtered variant

{slug} is the failing check's name slugified — lowercased, with each run of non-alphanumeric characters collapsed to a single - (e.g. check verify / verify → slug verify-verify). A single run can fail multiple checks, each with its own distinctly-slugged pair of files.

These paths are surfaced per entry inside the failure TOON's failing_checks[] array — as the log_file and filtered_log_file fields of each entry — and are never scalar top-level keys. failing_checks[] is the subset of the standard checks[] table whose result is failure, enriched with the two file paths plus run_id and error_style.

--error-style selector

Both checks wait and checks status accept an optional --error-style flag that governs how the raw log is filtered into its .filtered.log variant:

--error-style Filtering heuristic
maven Routes through the Maven build parser; falls back to generic.
gradle Routes through the Gradle build parser; falls back to generic.
npm Routes through the npm/node build parser; falls back to generic.
generic Default. Error-context heuristic (ERROR|FAIL|Exception|Traceback, case-insensitive) plus surrounding context lines. Used when no style is given or the job's build system is unknown.

The normative specification for the download/filter behavior, the failing_checks[] transport shape, the slug naming scheme, and multi-failure worked examples lives in standards/api-contract.md (CI Failure Log Download & Filtering). That document is authoritative; see also standards/ci-operations.md for the workflow-level walkthrough.


PR Comment Vocabulary

GitHub and GitLab expose several overlapping concepts for "commenting on a PR". Use the exact subcommand that matches the intent — they are NOT interchangeable:

Subcommand Target Publishing Notes
pr comment / pr reply Top-level issue comment on the PR Immediate Not attached to any line of code or review thread.
pr thread-reply Inline reply on an existing code-review thread Immediate Uses addPullRequestReviewThreadReply on GitHub and the /discussions/{id}/notes endpoint on GitLab. Requires a real thread id (PRRT_* on GitHub). Does NOT create or extend a pending review.
pr resolve-thread Collapse a review thread Immediate Independent of replies — resolving a thread neither posts nor requires a reply.
pr submit-review Publish a pending draft review Immediate GitHub-only safety net. Use when a previous call accidentally queued a reply into a draft PullRequestReview. GitLab has no equivalent — discussions are always immediate, so the GitLab handler returns an explicit error.

Breaking change note: pr thread-reply --thread-id requires a real review-thread node id (PRRT_*). Passing a review-comment id (PRRC_*) is no longer supported and will fail loudly — previous behavior silently queued replies into a PENDING draft review.


Error Handling

All operations return TOON error format on failure:

status: error
operation: pr_create
error: Authentication failed
context: gh auth status returned non-zero

Exit codes:

  • 0: Success (stdout)
  • 1: Error (stderr)

Canonical invocations

The canonical argparse surface for ci.py. The plugin-doctor missing-canonical-block rule checks that this section is PRESENT, matching its heading only — the body is never read; manage-invocation-invalid derives its accept-set from a live --help walk rather than from this section. Consuming docs xref this section by name instead of restating the command inline. See pm-plugin-development:plugin-script-architecture cross-skill-integration.md § "Script invocation in documentation". Each top-level subcommand carries nested sub-verbs; the first positional after the notation is the subcommand, the second is the sub-verb.

pr

Sub-verbs: view, list, landing-state, queue-state, wait-for-queue-settle, reply, resolve-thread, thread-reply, reviews, comments, wait-for-comments, merge, auto-merge, safe-merge, merge-queue, update-branch, close, ready, submit-review, edit, prepare-body, prepare-comment, create.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci pr landing-state \
  [--branch BRANCH]

pr landing-state classifies a branch's done-ness at the PR rather than at the commit, returning exactly one of merged / pr_open / pushed_no_pr / unpushed. Omit --branch to classify the checked-out branch of the routed working tree. It is GitHub-only — registered on the GitHub front-end rather than in the shared parser, so the GitLab surface rejects it until that provider implements the handler. Only a PR whose head IS the branch's current tip counts, so a stale PR that merged a previous tip on a reused branch name cannot report merged for new, unlanded commits. It fails closed: whenever the evidence a verdict rests on cannot be read, it returns status: error rather than a state that could clear the pre-archive gate.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci pr create \
  --title TITLE --plan-id PLAN_ID [--slot SLOT] [--base BASE] [--draft] [--head HEAD] [--label LABEL ...]

pr create takes the PR body from ONE source: the plan-bound body store. Run pr prepare-body --plan-id {id} to allocate a scratch path, write the body to that path with the Write tool, then call pr create --plan-id {id} — no multi-line body content crosses the shell boundary. A genuinely plan-less caller passes --plan-id NO_PLAN and uses the same store (see § "The NO_PLAN sentinel" below). --plan-id is required; there is no second body channel. --label is repeatable and passes through to the created PR (e.g. --label skip-bot-review).

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci pr safe-merge \
  (--pr-number PR_NUMBER | --head HEAD) [--strategy merge|squash|rebase] [--delete-branch] \
  [--admin-merge-on-stuck-state] [--poll-timeout SECONDS] [--poll-interval SECONDS]

pr safe-merge polls readiness before merging; --admin-merge-on-stuck-state (the GitHub-only stuck-state --admin fallback) has no effect on GitLab.

Every immediate-merge verb refuses a platform-queued target. The immediate-merge set is exactly pr merge and pr safe-merge: each runs a platform-queue preflight before acting and refuses — status: error, naming both remedies (route through ci pr merge-queue, or reconcile the plan's use_merge_queue step param via /marshall-steward) — when the platform requires the queue. The preflight's scope follows the provider's own scoping of the feature: on GitHub it probes the PR's base branch for a required merge queue, and on GitLab it probes the project for enabled merge trains. It exists on both providers and fails closed on any resolution failure; the --admin-merge-on-stuck-state fallback is GitHub-only, the preflight is not. pr auto-merge is not an immediate-merge verb and never refuses a queued target: it runs the same read-only probe and reports which of the two dispositions the platform actually performed (disposition: enabled | enqueued). It still fails closed on the probe itself — an unresolvable queue state is returned as status: error, never a guessed disposition. See standards/pr-operations.md § "The corroborate-not-report contract".

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci pr merge-queue \
  (--pr-number PR_NUMBER | --head HEAD)

pr merge-queue enqueues the PR into the platform merge queue so the platform re-tests-and-merges against the latest base, serializing a truly-external commit the session-scoped merge mutex cannot. It takes no --strategy or --delete-branch flag: the merge queue's own branch-protection configuration dictates the merge method, GitHub rejects --delete-branch when a merge queue is enabled, and the platform auto-deletes the head branch after the queue merge. On GitHub it engages the merge queue via gh pr merge --auto; on GitLab it performs a real merge-train enqueue via POST /projects/:id/merge_trains/merge_requests/:iid. On GitLab the merge train is a Premium/Ultimate-tier feature enabled per-project.

On BOTH providers the verb returns the actionable ineligible error rather than silently falling back to an immediate merge. On GitLab it fires when the project/tier does not offer merge trains; on GitHub it fires when the PR's base branch has no configured merge queue, where gh pr merge --auto would otherwise exit zero having quietly enabled plain auto-merge instead of enqueuing anything. That base-branch probe is the pre-condition, not evidence of membership. enqueued is true only when the PR's own place in the queue was observed, in provider-shaped form: GitHub reads the base branch's merge-queue entry list after the enqueue call (paginated to its end) and, when that list does not carry the PR, the PR's own mergeQueueEntry / autoMergeRequest state; it returns enqueued: true only when one of those reads shows the PR in the queue, naming the read(s) in enqueue_observation — otherwise enqueued: indeterminate with enqueue_unobserved_reason (membership_read_failed, entries_incomplete, auto_merge_armed_awaiting_checks, or pr_not_listed), never true; the probe's verdict is returned as queue_precondition. GitLab's dedicated train endpoint only succeeds against a real train and returns the created car, reported as merge_train_car_id; the GitLab arm returns enqueued: true on that success and never indeterminate. See standards/pr-operations.md § "Workflow: Merge-Queue PR".

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci pr queue-state \
  --pr-number PR_NUMBER

pr queue-state reports where a PR stands relative to the merge queue, from one read: pr_state (open / merged / closed), merge_commit_sha (an explicit null when the PR has no landing commit), in_queue, queue_position, queue_entry_state, auto_merge_armed, and a merge_group_run block (found, run_id, status, conclusion, url) for the newest merge_group-event workflow run whose head branch belongs to the PR. in_queue, auto_merge_armed and merge_group_run.found are tri-state — true / false / indeterminate — and a read that failed is indeterminate, never false. A PR that could not be read returns status: error, error: pr_read_failed. It takes no verb-level --plan-id; a --plan-id for it is the router flag and goes before the verb.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci pr wait-for-queue-settle \
  --pr-number PR_NUMBER [--timeout SECONDS] [--interval SECONDS]

pr wait-for-queue-settle polls that same observation until the PR leaves the queue or --timeout elapses, and returns settle from the closed set merged / closed / dequeued / timeout beside timed_out, duration_sec, polls, and the baseline and final observations. An indeterminate read never yields dequeued. The deadline is status: success with timed_out: true and settle: timeout — there is no status: timeout. A PR read failure returns error: pr_read_failed. The router --plan-id placement is the same as for pr queue-state.

Both verbs are GitHub-only — registered on the GitHub front-end like pr landing-state, so the GitLab surface rejects them. See standards/pr-operations.md § "Workflow: Merge-Queue State and Settle Wait" for the full contract.

checks

Sub-verbs: status, wait, rerun, logs, wait-for-status-flip, pull-request-runs.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci checks status \
  [--pr-number PR_NUMBER] [--head HEAD] [--error-style maven|gradle|npm|generic]

status and wait accept --error-style (default generic) to select how the auto-downloaded failure log is filtered when any check fails. See § "Automatic Failure-Log Download on checks wait / checks status" above.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci checks pull-request-runs \
  --pr-number PR_NUMBER

checks pull-request-runs is a pure read answering one PR-wide question: does ANY pull_request-event workflow run exist for the requested PR? The head branch is how the runs are fetched, not what the answer is scoped to — a run is excluded only when its pull_requests association reliably names a different PR. It is the observable behind the not_triggered review-participation state — when no such run exists, nothing ever ran on account of the PR, so no review bot could have published and a required bot's silence says nothing about that bot. It returns has_pull_request_run and its exact complement not_triggered, alongside head_branch, run_count, and pull_request_run_count. The predicate is existence only: a run that exists and concluded skipped keeps not_triggered false, because the workflow was triggered; no conclusion and no timestamp is consulted; and mergeable_state is never read.

GitHub only — the GitLab arm refuses explicitly. The verb's parser lives in the shared ci_base.build_parser, so the token resolves on both providers; GitLab registers a handler that returns a structured status: error naming the gap rather than leaving the token to surface as an unrecognised subcommand (which would misattribute a provider-coverage gap as a parser defect). The gap is genuine and deliberately not guessed at: GitLab distinguishes a merge-request pipeline by its source (merge_request_event) on a different endpoint, and that does not map one-to-one onto "a pull_request-event run exists", so inferring an equivalent would yield a not_triggered verdict from a signal that does not mean what the caller thinks. not_triggered is therefore derivable on GitHub only until the mapping is designed. See workflow-integration-github/SKILL.md § Canonical invocations → github_pr pull_request_runs for the full return contract and the fail-loud obligations, which are not restated here.

issue

Sub-verbs: create, comment, prepare-body, prepare-comment, view, close, wait-for-close, wait-for-label.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci issue create \
  --title TITLE --plan-id PLAN_ID [--labels LABELS] [--slot SLOT]
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci issue comment \
  --issue ISSUE --plan-id PLAN_ID [--slot SLOT]

branch

Sub-verb: delete.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci branch delete \
  --remote-only --branch BRANCH

repo

Three nouns, each grouping its own sub-verbs (the 3-level repo {noun} {sub-verb} shape):

  • merge-queue → probe, enable
  • label → ensure, list
  • file → read
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci repo merge-queue probe
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci repo merge-queue enable

repo merge-queue probe reports the platform merge-queue eligibility as one of the shared discriminators — eligible_configured, eligible_unconfigured, ineligible, or unsupported. repo merge-queue enable configures the platform merge queue (GitHub: a merge_queue ruleset on the default branch; GitLab: the per-project merge_trains_enabled setting) and is idempotent — an already-configured repo is left unchanged. Both verbs return the actionable error (never a stack trace) on an auth-scope failure, and enable refuses with the actionable ineligible message when the platform gates the feature off. On GitHub, enable additionally refuses with an actionable merge_group message when the target repo's .github/workflows carry no workflow that triggers on merge_group — provisioning the queue anyway would stall it and block all merges to the default branch.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci repo label ensure \
  --label LABEL [--color HEX] [--description TEXT]

repo label ensure guarantees the named repository label exists — create-if-missing and idempotent (an existing label is a no-op success). On GitHub it uses gh label create --force (which updates in place rather than erroring on a duplicate); on GitLab it treats an "already exists" / HTTP 409 as a no-op success. --color is a 6-hex-digit RGB string (no leading #; the GitLab handler prefixes # as that platform requires). The steward landing cycle calls this to ensure the skip-bot-review label exists before creating a --label skip-bot-review PR.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci repo label list \
  [--repo OWNER/NAME]

repo label list is read-only: every label of the repository (default: the repository of the routed working tree), all pages, as labels[]{name,color,description} beside count and the provider's own total_count. It is a population verb and reports completeness the way the org verbs below do.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci repo file read \
  --repo OWNER/NAME --path PATH [--ref REF]

repo file read is read-only: one file of any repository the provider credentials can see, at the repository's default branch (ref_source: default_branch) or at --ref (ref_source: explicit). Its answer has three shapes and they never collapse into one another:

Shape Meaning
status: success, state: found The file exists; content carries it verbatim as a block scalar, with sha and size. An empty file is found with empty content.
status: success, state: not_found Nothing is at that path. not_found_reason is path_absent (the repository was shown readable and the path is missing) or repository_empty.
status: error The question was not answered: repository_not_accessible, ref_not_found, not_a_file (a directory or submodule), content_unavailable (past the contents-API size limit), undecodable_content (not UTF-8), or a failed read.

A GitHub 404 is ambiguous between a missing path and a repository the credentials cannot see, so the verb attributes it to the PATH only after reading the repository itself — an unreadable repository is never reported as an absent file.

org

Sub-verbs: list-repos, search-code. Both are read-only, name their target with --org (so they reach beyond the routed checkout), and take no verb-level --plan-id — a --plan-id for them is the router flag and goes before the verb.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci org list-repos \
  --org ORG

org list-repos returns every repository of the organization, all pages, as repos[]{name,full_name,archived,fork,visibility,default_branch} beside count, archived_count, and the provider's own total_count.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci org search-code \
  --org ORG --query LITERAL

org search-code returns the indexed files GitHub's code search matches for the literal, sent as one quoted phrase (a query carrying a double quote is refused). GitHub's code search matches whole tokens and ignores most punctuation, so a hit is a token-phrase match rather than a guaranteed exact substring, and a zero does not rule out the literal occurring inside a longer token. The result is returned as matches[]{repository,path,sha} beside count, repository_count, the provider's total_count and incomplete_results flag, and scope: default_branch_index — GitHub's code index holds each repository's default branch only, so the result states the population it was computed over.

Completeness is part of every population answer. repo label list, org list-repos and org search-code return status: success only when the rows read ARE the whole population. A listing that demonstrably is not returns status: incomplete — a distinct member, so a caller branching on status alone never mistakes a partial listing for a whole one — carrying the rows that were read, complete: false, and an incomplete_reason from the closed set page_bound_reached / count_mismatch / provider_incomplete_results / result_cap_reached (ci_base.INCOMPLETE_REASONS). A page that could not be read at all is status: error; its rows are not returned.

GitHub only. The parsers live in the shared ci_base.build_parser, so the tokens resolve on GitLab too; the GitLab arm registers a handler for each of the four read verbs (org list-repos, org search-code, repo file read, repo label list) that returns status: error with error: not_supported rather than an empty success or an unrecognised-subcommand parser error.

barrier

Provider-agnostic verb — handled by the ci.py router directly (no provider dispatch, no CI provider required, no worktree resolution). It is the coordinator for the phase-6 concurrent finalize-wait barrier: given the one settled HEAD and the current state of each awaited signal, it computes the per-signal-proceed / bounded-re-settle decision. Pure computation, implemented in scripts/_ci_barrier.py.

python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci barrier \
  --settled-head SHA --signal NAME:STATE[:HEAD] [--signal NAME:STATE[:HEAD] ...]

--settled-head is the single HEAD sha the barrier polls off. --signal is repeatable — one per awaited signal (ci, review, sonar) — as NAME:STATE[:HEAD], where STATE is one of pending|settled|failed and HEAD is the sha the signal was last observed against (omit for an unobserved signal). It returns barrier_status ∈ {complete, waiting, failed, re_settle} plus the per-bucket signal-name lists proceed / pending / failed / affected. re_settle names the affected arms to re-enter against the new settled HEAD after a bounded re-settle push (affected signals only, never a full finalize replay). See phase-6-finalize/SKILL.md § "Wait-region: the concurrent barrier off one settled HEAD" for the consuming narrative.

References

  • standards/architecture.md - Static routing and skill boundaries
  • standards/api-contract.md - Shared TOON output formats
  • standards/github-impl.md - GitHub-specific implementation
  • standards/gitlab-impl.md - GitLab-specific implementation