tools-integration-ci
CI provider abstraction with unified API for GitHub and GitLab operations (PR, issues, CI status)
SKILL.mdmarketplace/bundles/plan-marshall/skills/tools-integration-ci/SKILL.md
GitHubTools 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
ghorglabdirectly; 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/glabflag names from memory when invokingcileaf subcommands — flag names diverge from the underlying tools (e.g.,ci pr mergeuses--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
cileaf subcommand whose exact flags you do not already know, Readstandards/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 withserialize_toon, and returnsEXIT_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(closingprint(serialize_toon(result, …))/return 0at lines 1973-1974) andgitlab_ops.py:2684(closing at lines 2761-2762). Each endsdispatch → print → return 0with no branch on the dispatched result'sstatus, 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 thenot_triggeredreview-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 workflowsworkflow-integration-gitlab— GitLab MR review comment workflowsworkflow-integration-git— git commit workflowsworkflow-pr-doctor— PR diagnosis workflowsphase-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 Xand--project-dir Ytogether — errormutually_exclusive_args. Pick one.--plan-id Xonly — auto-resolve throughfile_ops.resolve_plan_context, which owns the singlemanage-status get-worktree-pathinvocation. Whenuse_worktree=truethe persisted worktree path is used; when the query succeeds and reportsuse_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, oruse_worktree=truewith an empty persisted path — the resolver raisesWorktreeResolutionError, and the router emits a structured TOON error and exits 2. It does NOT degrade to the main checkout.--plan-id NO_PLANonly — the plan-less sentinel. Binds to the main checkout, always; the sentinel never resolves to a worktree.--project-dir Yonly — 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/glabsubprocess 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 withoutstatus.jsonas 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.--slotdisambiguates concurrent bodies exactly as for a real plan. - Use it only when the caller genuinely has no plan. A
--plan-idthat failed to resolve returnsplan_not_foundcarrying both a sentinelhintand ahint_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,enablelabel→ensure,listfile→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 boundariesstandards/api-contract.md- Shared TOON output formatsstandards/github-impl.md- GitHub-specific implementationstandards/gitlab-impl.md- GitLab-specific implementation