CI provider abstraction with unified API for GitHub and GitLab operations (PR, issues, CI status)
Unified CI provider abstraction using static routing - one script per provider, config stores full commands.
Execution mode: Run scripts exactly as documented; parse TOON output for status and route accordingly.
Prohibited actions:
gh or glab directly; all CI operations go through the script APIgh/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:
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:{script} {command} {args}ci leaf subcommand whose exact flags you do not already know, Read standards/leaf-command-reference.md (or the relevant group standard). Never guessThe 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.
ci verb reports failure as status: error at exit 0 โ by designThis 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.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.
pull_request-run observable (checks pull-request-runs) behind the not_triggered review-participation state โ GitHub only; the GitLab arm refuses explicitlyerror: not_supported)error: not_supportedThis skill is a script-only library (not registered in plugin.json). It is consumed by:
workflow-integration-github โ GitHub PR review comment workflowsworkflow-integration-gitlab โ GitLab MR review comment workflowsworkflow-integration-git โ git commit workflowsworkflow-pr-doctor โ PR diagnosis workflowsphase-6-finalize โ plan finalization with PR creationStatic 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
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).
| 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) |
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 |
--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).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).
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:
status.json, because the
plan-existence check treats a directory without status.json as absent.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.--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.
checks wait / checks statusWhen 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 selectorBoth 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.
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.
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)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.
Sub-verbs: view, list, landing-state, 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".
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.
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]
Sub-verb: delete.
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci branch delete \
--remote-only --branch BRANCH
Three nouns, each grouping its own sub-verbs (the 3-level repo {noun} {sub-verb} shape):
merge-queue โ probe, enablelabel โ ensure, listfile โ readpython3 .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.
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.
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.
standards/architecture.md - Static routing and skill boundariesstandards/api-contract.md - Shared TOON output formatsstandards/github-impl.md - GitHub-specific implementationstandards/gitlab-impl.md - GitLab-specific implementation