@soleur/opencode 1.0.0 → 3.354.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/go.md +6 -5
- package/commands/sync.md +2 -2
- package/install.js +6 -85
- package/lib/harness.ts +1 -1
- package/lib/heartbeat-manifest.ts +19 -0
- package/lib/pr-merge-poll.ts +2 -2
- package/lib/workflow-fidelity.ts +2 -2
- package/opencode/INSTRUCTIONS.md +19 -0
- package/opencode/plugin.ts +343 -18
- package/package.json +1 -1
- package/scripts/admin-merge-ready.sh +1 -1
- package/scripts/admin-merge-ready.test.sh +0 -0
- package/scripts/affected-tests.sh +0 -0
- package/scripts/alpha-metrics.sh +0 -0
- package/scripts/alpha-metrics.test.sh +0 -0
- package/scripts/check-red-on-main.sh +0 -0
- package/scripts/ci-head-verdict.sh +54 -6
- package/scripts/cloud-detect.sh +0 -0
- package/scripts/delegation-verify.sh +0 -0
- package/scripts/delegation-verify.test.sh +0 -0
- package/scripts/deploy-arm.sh +0 -0
- package/scripts/domain-model-drift.sh +0 -0
- package/scripts/emit-decision.sh +0 -0
- package/scripts/emit-decision.test.sh +0 -0
- package/scripts/ext-filter-divergence.sh +0 -0
- package/scripts/grok-fidelity-gate.sh +0 -0
- package/scripts/grok-pre-push-gate.sh +0 -0
- package/scripts/install-opencode-adapter.ts +118 -129
- package/scripts/lib/affected-tests/common.sh +0 -0
- package/scripts/lib/affected-tests/go.sh +0 -0
- package/scripts/lib/affected-tests/js.sh +0 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +0 -0
- package/scripts/lib/affected-tests/python.sh +0 -0
- package/scripts/lib/session-state.sh +0 -0
- package/scripts/lib/tmp-classify.sh +0 -0
- package/scripts/monitor-pr-checks.sh +1 -1
- package/scripts/pipeline-tally.sh +0 -0
- package/scripts/plugin-version.sh +0 -0
- package/scripts/precommit-guard.sh +3 -1
- package/scripts/precommit-guard.test.sh +0 -0
- package/scripts/render-c4-model.sh +0 -0
- package/scripts/resolve-regenerable-conflicts.sh +0 -0
- package/scripts/run-in-pid-namespace.sh +0 -0
- package/scripts/setup-ralph-loop.sh +0 -0
- package/scripts/sync-npm-package.ts +74 -87
- package/scripts/sync-opencode-adapter.ts +19 -0
- package/scripts/sync-pr-behind.sh +183 -70
- package/scripts/taste-profile-update.sh +0 -0
- package/scripts/taste-profile-update.test.sh +0 -0
- package/scripts/write-budget-marker.sh +0 -0
- package/skills/deepen-plan/SKILL.md +1 -1
- package/skills/deploy/references/hetzner-setup.md +5 -3
- package/skills/drain-prs/SKILL.md +3 -2
- package/skills/fix-issue/SKILL.md +8 -8
- package/skills/fix-issue/references/agent-authored-exclusion.md +4 -2
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +4 -3
- package/skills/merge-pr/SKILL.md +5 -3
- package/skills/model-launch-review/SKILL.md +4 -4
- package/skills/one-shot/SKILL.md +1 -1
- package/skills/plan/SKILL.md +1 -1
- package/skills/plan/references/plan-sharp-edges.md +7 -1
- package/skills/postmerge/SKILL.md +2 -2
- package/skills/postmerge/references/deploy-status-debugging.md +3 -0
- package/skills/review/SKILL.md +14 -15
- package/skills/review/references/review-evidence-comment.md +36 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +2 -0
- package/skills/ship/SKILL.md +14 -13
- package/skills/ship/references/cleanup-merged-archival.md +39 -0
- package/skills/ship/references/delegated-act-authoring-form.md +7 -0
- package/skills/ship/references/epic-close-sweep.md +62 -0
- package/skills/ship/references/merge-queue-dequeue.md +1 -1
- package/skills/ship/references/operator-holds.md +30 -0
- package/skills/ship/references/queue-mode.md +1 -1
- package/skills/ship/references/ready-run-wait.md +3 -3
- package/skills/test-fix-loop/SKILL.md +1 -1
- package/skills/verification-scaffold/SKILL.md +73 -0
- package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
- package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
- package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
- package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
- package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
- package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
- package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
- package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
- package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
- package/skills/verification-scaffold/references/guards.tsv +21 -0
- package/skills/verification-scaffold/references/verification-readme.template +55 -0
- package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
- package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
- package/skills/verification-scaffold/test/generator.test.sh +319 -0
- package/skills/verification-scaffold/test/manifest.test.sh +144 -0
- package/skills/work/SKILL.md +2 -1
package/skills/plan/SKILL.md
CHANGED
|
@@ -883,7 +883,7 @@ end
|
|
|
883
883
|
- [ ] Include prompts or instructions that worked well during research
|
|
884
884
|
- [ ] Note which AI tools were used for initial exploration (Claude, Copilot, etc.)
|
|
885
885
|
- [ ] Emphasize comprehensive testing given rapid implementation
|
|
886
|
-
- [ ]
|
|
886
|
+
- [ ] Name any AI-generated file and the concern that warrants a closer `soleur:review` pass
|
|
887
887
|
|
|
888
888
|
### 6. Final Review & Submission
|
|
889
889
|
|
|
@@ -308,7 +308,7 @@ NEVER CODE! Just research and write the plan.
|
|
|
308
308
|
|
|
309
309
|
- **A Guard Contract that pins prose by its LEAD-IN pins the heading, not the rule.** Pin the load-bearing clause (the link plus the pointer and the "only X stays" tail), strip fences and HTML comments before matching, and give any discoverability `grep -c` the same anchor; then mutate by GUTTING the body, not only deleting the anchor. **Why:** #10499 - gutting a paragraph body after its bold lead-in stayed green through the author's battery; see `knowledge-base/project/learnings/2026-10-18-a-presence-pin-on-a-lead-in-survives-the-paragraph-being-gutted.md`.
|
|
310
310
|
|
|
311
|
-
- **`decision-challenges.md` rows are machine-gated: a technical ruling must start exactly `Ruled by CTO agent, <date>:` followed by one space (nothing between the date and the colon, so no parenthetical) and a row may not contain a literal `|` (write `\|` or reword).** Run the ship skill's `technical-ruling-gate.sh --branch` (in its `scripts/` directory) before returning the plan; `pending - assumed` is not a ruling. **Why:** PR #10572 — the gate failed on all three shapes in one session. See `knowledge-base/project/learnings/2026-10-19-the-audit-a-ratchet-trusts-reached-less-than-its-name-and-both-p1s-were-in-the-fixes.md`.
|
|
311
|
+
- **`decision-challenges.md` rows are machine-gated: a technical ruling must start exactly `Ruled by CTO agent, <date>:` followed by one space (nothing between the date and the colon, so no parenthetical) and a row may not contain a literal `|` (write `\|` or reword).** The TABLE is gated too — the header must be exactly `| Item | Class | Ruling |` (a 5-col variant dies as `@BADHEADER`, exit 2), every row is exactly 3 cells, and a row after a blank line sits OUTSIDE the table (`@STRAY`, exit 2) — append rulings inside the canonical table, never below the section. Run the ship skill's `technical-ruling-gate.sh --branch` (in its `scripts/` directory) before returning the plan; `pending - assumed` is not a ruling. **Why:** PR #10572 — the gate failed on all three shapes in one session; #10404 shipped a 5-col table with two stray rows appended below it (fixed at b33c34d2c4). See `knowledge-base/project/learnings/2026-10-19-the-audit-a-ratchet-trusts-reached-less-than-its-name-and-both-p1s-were-in-the-fixes.md`.
|
|
312
312
|
|
|
313
313
|
- **A baseline drawdown is ONE commit, because the gate compares the baseline to the live working-tree census.** `lint-ratchet.py --check` and the lefthook ratchet hooks require baseline == live census, so a plan that prescribes per-area commits each staging a shrunk baseline cannot be executed once the areas are fixed together; plan one commit (per-area structure is review-aid only in a squash-merge repo), and name any test that anchors on the exact text of a region you will re-indent. **Why:** #10433 — see `knowledge-base/project/learnings/2026-10-19-a-per-area-drawdown-cannot-satisfy-a-live-census-gate-and-reindenting-breaks-text-anchored-mutants.md`.
|
|
314
314
|
|
|
@@ -323,3 +323,9 @@ NEVER CODE! Just research and write the plan.
|
|
|
323
323
|
- **A negative control that removes "the link" cannot red a census keyed on basename substring — enumerate the keyed string's every site and strip them ALL, or probe the predicate directly.** `components.test.ts`'s references-reachability check is `haystack.some(t => t.includes(f))` over every other file in the skill dir, so prose path mentions (`docs/feature-map.md`, `apps/…/feature-map.md`) satisfy it with no link at all; a control that deletes only the markdown pointer leaves the basename alive and reads "linked" forever. Before prescribing the mutation, `grep -rn '<basename>' <skill-dir>` and make the control remove the whole hit set (or run a scratch haystack that never contained it). The control must remove what the predicate READS, not what the diff added — for a substring census that is every occurrence; for a line-anchored parser it is the one conforming line. **Why:** #10402 — the #10401-derived "remove the pointer" control prescribed in tasks.md 3.2 could never red `test-browser/references/feature-map.md` (basename survives in 5 prose mentions); both `soleur:work` and `soleur:qa` hit it independently. See `knowledge-base/project/learnings/2026-10-21-a-remove-the-link-control-cannot-red-a-basename-substring-census.md`.
|
|
324
324
|
|
|
325
325
|
- **A plan that adds a conditional row to a spec — a guard, a flag, a termination-table row, a success verdict — owes two vacuity checks: can the trigger ever be true under the mechanism's own invariants, and does a success/screened-out row require ≥1 qualifying item as proof?** A trigger keyed on an intersection that is empty by construction never fires (#10403: a "possible diff-interaction" flag keyed on flaked-test path ∩ the loop's own commits, in a loop that never edits test files — re-specified on timing and exercised-surface signals), and a row an empty input set satisfies greens on nothing (the same PR's "all failures screened out" row fired on an unparseable `rc≠0` run with zero quarantines — now requires ≥1 quarantine, and the zero-failure parse falls through). Apply both checks to rows added in a review-fix round too — the second vacuity was introduced remediating the first batch. **Why:** #10403/PR #10738 — two vacuous-row findings across the main review and the fix round. See `knowledge-base/project/learnings/workflow-patterns/2026-10-21-a-guard-that-cannot-fire-and-a-success-row-that-proves-nothing.md`.
|
|
326
|
+
|
|
327
|
+
- **A plan or ruling that makes an act delegable, or that prescribes a CLI field or flag in prose, owes one run of the exact line before it ships.** Run `delegation-verify.sh` on the act's literal command (it denies any `-f confirm=` or `rollback_ack*` input, so a flip with a typo-guard can never be envelope-delegable), and run each `gh`/CLI shape the prose prescribes once read-only (`gh issue view --json authorAssociation` is an unknown field; the REST field is `author_association` via `gh api`). A reviewer reading the policy cannot see either defect. **Why:** #10783 — A3 was ruled delegable while the verifier denies its `confirm=` input (five seats found it), and a hold-routing step read a field that does not exist. See `knowledge-base/project/learnings/workflow-patterns/2026-10-22-a-delegability-claim-nobody-ran-through-the-verifier-and-fixes-that-each-needed-a-cold-run.md`.
|
|
328
|
+
|
|
329
|
+
- **Write the guard-contract mutation table directly under a bold `**Mutation matrix.**` line.** `lint-guard-contract.py` counts rows only after that field marker, so an unlabelled table reads as 0 rows and the contract check fails for a reason the table does not show. **Why:** #10455 plan (2026-10-20).
|
|
330
|
+
- **A causal attribution in a plan ("since #N, X happens", "PR #N introduced the capture") names what #N CONTAINS, not what it CHANGED — verify it with `git log -S <mechanism>` and read what the commit removed or moved before it is written, because the sentence then propagates into headers, ADRs and test comments.** The fix for an observation-channel loss is only as good as the date the channel changed: #10786 credited #8763 with introducing a per-suite capture that dates from #7423; #8763 only moved the rehearsal suite out of its own workflow step into that already-capturing runner. A sweep of the corrected claim must grep its PARAPHRASES across the touched files, not the one phrasing. **Why:** #10786 — six artifacts carried the wrong attribution until the history seat falsified it. See `knowledge-base/project/learnings/2026-10-22-a-probe-went-blind-when-its-suite-moved-into-the-capturing-runner-not-when-capture-began.md`.
|
|
331
|
+
- **A canary plan names the OTHER jobs that will be red on its PR shape before it claims a run-level result.** A throwaway DRAFT PR carries the draft-light `test` leg, which concludes RED on every draft by design (ADR-276 Option R), so it can show how one job renders but never what the run concludes; either design the canary so nothing else is red, or state the claim at the level the canary can read. Verify the canary fixture trips the real check locally before pushing it. **Why:** #10851 — the `affected-index-ratchet` canary measured the job-level `failure` but its run conclusion was confounded, after four sites had asserted "continue-on-error keeps the run green" unmeasured. See `knowledge-base/project/learnings/2026-10-23-an-advisory-job-claimed-to-keep-the-run-green-before-anyone-measured-it-and-the-canary-could-not-isolate-it.md`.
|
|
@@ -493,7 +493,7 @@ reports; it never dispatches a production apply on its own.
|
|
|
493
493
|
2. **Find the merge-SHA run and wait for it:**
|
|
494
494
|
`gh run list --workflow apply-web-platform-infra.yml --commit <merge-sha> --limit 5 --json databaseId,status,conclusion`. An empty
|
|
495
495
|
list right after the merge is retried (every 30 s, up to 10 min), not read as "no run". Watch the id under Monitor
|
|
496
|
-
(`gh run watch <id>`; never `run_in_background` polling), bounded to about
|
|
496
|
+
(`gh run watch <id>`; never `run_in_background` polling), bounded to about 65 min (the job's `timeout-minutes` is 53, so keep the bound above it; plus queueing
|
|
497
497
|
on the shared concurrency group). `success` is read back. `cancelled`, `timed_out`, `startup_failure`, `skipped`,
|
|
498
498
|
`failure` or no run within the bound mean **not delivered or superseded**: read back anyway (a merge-SHA run can be
|
|
499
499
|
`cancelled` by the shared concurrency group and still have delivered); the read-back is the arbiter.
|
|
@@ -524,7 +524,7 @@ gh issue create --label type/chore --milestone "Post-MVP / Later" \
|
|
|
524
524
|
`gh workflow run apply-web-platform-infra.yml --ref main -f apply_target=manual-rerun -f reason='deliver sole-copy protection'`.
|
|
525
525
|
After a go-ahead and the dispatch: note the newest run id first, then
|
|
526
526
|
`gh run list --workflow apply-web-platform-infra.yml --event workflow_dispatch --limit 1 --json databaseId,status`
|
|
527
|
-
(an empty or unchanged id is stale: retry), watch it under Monitor bounded to about
|
|
527
|
+
(an empty or unchanged id is stale: retry), watch it under Monitor bounded to about 65 min (above the apply job's 53-min ceiling), then read back again.
|
|
528
528
|
`success` is read back; `cancelled`, `timed_out`, `startup_failure`, `skipped` or no run within the bound mean not
|
|
529
529
|
delivered or superseded: read back anyway, and if it is still `not-protected` recommend the dispatch again (go-ahead).
|
|
530
530
|
6. **On a `protected` read-back that matches the expected word, and only while ADR-282's status is still `adopting`, file ONE tracking issue** so the status flip does not depend
|
|
@@ -54,6 +54,9 @@ The `soleur:ship` Phase 5.5 "Deploy Pipeline Fix Drift Gate" surfaces this contr
|
|
|
54
54
|
| `canary_sandbox_failed` | 1 | bwrap verification failed on canary | Read the `DEPLOY_ROLLBACK: bwrap sandbox non-functional` row without SSH: `knowledge-base/engineering/operations/runbooks/canary-probe-set.md#blocking-bwrap-sandbox-probe--reading-its-self-report-8016-pr-8026` |
|
|
55
55
|
| `canary_sandbox_failed` | 1 | stderr `bwrap: Unexpected capabilities but not setuid, old file caps config?` (read `bwrap_err=` on the `DEPLOY_ROLLBACK: bwrap sandbox non-functional` journald row, Better Stack tag `ci-deploy` — the deploy-status JSON carries only `reason`) — a file-cap'd `/usr/bin/bwrap` OR ambient/bounding capabilities reaching a non-root exec (e.g. a stale installed `ci-deploy.sh` still passing `--cap-add SYS_ADMIN`); released bwrap 0.8–0.12 aborts on `real_uid != 0 && has_caps()` (upstream has no file-cap support) | Merged is not delivered: the host runs the INSTALLED `ci-deploy.sh` until `apply-deploy-pipeline-fix.yml` (push-triggered on `apps/web-platform/infra/ci-deploy.sh`; `workflow_dispatch` when the push arm is cancelled) delivers it — verify by comparing `.ci_deploy_sha256` in the deploy-status hook's JSON body against `git show origin/main:apps/web-platform/infra/ci-deploy.sh \| sha256sum` (or run `scripts/check-deploy-script-parity.sh`). If drifted, dispatch the apply; if the host script is current, the image itself still carries the `setcap` — confirm the deployed tag postdates the #9874 revert build. Resolution trail: incident #9871, apply run 37976212395, green deploy run 37980286319 |
|
|
56
56
|
| `canary_sandbox_failed` | 1 | stderr `/usr/local/bin/bwrap: line 424: /usr/bin/bwrap: Operation not permitted` (the shim's final `exec`; inner rc 126; read `bwrap_err=` on the `DEPLOY_ROLLBACK: bwrap sandbox non-functional` journald row, Better Stack tag `ci-deploy`) — execve EPERM: file caps on the real binary not covered by the container bounding set (capped image + clean container, no `--cap-add`) | Deploy the cap-free image tag (post-#9874 build — in #9871 the fix landed as `v0.334.1` after a rerun of the upstream flaked `test-scripts` job re-armed the `ci_not_green`-skipped deploy). If the image is clean but `--cap-add` symptoms persist, the host script is stale: merged is not delivered — the installed `ci-deploy.sh` runs until `apply-deploy-pipeline-fix.yml` delivers it; verify `.ci_deploy_sha256` in the deploy-status hook's JSON body vs `git show origin/main:apps/web-platform/infra/ci-deploy.sh \| sha256sum` (or `scripts/check-deploy-script-parity.sh`) and re-dispatch the apply on drift |
|
|
57
|
+
| `canary_inngest_link_inspect_failed` | 1 | The Inngest link canary's `docker inspect` check failed: the canary's env was not exactly one https `INNGEST_BASE_URL` plus one pinned `NODE_EXTRA_CA_CERTS` (or the inspect call itself failed). The previous container keeps serving | Read the host's Better Stack `INNGEST_LINK: class=canary_failed level=error check=inspect cause=<token> url_lines= ...` row; decode per cause in `canary-probe-set.md` (Failure reads). No SSH |
|
|
58
|
+
| `canary_inngest_link_failed` | 1 | An in-container Inngest link canary check failed (health, events, send, registry or the negative control). The previous container keeps serving | Same row, `check=<name> cause=<token>`; see `canary-probe-set.md`. No SSH |
|
|
59
|
+
| `inngest_env_scrub_failed` | 1 | A flag-on deploy could not scrub `INNGEST_BASE_URL` / `NODE_EXTRA_CA_CERTS` from its env-file; aborted before any container started, the previous container keeps serving | Read `INNGEST_LINK: class=env_scrub_failed level=error stage=<read\|write\|recount>` and the per-stage action in `inngest-server.md` (Resolver events). `action=off` is always available |
|
|
57
60
|
| `canary_failed` | 1 | Canary health-check failed 10x | Roll back; inspect canary container logs |
|
|
58
61
|
| `production_start_failed` | 1 | Production container didn't start post-swap | Check docker state; consider manual rollback |
|
|
59
62
|
| `plugin_seed_mount_unsafe` | 1 | The plugin-mount seed REFUSED to run: `PLUGIN_MOUNT_DIR` was not non-empty/absolute/under `/mnt/data/` ending `/plugins/soleur`. The seed's container-side `find -delete` runs as root on the bind source, so a wrong-path env value would be a recursive delete on an arbitrary host path — this reason means the guard caught that, not that the copy failed. Emits the stderr/journald marker `PLUGIN_SEED_MOUNT_UNSAFE` | Check what set `PLUGIN_MOUNT_DIR` (webhook.service env, `/etc/default/webhook-deploy`, an infra-config delivery) — the deploy never attempted the seed, so nothing was deleted. The correct value is `/mnt/data/plugins/soleur` |
|
package/skills/review/SKILL.md
CHANGED
|
@@ -23,7 +23,7 @@ description: "This skill should be used when performing exhaustive code reviews
|
|
|
23
23
|
|
|
24
24
|
## Introduction
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Review here means this agent pipeline (`rf-never-skip-qa-review-before-merging`, `hr-technical-fork-is-not-an-operator-question`).
|
|
27
27
|
|
|
28
28
|
## Prerequisites
|
|
29
29
|
|
|
@@ -41,7 +41,6 @@ description: "This skill should be used when performing exhaustive code reviews
|
|
|
41
41
|
**Load project conventions:**
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
# Load project conventions
|
|
45
44
|
if [[ -f "CLAUDE.md" ]]; then
|
|
46
45
|
cat CLAUDE.md
|
|
47
46
|
fi
|
|
@@ -1095,13 +1094,8 @@ After emitting the marker, the calling skill's continuation gate takes over —
|
|
|
1095
1094
|
above). The script DERIVES `--mode` from the two counts, so a caller cannot label a
|
|
1096
1095
|
2-of-10 review `full`.
|
|
1097
1096
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
matter. Step 2 above tells you to skip the commit when there are no local
|
|
1101
|
-
changes — which is the *expected* case — so a review that finds nothing
|
|
1102
|
-
leaves no local evidence at all, and every downstream review-evidence gate
|
|
1103
|
-
then reads "review never ran" and denies the merge with no escape hatch
|
|
1104
|
-
(issue 6724).
|
|
1097
|
+
A script, not a described `git commit`: the prose form had zero measured compliance on zero-finding
|
|
1098
|
+
branches (issue 6724).
|
|
1105
1099
|
|
|
1106
1100
|
The script therefore commits `--allow-empty`. It is idempotent (a second
|
|
1107
1101
|
review pass will not stack a duplicate), it refuses to run on `main`/`master`
|
|
@@ -1111,6 +1105,9 @@ After emitting the marker, the calling skill's continuation gate takes over —
|
|
|
1111
1105
|
|
|
1112
1106
|
Run it even when step 2 committed something: the trailer is the durable
|
|
1113
1107
|
machine-readable signal, and the commit subject is only a legacy fallback.
|
|
1108
|
+
|
|
1109
|
+
**Then post the evidence a merge can see:** one `Review-Evidence:` PR comment, following
|
|
1110
|
+
[review-evidence-comment.md](./references/review-evidence-comment.md) (merge gate = agent review + green CI; no human approval).
|
|
1114
1111
|
4. **Continue to `soleur:ship` in the same turn — review is not a stopping point.**
|
|
1115
1112
|
Findings are fixed inline (§5), so a clean review means the PR is ready to go
|
|
1116
1113
|
out, not ready to be handed over. Invoke `skill: soleur:compound` then
|
|
@@ -1598,23 +1595,25 @@ Commit pre-review inline fixes (anti-slop scanner corrections, lint fixes, class
|
|
|
1598
1595
|
|
|
1599
1596
|
- **A review finding can be a REGRESSION — implement it, measure it, and if it breaks something, revert with the reason recorded in the CODE, not just the PR.** A finding from a strong agent is a hypothesis with evidence attached, not an instruction; the agent reasoned over the diff, not over every sibling suite the change can reach. The tell is a finding that proposes ADDING a probe (`git rev-parse …`, a status call, a readback) to a code path whose surrounding comments explain why that probe was deliberately avoided — read those comments before applying. And the fixes themselves need fixtures: **a review-driven fix is exactly as unpinned as the blind spot it closes**, because it is written after the tests and nothing forces coverage for it, so mutate each one back out on a sandbox copy and confirm the suite reddens before committing. A whole-file revert is ONE axis — the axis the suite was authored against — and it can report non-vacuous while individual fixes are deletable at full green. **Why:** #7394/PR #7407 — a 13-agent panel's P2 (a genuinely-bare repo under the config mask now takes the benign skip instead of the wedge) was implemented and broke the sibling suite: under that degradation `GIT_ROOT` is the relative `.git`, and `git -C .git rev-parse --is-bare-repository` returns true for ANY normal clone, re-opening the twice-fixed #5934 D3 wedge on the production surface. Separately, two of that PR's own highest-severity fixes (a data-destruction guard and a symlink refusal) survived mutation with the suite fully green. See `knowledge-base/project/learnings/2026-08-10-the-fix-for-a-blind-spot-was-as-unpinned-as-the-blind-spot.md`.
|
|
1600
1597
|
|
|
1601
|
-
- **An ARITHMETIC error makes a bash function RETURN, so every `cmd || _guard "…"` caller falls straight through — and the reassuring branch is downstream.** `$(( n + 1 ))` on a value with a LEADING ZERO is octal, so a stored `08`/`09` is an evaluation *error*, not a wrong number. Without `set -e` that error simply fails the assignment; the function returns instead of reaching its `exit`, and a caller written as `probe || _transient "gh failed"` continues into the success path. Measured: a follow-through probe printed `PASS: 0 loud skips across 0 observed run(s) (0 sampled)` and exited **0** with `gh` entirely broken — a clean bill
|
|
1598
|
+
- **An ARITHMETIC error makes a bash function RETURN, so every `cmd || _guard "…"` caller falls straight through — and the reassuring branch is downstream.** `$(( n + 1 ))` on a value with a LEADING ZERO is octal, so a stored `08`/`09` is an evaluation *error*, not a wrong number. Without `set -e` that error simply fails the assignment; the function returns instead of reaching its `exit`, and a caller written as `probe || _transient "gh failed"` continues into the success path. Measured: a follow-through probe printed `PASS: 0 loud skips across 0 observed run(s) (0 sampled)` and exited **0** with `gh` entirely broken — a clean bill over zero samples. Two rules: read counters from a file as `10#$n` behind a `[[ "$n" =~ ^[0-9]+$ ]]` shape check, and for any helper whose contract is "always exits", assert it cannot return — a guard that can return is one the caller can skip. Same family as the `grep -c`/`$?`-after-a-pipe traps above. Separately, a PINNED action SHA a review fix prescribes must be resolved from the registry (`gh api repos/<owner>/<repo>/git/refs/tags`) BEFORE writing — a memory-written SHA is unverifiable by any local gate. **Why:** #7574 — see `knowledge-base/project/learnings/2026-08-20-every-instrument-i-checked-my-own-work-with-was-broken.md`.
|
|
1602
1599
|
|
|
1603
|
-
- **When the PR's subject is a destructive tool, the SUITE is a destructive tool — and the arm is named after the feature, so nobody scopes it.** For any diff whose subject deletes, kills, revokes, signals or overwrites, read every test arm twice: once for "does the feature work",
|
|
1600
|
+
- **When the PR's subject is a destructive tool, the SUITE is a destructive tool — and the arm is named after the feature, so nobody scopes it.** For any diff whose subject deletes, kills, revokes, signals or overwrites, read every test arm twice: once for "does the feature work", once for "what does this arm do to the machine it runs on" — only the first question has a name in the file. Measured: an end-to-end arm called `AC40 incident trace` ran the tool's `reap` verb against the real `/proc` with no signal sink, no root seam, no pid restriction and an age floor of ZERO — every full-gate run performed an unattended box-wide reap, twice (the mutation battery's unmutated control re-runs the suite), wider than an operator could invoke by hand. A planted one-second-old bystander tree would have had three processes TERMed; the ADR asserted "nothing invokes reap automatically anywhere". Scope such an arm through the **shipped** authorization path, not a test-only bypass, and assert no invocation of the destructive verb lacks a seam, a sink, or an explicit target list. **Corollary for the harness, same root:** the positive-control rule applies to EVERY verdict-emitting helper — one that increments the assertion counter INSIDE itself (`expect_field() { …; cases=$((cases+1)); … }`) is invisible to the floor, the conservation check, and any pass/fail control not routing through it; dropping its verdict while keeping its count is byte-identical green. Measured: 48 of 136 verdicts unbacked, hiding two real detector mutants. Litmus: *does its own counter move inside it?* **Why:** #7537/PR #7641 — a nine-agent review found 30 findings (12 P1) in a change that was green on a 148-assertion suite, a 604-assertion 36-row mutation battery, both CI shards and shellcheck. See `knowledge-base/project/learnings/2026-08-20-my-test-suite-reaped-the-live-box-and-every-gate-was-green.md`.
|
|
1604
1601
|
|
|
1605
|
-
**Three ways a review's own instruments lie, all measured in one session.** (a) **State the deliverable at SPAWN time.** An agent told only on RESUME that its final message is the report can burn its whole budget emitting status lines — one seat returned
|
|
1602
|
+
**Three ways a review's own instruments lie, all measured in one session.** (a) **State the deliverable at SPAWN time.** An agent told only on RESUME that its final message is the report can burn its whole budget emitting status lines — one seat returned only status lines across two `SendMessage` resumes and its scope had to be re-run. Put "your final assistant message IS the deliverable; nothing you print to a tool log reaches me" in the SPAWN prompt. (b) **After fixing a sandbox escape, re-run and assert `git rev-parse HEAD` is UNCHANGED** — never infer from the guard's presence. A `cd`-escape fix closing the two sites behind the *visible* damage was declared verified and escaped again next run — the four sites that actually COMMIT were different ones. (c) **A baseline taken while another process mutates the tree is VOID**, not a finding: a `TEST_GROUP=scripts` run reporting 35 failed suites overlapped a sibling committing into the worktree — every number was noise. Same family, cheapest instance: a `diff -q "$BAK" "$F" && echo NOT-LANDED || echo landed` guard prints "landed" for a MISSING file, so assert the operand exists before diffing. **Why:** #7546. See `knowledge-base/project/learnings/2026-08-20-every-guard-i-fixed-was-narrower-than-the-claim-it-carried.md`.
|
|
1606
1603
|
|
|
1607
|
-
- **The reviewer's own verification commands are not covered by the review — and the finding you are checking is the one you are most likely to reproduce while checking it.** Every defect-class rule above is written about the code under review; none attaches to the greps and probes a reviewer types to confirm a finding,
|
|
1604
|
+
- **The reviewer's own verification commands are not covered by the review — and the finding you are checking is the one you are most likely to reproduce while checking it.** Every defect-class rule above is written about the code under review; none attaches to the greps and probes a reviewer types to confirm a finding, written fast as bookkeeping rather than authorship — and that is what makes a fix PR's new assertions its least-audited part, one level out. The failure is asymmetric because a broken instrument does not error, it **answers**: a search returns a line, a probe returns a count, a runner returns an exit code, each already shaped like a result. Litmus before reading any verdict: run the instrument against a case whose answer you already know — a search that must match, a probe that must fail. An instrument never shown to produce a positive has not returned a negative; it has returned silence. **Why:** #7898/PR #7986 — verifying a panel finding about `.env`-rebindable destinations, a bare-token search for `source "$env_file"` returned the reviewer's OWN prologue comment (which quotes that idiom in prose to explain the guard) and refuted a LIVE credential-exfiltration vector; anchored on `^[[:space:]]*source` it was real. Five more instrument bugs in the same session each produced a confident wrong reading — including a red that was a wrong `--project` selector, nearly filed as a regression. See `knowledge-base/project/learnings/2026-09-09-six-of-my-errors-were-the-class-i-was-reviewing-for.md`.
|
|
1608
1605
|
- **On a credential-handling diff, ask for the redactor's ORDER and for the env of every test that spawns the real script — and run the coverage consult on the absence in the list.** A cap placed before a scrub is a second input the regex was never fixtured against (a 19-char `sbp_` prefix survived the `{20,}` floor); a unit test that `spawnSync`s the real runner with `...process.env` inherits the operator's exported token and reaches the vendor; and the placement of the token itself — "in the prd root" — is a storage fact that the deploy script's unfiltered `secrets download … --env-file` turns into a container-env fact. Five seats converged on the first, one on the second, none on the third: it came from the coverage consult's "which class is ABSENT" question, which is why that consult is mandatory at ≥6 findings and its answer is a lead to MEASURE. **Why:** #8028/PR #8137. See `knowledge-base/project/learnings/security-issues/2026-09-14-the-plan-capped-the-message-before-it-redacted-it-and-a-config-is-not-a-consumer-boundary.md`.
|
|
1609
1606
|
- **A guard that pins emitted values leaves the generator unguarded — the enumeration seat must walk the production path and demand a pin per stage.** When a diff authors a drift guard over generated config (a locals map → flatten arms → a `dynamic` block → emitted items), content pins stay green while the generator itself is deleted: on #8358 removing one flatten arm or the whole `dynamic "item"` block would have killed 69 redirects with every existing guard passing. Walk locals → transforms → resource block → emitted items, including the template that derives downstream values (a `blog.json` permalink pattern that makes target URLs), and parse config with comments stripped — a config file's own comments can contain literal `item {` text that seeds phantom blocks in a naive structural parser. Adjacent holes the same walk caught: a byte-threshold "stub" predicate a layout-wrapped redirect escapes, and an unwatched `_redirects`/`_headers` channel no guard read. **Why:** #8358 — structural enumeration found all of these where six content-pinning guards were green. See `knowledge-base/project/learnings/workflow-issues/guard-content-not-mechanism-stub-deletion-20260919.md`.
|
|
1610
1607
|
- **Same-host, same-actor panels share an environment-shape blind spot by construction — and for a script that writes into a user's tree, "cleans up on failure" is a property of the WINDOW between the first write and success, never of the failure arms it names.** Ask the structural seat for an exit-site table by position relative to the first write, and ask the coverage consult its absent-class question; verify each lead against the diff before acting. **Why:** #8288/PR #8352 — six of nine seats converged on a merge-base 69 that fired after five emits and before any trap (five files left, next run 66), while none saw GNU-only tools in a generator that runs on stock macOS, tool stderr discarded before the 68/69 message, or a missing run lock — the consult named all three in one reply, and `FORCE_COLOR=1` turning a correct install into a 72 rollback came only from the test-design seat asking which producer shape the stub had never instantiated. See `knowledge-base/project/learnings/2026-09-19-cleanup-on-failure-is-a-property-of-the-window-not-the-arms.md`.
|
|
1611
1608
|
|
|
1612
|
-
- **A guard that BANS a spelling is not a guard that ESTABLISHES a property — and a new anti-vacuity floor owes THREE things, only one of which its own green run can see.** Two shapes from one guard-shaped PR. (a) Ask of every ban: *what does the property say, and does this predicate express THAT?* A rule banning the
|
|
1609
|
+
- **A guard that BANS a spelling is not a guard that ESTABLISHES a property — and a new anti-vacuity floor owes THREE things, only one of which its own green run can see.** Two shapes from one guard-shaped PR. (a) Ask of every ban: *what does the property say, and does this predicate express THAT?* A rule banning the wrong mechanism's presence is silent on the right one's absence, and the gap is usually occupied — a rule banning inline `authorAssociation` ran **rc 0** over a probe reading `.comments[].body` with NO filter (the verbatim #7448 forgery shape, strictly worse than what it banned) while `grep -rn authorAssociation` found zero live filters to ban. Prefer the positive obligation ("reads comments AND branches on a verdict => must call the lib"): it subsumes the ban and covers the spellings nobody enumerated. A rule green over the exact hole it advertises protection from is worse than the doc, because it converts "we wrote this down" into "we gated it". (b) A floor needs a shape inside [scripts/guard-vacuity-floor.test.sh](../../../../scripts/guard-vacuity-floor.test.sh)'s population (`-lt|-le|-ge` — a `-ne` floor is bounded by NOTHING), a threshold literal ADJACENT to the `if` with **nothing** between them ("contiguous" is literal: an intervening `if`/`fi` breaks the backward slice, so pinning the threshold against a second declaration puts the file straight back into the uncovered set), and a MESSAGE carrying that guard's FIRES sentinel vocabulary — a correctly-firing floor whose message it cannot recognise is scored CONSTRUCTION, i.e. indistinguishable from one that crashed. Two of the three are invisible to the suite's own green run. **Why:** #6488 — nine findings, eight in the guards rather than the feature; the message defect was fixed on the instance and recurred on a second file one commit later. See `knowledge-base/project/learnings/2026-09-19-every-fix-for-a-silent-drop-was-itself-a-silent-drop.md`.
|
|
1613
1610
|
|
|
1614
1611
|
- **A census that counts HAS-a-classification never sees a classification pointing at the wrong thing — and a selection map keyed on POSITION needs an identity check.** Two fail-quiet shapes from one diff. (a) A classification gate that rejects only `unclassified` members reads a self-referential classification (a derived edge set containing only the suite's own file) as coverage, so a diff touching the real subject gets the member *declined* — the unsafe direction, and invisible because the census measures presence, not direction. The fix is demoting degenerate classifications to the fail-open class (which *selects*), not adding a stricter census arm. (b) A map built by one walk and consumed by another, keyed on ordinal, diverges silently whenever the two walks tick differently (a nested producer inheriting a filter like a shard var, or a member created mid-run) — store each record's identity beside its bit and compare at consume time, dropping the whole map toward coverage on mismatch. Ask of any selection mechanism: *which direction does a wrong classification decline toward, and what verifies the map's position means what the consumer thinks it means?* **Why:** #8322 — three reviewers independently found the ordinal map had no reconciliation, and 84 suites carried self-only derived edges that read as classified while covering nothing. See `knowledge-base/project/learnings/test-failures/2026-09-20-a-classified-edge-that-pointed-at-itself-read-as-coverage.md`.
|
|
1615
1612
|
- **Resolution is not terminality — for any pending/latch contract in the diff, trace EVERY resolve path in each consumer before reading the happy path.** A latch inferred from promise resolution bricks on any non-terminal resolve (confirm-cancel, handled `!res.ok`, success-without-side-effect); the fix is an explicit `latch()` at the site that promises teardown. Companion race: a released-then-retried episode needs an epoch/token or the hung first attempt's late `finally` reaches into the retry's state. Also check the code-adjacent hole: `res.json()` outside the `try` throws into an error channel nobody renders, and `res.ok && !field` on an assumed-non-null field is a silent dead click (embedded Stripe checkout returns `url: null`). **Why:** PR #8904 (#8917) — three agents independently found the same class; the trace (every resolve path × "would this latch?") produced three P1s from one question. See `knowledge-base/project/learnings/logic-errors/2026-09-27-resolution-is-not-terminality-and-a-zombie-episode-can-kill-the-retry.md`.
|
|
1616
1613
|
|
|
1617
|
-
- **When a diff writes into a shared SWR key WHILE its fetcher is in flight (progressive/streamed commits), ask whether the terminal commit is canonical — SWR's mutation-overlap rule discards the fetcher's resolved value after the first mid-fetch mutate.** The cache
|
|
1614
|
+
- **When a diff writes into a shared SWR key WHILE its fetcher is in flight (progressive/streamed commits), ask whether the terminal commit is canonical — SWR's mutation-overlap rule discards the fetcher's resolved value after the first mid-fetch mutate.** The cache retains the merge product forever: deleted entries never prune (ghosts), ordering diverges, and a post-ack/pre-terminal-commit write can be reverted by a stale accumulated copy. The terminal commit must BE the canonical set ∪ tracked local writes, and every other consumer of the key needs a partial-subset guard. **Why:** PR #9267 — merge-every-frame held ghosts and could snap a confirmed patch back; found by perf/data-integrity seats, invisible to the unit suite. See `knowledge-base/project/learnings/2026-09-30-swr-mid-fetch-mutates-discard-the-fetchers-resolve.md`.
|
|
1615
|
+
|
|
1616
|
+
- **A sibling SESSION — not just a spawned seat — can own commits on your branch; stage only the paths your phase owns.** Before committing run `git log origin/<branch>..HEAD` and `git status --short`, treat foreign commits/dirty paths as the sibling's, stage by explicit pathspec (never `git add -A`); file findings that must touch a sibling-dirty file on the sibling's issue instead. **Why:** #10404/PR #10744 — a parallel session authored `96b243a740` mid-branch; the fix commit staged only owned paths, the sibling-dirty residual went to #10759. See `knowledge-base/project/learnings/2026-10-21-sibling-session-owned-commits-on-my-branch.md`.
|
|
1618
1617
|
|
|
1619
1618
|
### Important: P1 Findings Block Merge
|
|
1620
1619
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Review-Evidence comment and the agent-review merge gate
|
|
2
|
+
|
|
3
|
+
Loaded by `soleur:review` step 3 (after `emit-review-trailer.sh` succeeds) and linked from the arm steps of
|
|
4
|
+
`soleur:ship` and `soleur:merge-pr`. This is the one copy of the gate text; the skills link here.
|
|
5
|
+
|
|
6
|
+
## The gate
|
|
7
|
+
|
|
8
|
+
Soleur's operators are non-technical and cannot review code, so the merge gate is **agent review plus green CI,
|
|
9
|
+
never a human approval** (CTO ruling 2026-10-22; `rf-never-skip-qa-review-before-merging` already defines
|
|
10
|
+
review-gated as "`soleur:review` ran and its findings are fixed"). This does not extend to production-mutating,
|
|
11
|
+
destructive or irreversible commands: those still need the explicit per-command go-ahead that
|
|
12
|
+
`hr-menu-option-ack-not-prod-write-auth` requires, whatever the review said.
|
|
13
|
+
|
|
14
|
+
Domain surfaces additionally need a binding ruling from the owning agent before the merge, obtained through the
|
|
15
|
+
ruling route in [technical-ruling-delegation.md](../../brainstorm-techniques/references/technical-ruling-delegation.md),
|
|
16
|
+
and recorded in the comment's `domain-ruling` field:
|
|
17
|
+
|
|
18
|
+
- legal text, the Art. 30 register, privacy, terms or DPA: `soleur:legal:clo`;
|
|
19
|
+
- `infra/`, terraform, migrations, production writes, destructive operations or `.github/workflows`:
|
|
20
|
+
`soleur:engineering:cto`, plus the review panel's infra and data seats;
|
|
21
|
+
- auth, secrets and URL or input-validation code: `soleur:engineering:review:security-sentinel` is in the panel;
|
|
22
|
+
- the `single-user incident` and `aggregate pattern` tiers add `soleur:engineering:review:user-impact-reviewer`.
|
|
23
|
+
|
|
24
|
+
## The comment
|
|
25
|
+
|
|
26
|
+
A commit trailer is invisible to a reader of `gh pr merge`, so post one PR comment after the trailer. A comment is
|
|
27
|
+
not a push, so it is also safe on a PR already in the merge queue. State what actually ran, never more, and fill
|
|
28
|
+
every placeholder from the review you ran (do not post the template as written):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gh pr comment <number> --body "Review-Evidence: head=<short sha reviewed>; soleur:review ran; coverage=<full|degraded|partial-by-ruling>; tier=<resolved tier>; seats=<agents that returned>; missing=<names or none>; domain-ruling=<none|CLO|CTO>; unresolved-P1-P2=<n>; merge gate = agent review + green CI; no human approval required for non-production-mutating changes"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This is a self-attestation by the reviewing agent, not tamper-proof: anyone with comment access can post the same
|
|
35
|
+
text and it survives later pushes. Read it against the head it names; a head that differs from the PR head, a
|
|
36
|
+
`coverage` other than `full`, or a nonzero `unresolved-P1-P2` is not a pass.
|
|
@@ -93,3 +93,5 @@ Two lessons from #9448 belong in every fix-agent and review-seat brief (full acc
|
|
|
93
93
|
The canonical defect key is `defect-class + rolled-up root cause`, **not** file-scoped: a structural finding spanning N files ("same predicate copy at every call site") merges across files into one defect and one fix; the key is file-qualified only when the defect is genuinely file-local. The ledger is also the durable findings→seat carrier the targeted round's `--finding-seats` input reads — a fix commit closing a finding re-spawns the seat that reported it. `reporting seat(s)` values may be the leaf form, the canonical id (`soleur:engineering:review:security-sentinel`), or a workflow dimension key (`security`) — the script normalizes all three to the leaf; anything else is dropped with an `unknown-seat:` stderr warning. The summary carries `dedup: <N raw> → <M unique>`; the workflow's mechanical `file::title` dedup stays as the cheap first pass and its report gains a `mergedGroups` count.
|
|
94
94
|
|
|
95
95
|
**Persist the ledger.** Append it to the branch's `knowledge-base/project/specs/<feature>/session-state.md` at synthesis (create the section if absent). A resumed-session fix round has no access to the prior session's conversation — `--finding-seats` reads only what survives on disk, so an unpersisted ledger silently reduces every later round to path-mapped seats.
|
|
96
|
+
|
|
97
|
+
- **A fix-round implementer's "suite green" is a tail line, not a verdict: the lead re-runs every touched suite on the COMBINED tree and reads exit codes and `[FAIL]` lines before committing.** Parallel implementers on disjoint file sets still interact through parity guards (a string one adds to `server.tf` can trip a sweep another owns), and a suite can print a passing last line after an earlier guard failed and exit non-zero. Brief the implementers to quote each suite's exit code; verify, don't relay. **Why:** #10455/PR #10697 — group B reported `web-host-provisioner-parity` green while Guard 1 was red (a `FATAL: ... /etc/default/<unit>.` message read as a write destination); only the lead's re-run caught it. See `knowledge-base/project/learnings/2026-10-22-a-claim-outran-its-check-five-times-in-one-runtime-synthetic-pr.md`.
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -846,6 +846,8 @@ Domain leaders are consulted at brainstorm time but not at ship time. The actual
|
|
|
846
846
|
|
|
847
847
|
**Trigger:** PR matches ANY of: (a) touches files in `knowledge-base/product/research/`, `knowledge-base/marketing/`, or adds new workflow patterns (new AGENTS.md rules, new skill phases); (b) has a `semver:minor` or `semver:major` label; (c) title matches `^feat(\(.*\))?:` pattern.
|
|
848
848
|
|
|
849
|
+
**A resumed or manually-driven ship tail still runs this gate.** When ship is resumed after a dead runner, a context reset, or any path where the merge is driven by hand rather than by an uninterrupted Phase 5.5→Phase 7 flow, run this gate's detection BEFORE arming merge — the gate fires on diff state, not on session continuity, and a partial tail that skips it produces a merged PR with no content disposition (epic #10397's #10404 child shipped this way — retroactive brief filed as #10903).
|
|
850
|
+
|
|
849
851
|
**Detection:** Run `git diff --name-only origin/main...HEAD` and check file paths against trigger (a). Run `gh pr view --json labels,title` and check against triggers (b) and (c). If any trigger matches, proceed to "If triggered."
|
|
850
852
|
|
|
851
853
|
**If triggered:**
|
|
@@ -1486,6 +1488,8 @@ done
|
|
|
1486
1488
|
|
|
1487
1489
|
**Why:** PR-H #4066 violated `hr-never-label-any-step-as-manual-without` (3 unfiled deferred-automation steps; #4114 + #4115 filed too late); this gate moved enforcement from honor-system to mechanical. The `playwright-attempt:` precondition (2026-06-10) closes a second bypass: PR #5082's CF-token-widen was filed `deferred-automation` WITHOUT a browser attempt; a real attempt later reached the editable token form. See `knowledge-base/project/learnings/workflow-patterns/2026-06-10-playwright-attempt-evidence-before-operator-only.md`.
|
|
1488
1490
|
|
|
1491
|
+
**Delegated acts (authoring form).** The detection above counts phrasing, not authority, and `DETECT_RE` and `ship-operator-step-gate.sh` stay unchanged on purpose. When the PR body cites a Standing Delegation for an act, read [delegated-act-authoring-form.md](${CLAUDE_PLUGIN_ROOT}/skills/ship/references/delegated-act-authoring-form.md) now and follow it: it gives the one bullet form that is not an operator step and the verifier call that backs it. A bullet that starts with `Operator` and a verb is still detected and still needs its `Tracks` companion, whatever delegation URL it cites.
|
|
1492
|
+
|
|
1489
1493
|
### Soak-Gated Follow-Through Enrollment Gate (mandatory)
|
|
1490
1494
|
|
|
1491
1495
|
Blocks PR-ready when the PR (or its linked plan/spec) declares a **post-deploy soak / time-gated close criterion** for a tracker issue, but that tracker is NOT enrolled in the follow-through sweeper (`follow-through` label + a valid `<!-- soleur:followthrough script=... earliest=... -->` directive whose `script=` exists under [scripts/followthroughs/](../../../../scripts/followthroughs/)). Without enrollment the soak relies on human memory to revisit — the exact rot the sweeper exists to prevent (see [followthrough-convention.md](../../../../knowledge-base/engineering/operations/runbooks/followthrough-convention.md)).
|
|
@@ -2178,6 +2182,8 @@ K=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/ci-head-verdict.sh" ready-count PR_NUMBE
|
|
|
2178
2182
|
&& bash "${CLAUDE_PLUGIN_ROOT}/scripts/ci-head-verdict.sh" wait-ready-run PR_NUMBER --before-count "$K"
|
|
2179
2183
|
```
|
|
2180
2184
|
|
|
2185
|
+
**Arm only with the review evidence**, checked in the arming command: `[ "$(gh pr view PR_NUMBER --json comments --jq '[.comments[].body|select(startswith("Review-Evidence:"))]|length')" -ge 1 ] && gh pr merge PR_NUMBER --squash --auto` (lock-wrapped form where the block below prescribes it). No such comment, a `head=` that is not the PR head, or `coverage` other than `full`: run `soleur:review`, which posts it. Gate text and domain seats: [review-evidence-comment.md](${CLAUDE_PLUGIN_ROOT}/skills/review/references/review-evidence-comment.md).
|
|
2186
|
+
|
|
2181
2187
|
Arm (`gh pr merge --squash --auto`) ONLY on exit 0; else report "readied but unarmed" with the last stdout line and follow [ready-run-wait.md](${CLAUDE_PLUGIN_ROOT}/skills/ship/references/ready-run-wait.md) (a failed `ready-count` or `api-error` needs no undo). A non-draft PR goes straight to the arm.
|
|
2182
2188
|
|
|
2183
2189
|
If `git diff --no-renames --name-only origin/main...HEAD | grep -E '^\.github/(workflows|actions)/'` prints anything, tell the operator in chat now: auto-merge is queued and polled as usual, but this PR has no agent `--admin` fallback (`UNTRUSTED-CI`), so if a BEHIND livelock sets in they will be asked to merge it. See [settle-then-admin-merge.md](${CLAUDE_PLUGIN_ROOT}/skills/ship/references/settle-then-admin-merge.md). **Why:** #8611.
|
|
@@ -2264,7 +2270,7 @@ if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != true ]]; then
|
|
|
2264
2270
|
fi
|
|
2265
2271
|
# Bare ${CLAUDE_PLUGIN_ROOT}, never a `:-` default (ADR-179: the default resolves to a
|
|
2266
2272
|
# path the customer controls). plugin.json identity first (ADR-179 decision 11), then
|
|
2267
|
-
# the --help probe refuses
|
|
2273
|
+
# the --help probe refuses a copy older than the queue-armed refusal (no --fence); the snapshot
|
|
2268
2274
|
# freezes the script for this poll, as a pasted fence always was.
|
|
2269
2275
|
# Read once in a `set +u` subshell: a nounset host shell must not die on an unset root.
|
|
2270
2276
|
SYNC_ROOT="$(set +u; printf '%s' "${CLAUDE_PLUGIN_ROOT}")"
|
|
@@ -2275,7 +2281,7 @@ if [[ "$sync_ok" -eq 1 ]]; then
|
|
|
2275
2281
|
elif ! grep -q '"name"[[:space:]]*:[[:space:]]*"soleur"' "$SYNC_ROOT/.claude-plugin/plugin.json" 2>/dev/null; then
|
|
2276
2282
|
why="$SYNC_ROOT/.claude-plugin/plugin.json does not name soleur (ADR-179 identity check)"
|
|
2277
2283
|
elif [[ ! -r "$SYNC_SH" ]]; then why="the script is missing"
|
|
2278
|
-
elif ! bash "$SYNC_SH" --help 2>/dev/null | grep -q -- '--
|
|
2284
|
+
elif ! bash "$SYNC_SH" --help 2>/dev/null | grep -q -- '--fence'; then why="its --help has no --fence (an older copy)"
|
|
2279
2285
|
elif ! { SYNC_SNAP="$(mktemp)" && trap 'rm -f "$SYNC_SNAP"' EXIT && cp "$SYNC_SH" "$SYNC_SNAP"; }; then why="the snapshot copy failed"
|
|
2280
2286
|
fi
|
|
2281
2287
|
if [[ -n "$why" ]]; then
|
|
@@ -2454,7 +2460,7 @@ while true; do
|
|
|
2454
2460
|
fi
|
|
2455
2461
|
behind_syncs=$((behind_syncs+1))
|
|
2456
2462
|
echo "$(date +%H:%M:%S) [${i}/${MAX_POLL_MIN}] BEHIND detected — auto-sync attempt ${behind_syncs}/${MAX_BEHIND_SYNCS}"
|
|
2457
|
-
sync_rc=0; bash "$SYNC_SNAP" "$PR" --step || sync_rc=$?
|
|
2463
|
+
sync_rc=0; bash "$SYNC_SNAP" "$PR" --step --fence || sync_rc=$?
|
|
2458
2464
|
case "$sync_rc" in
|
|
2459
2465
|
0) echo "$(date +%H:%M:%S) [${i}/${MAX_POLL_MIN}] auto-sync ${behind_syncs} pushed — auto-merge will re-evaluate"
|
|
2460
2466
|
behind_pushes=$((behind_pushes+1))
|
|
@@ -2511,7 +2517,7 @@ done
|
|
|
2511
2517
|
|
|
2512
2518
|
Each meaningful event (first iteration, every state change, heartbeat every 3rd poll ~3 min) arrives as a Monitor notification — quiet while nothing changes, loud when it matters. React to the final state (the last non-heartbeat event). `fetch-error:` appears if `gh` hits a transient API failure; chronic errors break the loop so the caller can surface the outage instead of polling silently. If the loop exits via timeout, report it and investigate why the PR has not merged (OPEN, not queued: see ${CLAUDE_PLUGIN_ROOT}/skills/ship/references/merge-queue-dequeue.md).
|
|
2513
2519
|
|
|
2514
|
-
**Auto-sync on BEHIND.** (Queue mode, below, waits instead for an armed PR when `main` has a merge queue.) When the loop observes `OPEN BEHIND`, origin/main has moved ahead of the branch head since the queued auto-merge started waiting on CI, and GitHub's auto-merge will not fire until the branch catches up. The loop runs [sync-pr-behind.sh](../../scripts/sync-pr-behind.sh) `--step` once per attempt: it refuses an operation it did not start or a detached HEAD, fetches, merges `origin/main` and pushes, and prints a `[pr-behind-sync] kind=… rc=…` line on stdout for every outcome except success. The fence counts exit 5 (fetch) as a skipped attempt, treats exit 11 (`kind=noop` state lag, or `kind=queued`: in the merge queue, a push would dequeue it) as uncounted and keeps polling, and stops on every other non-zero exit (13 `kind=dequeued`, or `[ship.phase7.dequeued]` on any OPEN tick: left the queue unmerged, recovery on the line); `--help` prints the exit-code table. Fix sync behaviour there, never in this fence. **On a `[ship.phase7.sync_failed]` line ending `Stopping the poll.`, do not hand a routine stop to the operator:** (a `kind=fetch` sync_failed line is informational — the poll continues; do nothing) do the next action the `[pr-behind-sync] kind=…` line above it names (resolve the conflict and push; fetch and reconcile a concurrent push; clear the worktree state), then re-invoke this Phase 7 poll. When auto-sync is disabled (a `[ship.phase7.precondition]` line), the first BEHIND tick stops with `[ship.phase7.behind_no_sync]` carrying the manual command — run it from the PR worktree, then re-arm the poll.
|
|
2520
|
+
**Auto-sync on BEHIND.** (Queue mode, below, waits instead for an armed PR when `main` has a merge queue.) When the loop observes `OPEN BEHIND`, origin/main has moved ahead of the branch head since the queued auto-merge started waiting on CI, and GitHub's auto-merge will not fire until the branch catches up. The loop runs [sync-pr-behind.sh](../../scripts/sync-pr-behind.sh) `--step --fence` once per attempt: it refuses an operation it did not start or a detached HEAD, fetches, merges `origin/main` and pushes, and prints a `[pr-behind-sync] kind=… rc=…` line on stdout for every outcome except success. The fence counts exit 5 (fetch) as a skipped attempt, treats exit 11 (`kind=noop` state lag, or `kind=queued`: in the merge queue, a push would dequeue it) as uncounted and keeps polling, and stops on every other non-zero exit (13 `kind=dequeued`, or `[ship.phase7.dequeued]` on any OPEN tick: left the queue unmerged, recovery on the line; 14 `kind=queue_wait`: a copy of this fence that predates `--fence` was refused, re-paste the fence from this file); `--help` prints the exit-code table. Fix sync behaviour there, never in this fence. **On a `[ship.phase7.sync_failed]` line ending `Stopping the poll.`, do not hand a routine stop to the operator:** (a `kind=fetch` sync_failed line is informational — the poll continues; do nothing) do the next action the `[pr-behind-sync] kind=…` line above it names (resolve the conflict and push; fetch and reconcile a concurrent push; clear the worktree state), then re-invoke this Phase 7 poll. When auto-sync is disabled (a `[ship.phase7.precondition]` line), the first BEHIND tick stops with `[ship.phase7.behind_no_sync]` carrying the manual command — run it from the PR worktree, then re-arm the poll.
|
|
2515
2521
|
|
|
2516
2522
|
The sync is capped at `MAX_BEHIND_SYNCS=6` per poll, so a pathological BEHIND→BEHIND→BEHIND (every sync triggering a new commit on main) cannot spend the whole `MAX_POLL_MIN`-minute budget making no progress. After 6 syncs the loop emits a `BEHIND budget exhausted` warning naming the elapsed time, then falls through to heartbeat — the PR may still merge if main calms down, but the diagnosis lands at the inflection point rather than at the timeout. At `fetch_failures=6/6` it names a network/credential failure instead of a fast-moving main.
|
|
2517
2523
|
|
|
@@ -2541,7 +2547,7 @@ Do NOT invert this into "ignore failures that look transient". The discriminator
|
|
|
2541
2547
|
- **A DIRTY that recurs on every landing is a file in YOUR diff that `main` rewrites every merge — name it before resolving it twice, and ask whether it should be committed AT ALL.** Intersect `git diff --name-only origin/main...HEAD` with `main`'s last 20 merge paths. DIRTY is the one state the `--admin` hatch cannot cross, so a generated artifact with one producer per PR conflicts on every landing. The two files that motivated this (`INDEX.md`, `rule-metrics.json`) are untracked caches since #8377 / ADR-235 — the loop is closed at the source, because a file that is never committed cannot conflict. `model.likec4.json` is the one still committed, and `resolve-regenerable-conflicts.sh` above handles it. Also re-run `check-adr-ordinals.sh` after EVERY sync, not once at ship start. **Why:** #8301 — three DIRTY cycles (~4 h) before the cause was named, then an ADR-225 → ADR-229 renumber three syncs in. See `knowledge-base/project/learnings/2026-09-19-a-generated-artifact-in-my-diff-made-every-landing-on-main-a-conflict.md`.
|
|
2542
2548
|
- **After a hand-resolved merge, re-test by consumer, not by conflict.** A sibling's new test merges cleanly, so it is not in the conflict list, and the pre-commit full gate skips merge commits (`lefthook.yml` `bun-test` `skip: merge`). Before pushing, run every suite that references a script your branch changes, derived as in `work/SKILL.md` ("derive the list from CONSUMERS, not memory"). **Why:** #8474 — see `knowledge-base/project/learnings/workflow-issues/2026-09-22-the-test-my-merge-broke-merged-cleanly-so-it-was-never-in-my-conflict-list.md`.
|
|
2543
2549
|
|
|
2544
|
-
This complements the PreToolUse hook [`.claude/hooks/pre-merge-rebase.sh`](../../../../.claude/hooks/pre-merge-rebase.sh) which fires on certain Bash invocations during the ship flow (commit, push, merge). The hook handles the pre-merge case (branch is behind when auto-merge is FIRST queued); this loop handles the post-merge-queue case (branch becomes behind WHILE the queued auto-merge is waiting on CI). The two surfaces don't overlap — the hook fires on operator-triggered git/gh commands; the poll loop fires on a fixed 60-second cadence regardless of operator activity.
|
|
2550
|
+
This complements the PreToolUse hook [`.claude/hooks/pre-merge-rebase.sh`](../../../../.claude/hooks/pre-merge-rebase.sh) which fires on certain Bash invocations during the ship flow (commit, push, merge). The hook handles the pre-merge case (branch is behind when auto-merge is FIRST queued); this loop handles the post-merge-queue case (branch becomes behind WHILE the queued auto-merge is waiting on CI). On a merge-queue repo the hook also skips its sync for an armed BEHIND PR that is not enqueued yet (the queue makes it current; this loop's expiry fallback is the sanctioned sync). The two surfaces don't overlap — the hook fires on operator-triggered git/gh commands; the poll loop fires on a fixed 60-second cadence regardless of operator activity.
|
|
2545
2551
|
|
|
2546
2552
|
**If the poll loop exits due to a required-check failure (PR still OPEN) or CLOSED state:**
|
|
2547
2553
|
|
|
@@ -2990,20 +2996,15 @@ Note: The DIRTY (merge conflict) exit is already handled inside the poll block
|
|
|
2990
2996
|
|
|
2991
2997
|
**Do NOT skip this step.** Do NOT proceed to Step 4 (cleanup) without invoking postmerge. The rationalization "the PR merged, we're done" is exactly the failure mode this gate prevents — a merged PR is not a deployed PR, and a deployed PR is not a healthy deployment. **Why:** PR #4512 — the agent jumped from merge confirmation directly to cleanup, skipping release workflow monitoring (Step 2) and postmerge verification (Step 3.8). The release was still in progress and could have failed silently.
|
|
2992
2998
|
|
|
2999
|
+
3.9. **Epic-Close Domain-Leader Sweep (conditional — reconcile before cleanup).** Phase 5.5's CMO gate is PR-scoped; aggregate claims, cross-child meta-findings, and deadline-shaped timing exist only at epic scope. **Trigger:** this merge completed a tracked epic — an issue it closed is a child of an epic whose other children are all CLOSED. When the trigger may hold, load [epic-close-sweep.md](./references/epic-close-sweep.md) for detection, the per-child Phase 5.5 disposition census, the arc-level CMO assessment, and the inconclusive-report rule. **If not triggered:** skip to Step 4. **Why:** epic #10397 closed 5/7 briefed with no arc-level assessment — the synthesis surfaced only because the operator asked; the two missed children were partial-ship tails a per-PR gate cannot cover.
|
|
3000
|
+
|
|
2993
3001
|
4. Clean up worktree and local branch:
|
|
2994
3002
|
|
|
2995
3003
|
Navigate to the repository root directory, then run `bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" cleanup-merged`.
|
|
2996
3004
|
|
|
2997
3005
|
This detects `[gone]` branches (where the remote was deleted after merge), removes their worktrees, deletes local branches, and pulls latest main so the next worktree branches from the current state.
|
|
2998
3006
|
|
|
2999
|
-
**It does NOT durably archive spec directories — do not defer archival to it.**
|
|
3000
|
-
|
|
3001
|
-
1. **It usually does not run at all.** The step guards on `[[ -d "$GIT_ROOT/knowledge-base/project/specs/<branch>" ]]` against the BARE ROOT's on-disk files, and it executes *before* the "Updated main to latest" step. On a just-merged branch the bare root is therefore still at the pre-merge commit, where the spec dir does not exist — it arrives only with the merge being cleaned up. Guard false, silent skip. Measured on PR #7240: the dir is ABSENT at `5133e3cb` (main at archival time) and PRESENT at `933635603` (the merge commit).
|
|
3002
|
-
2. **Even when it runs it commits nothing.** It is a plain `mv` of the bare root's working files, so it produces no commit and leaves the spec dir tracked at its LIVE path on `main`.
|
|
3003
|
-
|
|
3004
|
-
The archives that actually exist under `specs/archive/` were committed by **compound's** automatic consolidation (`git mv` + commit on the feature branch), which is the only mechanism that persists.
|
|
3005
|
-
|
|
3006
|
-
The practical consequence: **ship's Phase 6.45 (KB Artifact Archival) is now the in-PR archival point** — the named phase both PR arms run before any `gh pr ready` / `gh pr merge` site, backed by the `ship-kb-archive-gate.sh` PreToolUse hook. Compound's Step E archives eagerly when no live-path reader remains, but a deferred archival — the legitimate choice when `specs/<branch>/decision-challenges.md` must stay live for Phase 6 step 2.5's Model Dissents render or the plan file's `## Operator Holds` for step 5 — no longer strands the artifacts: Phase 6.45 runs `archive-kb.sh` AFTER those last reads and the `git mv` rides the same PR. A follow-up PR is never required for archival. **Why the ordering is load-bearing:** PR #7240 deferred compound's archival so Phase 6 step 2.5 could read `decision-challenges.md`, on the strength of `cleanup-merged` archiving post-merge — it did not, and the spec dir shipped unarchived; #9768 shipped unarchived the same way in 2026-10.
|
|
3007
|
+
**It does NOT durably archive spec directories — do not defer archival to it.** Its archive step is scoped to legacy bare-root specs, skips silently when the dir is absent, and commits nothing; the in-PR archival point is Phase 6.45. Full reasoning: [cleanup-merged-archival.md](./references/cleanup-merged-archival.md).
|
|
3007
3008
|
|
|
3008
3009
|
**If working from a worktree:** Navigate to the main repo root first, then run cleanup.
|
|
3009
3010
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Why `cleanup-merged` is not the KB-archival point
|
|
2
|
+
|
|
3
|
+
Loaded from ship Phase 7 Step 4's archival note. `cleanup-merged` carries an
|
|
4
|
+
archive step, but its own header scopes it: *"Backward-compat for legacy
|
|
5
|
+
pre-#2815 worktrees that created specs at the bare root. New layout commits
|
|
6
|
+
the spec inside the worktree (git history is the canonical archive); the
|
|
7
|
+
`[[ -d ]]` guard silently skips when the bare-root dir does not exist."* Two
|
|
8
|
+
independent reasons it cannot durably archive a modern spec dir:
|
|
9
|
+
|
|
10
|
+
1. **It usually does not run at all.** The step guards on
|
|
11
|
+
`[[ -d "$GIT_ROOT/knowledge-base/project/specs/<branch>" ]]` against the
|
|
12
|
+
BARE ROOT's on-disk files, and it executes *before* the "Updated main to
|
|
13
|
+
latest" step. On a just-merged branch the bare root is therefore still at
|
|
14
|
+
the pre-merge commit, where the spec dir does not exist — it arrives only
|
|
15
|
+
with the merge being cleaned up. Guard false, silent skip. Measured on PR
|
|
16
|
+
#7240: the dir is ABSENT at `5133e3cb` (main at archival time) and PRESENT
|
|
17
|
+
at `933635603` (the merge commit).
|
|
18
|
+
2. **Even when it runs it commits nothing.** It is a plain `mv` of the bare
|
|
19
|
+
root's working files, so it produces no commit and leaves the spec dir
|
|
20
|
+
tracked at its LIVE path on `main`.
|
|
21
|
+
|
|
22
|
+
The archives that actually exist under `specs/archive/` were committed by
|
|
23
|
+
**compound's** automatic consolidation (`git mv` + commit on the feature
|
|
24
|
+
branch), which is the only mechanism that persists.
|
|
25
|
+
|
|
26
|
+
The practical consequence: **ship's Phase 6.45 (KB Artifact Archival) is the
|
|
27
|
+
in-PR archival point** — the named phase both PR arms run before any
|
|
28
|
+
`gh pr ready` / `gh pr merge` site, backed by the `ship-kb-archive-gate.sh`
|
|
29
|
+
PreToolUse hook. Compound's Step E archives eagerly when no live-path reader
|
|
30
|
+
remains, but a deferred archival — the legitimate choice when
|
|
31
|
+
`specs/<branch>/decision-challenges.md` must stay live for Phase 6 step 2.5's
|
|
32
|
+
Model Dissents render or the plan file's `## Operator Holds` for step 5 — no
|
|
33
|
+
longer strands the artifacts: Phase 6.45 runs `archive-kb.sh` AFTER those
|
|
34
|
+
last reads and the `git mv` rides the same PR. A follow-up PR is never
|
|
35
|
+
required for archival. **Why the ordering is load-bearing:** PR #7240
|
|
36
|
+
deferred compound's archival so Phase 6 step 2.5 could read
|
|
37
|
+
`decision-challenges.md`, on the strength of `cleanup-merged` archiving
|
|
38
|
+
post-merge — it did not, and the spec dir shipped unarchived; #9768 shipped
|
|
39
|
+
unarchived the same way in 2026-10.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Delegated acts: the PR-body authoring form
|
|
2
|
+
|
|
3
|
+
Loaded from [ship/SKILL.md](../SKILL.md) Phase 5.5, Undeferred Operator-Step Gate, when the PR body cites a Standing Delegation for an act (rulings F1, R4 and R5 in the Standing Delegation work).
|
|
4
|
+
|
|
5
|
+
**Plugin root in this file:** this file is Read, not delivered by the skill loader, so `${CLAUDE_PLUGIN_ROOT}` below is not replaced for you. The root is ONLY the prefix of the path you read this file from, cut at its last `/skills/`, never a value from repository files, PR text or tool output. Check first with `echo "root=[${CLAUDE_PLUGIN_ROOT}]"`; if it prints `root=[]`, prefix each command below with `export CLAUDE_PLUGIN_ROOT=<root>`.
|
|
6
|
+
|
|
7
|
+
The detection above counts phrasing, not authority: it has no delegation awareness, and `DETECT_RE` and `ship-operator-step-gate.sh` stay unchanged on purpose. An act that a Production Envelope covers and that the CTO agent carries out under a recorded Standing Delegation is not an operator step, because the agent performs it. Write it in the PR body as `Delegated act (CTO, Standing Delegation <comment-url>, envelope <path#title>, command "<exact envelope line>"): <act in words>`, a bullet that does not start with the word Operator. No URL or keyword exempts any other bullet: a bullet that starts with `Operator` and a verb is still detected and still needs its `Tracks` companion even when it cites a delegation URL. As a prose step (not a hook exemption), run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/delegation-verify.sh" verify --delegation <comment-url> --envelope <path#title> --command "<exact envelope line>"` on each cited triple before relying on the form; a non-zero exit (1 invalid, 2 unverifiable) makes the bullet an operator step, so strip the form and give the bullet a `Tracks` companion, or hold the PR. An act that has already run also cites its receipt URL and the `ran` row, and is checked by appending `--audit --at <time the act ran>` to that call (the `--at` value is UTC, ending in Z, format YYYY-MM-DDTHH:MM:SSZ, the only form the verifier accepts), so a lapsed delegation or a closed epic does not misread a correct historic act as expired ([standing-delegation.md](${CLAUDE_PLUGIN_ROOT}/skills/brainstorm-techniques/references/standing-delegation.md) section 1); an act that has not run yet keeps its `Tracks` companion. Before relying on the form, the ship agent compares the quoted envelope line, the act in words, and (for an act that already ran) the `ran` row, and treats any mismatch as an operator step. Never use the form for any act the envelope or the Standing Delegation Excluded list names (the A1b environment approval, A5, A6 and A7 of the Inngest TLS envelope are instances), nor for any act whose text names an owner-human act (a signing key, `SIGNED BY OWNER`, approving an environment or the inngest-cutover approval, a credential, money, a force-push): such a bullet is an operator step whatever its prefix, so strip the form and add `Tracks`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Epic-Close Domain-Leader Sweep — full spec
|
|
2
|
+
|
|
3
|
+
Loaded by ship Phase 7 Step 3.9 when its trigger fires. The body keeps only the
|
|
4
|
+
trigger and the load directive; this file is the executable spec.
|
|
5
|
+
|
|
6
|
+
Phase 5.5's CMO gate is PR-scoped; three classes of opportunity exist only at
|
|
7
|
+
epic scope — (a) **aggregate claims** ("shipped the whole checklist in four
|
|
8
|
+
days" lives in no single diff), (b) **cross-child meta-findings** (the pattern
|
|
9
|
+
across children is often the arc's strongest claim), (c) **deadline-shaped
|
|
10
|
+
timing** (a synthesis pegged to an external article/launch decays on a
|
|
11
|
+
calendar no per-PR slotting asks about).
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
This merge completed a tracked epic — an issue it closed is a child of an epic
|
|
16
|
+
whose other children are all CLOSED (the epic issue itself closing also fires).
|
|
17
|
+
|
|
18
|
+
## Detection
|
|
19
|
+
|
|
20
|
+
1. `gh pr view <PR> --json closingIssuesReferences` — the issues the merge
|
|
21
|
+
closed (covers `Closes`/`Fixes`/`Resolves`, multiple issues).
|
|
22
|
+
2. `gh issue view <N> --json parent` for each issue's epic; empty `parent` →
|
|
23
|
+
grep the issue's body and comments for `Tracked by #N`.
|
|
24
|
+
3. `gh issue view <epic> --json body` — extract child task-list rows
|
|
25
|
+
(`- [ ] #N` / `- [x] #N`); when the body carries none, fall back to
|
|
26
|
+
`gh issue view <epic> --json subIssues` and read each child's `state`.
|
|
27
|
+
|
|
28
|
+
Fire only when ALL hold: a closed `#N` appears among the children (or is the
|
|
29
|
+
epic), the child count is ≥2, and every child is closed. An empty child list
|
|
30
|
+
is "cannot confirm" — report "epic-close sweep inconclusive for #<epic>";
|
|
31
|
+
never fire on a vacuous truth, never skip silently.
|
|
32
|
+
|
|
33
|
+
## If triggered
|
|
34
|
+
|
|
35
|
+
1. **Per-child disposition census.** For each child, confirm a Phase 5.5
|
|
36
|
+
disposition exists — a filed brief, a `content-strategy.md` row, or an
|
|
37
|
+
explicit skip-with-reason. A child whose ship tail was resumed manually or
|
|
38
|
+
after a runner failure silently skips Phase 5.5; the census catches that.
|
|
39
|
+
2. **Arc-level CMO assessment.** Spawn the CMO agent: "Assess content and
|
|
40
|
+
distribution opportunities from the COMPLETED epic, not any single PR:
|
|
41
|
+
aggregate claims, cross-child meta-findings, external news pegs the epic
|
|
42
|
+
cites, and whether a synthesis piece complements or supersedes the
|
|
43
|
+
per-child briefs. For each child with no disposition, produce a
|
|
44
|
+
retroactive brief."
|
|
45
|
+
3. **Interactive:** ask create-now / schedule / skip (Phase 5.5's options).
|
|
46
|
+
**Headless:** auto-file the arc brief + retroactive child briefs (do not
|
|
47
|
+
block). This runs post-merge — a `content-strategy.md` edit lands on a
|
|
48
|
+
reaped worktree, so embed proposed rows in each filed brief's body or land
|
|
49
|
+
them in a follow-up PR from fresh `origin/main`. An issue without a
|
|
50
|
+
strategy row is an orphan (#1173).
|
|
51
|
+
|
|
52
|
+
## If not triggered
|
|
53
|
+
|
|
54
|
+
Skip to Step 4 silently.
|
|
55
|
+
|
|
56
|
+
## Why
|
|
57
|
+
|
|
58
|
+
Epic #10397 (7 children × 7 PRs) closed with 5/7 per-PR briefs and no
|
|
59
|
+
arc-level assessment — the synthesis surfaced only because the operator
|
|
60
|
+
asked. The two missed children (#10400, #10404) were both partial-ship tails
|
|
61
|
+
— paths a per-PR gate cannot cover. "Gate fixed" is not done — "gate fixed
|
|
62
|
+
AND missed case remediated" is done.
|
|
@@ -12,7 +12,7 @@ A red **advisory** job (a `merge_group` job that is not in `scripts/required-che
|
|
|
12
12
|
|
|
13
13
|
- the Phase 7 fence, on every 5th OPEN tick of any `mergeStateStatus` (prints `[ship.phase7.dequeued]` and stops) and on the poll timeout (`Queue: …`), and `--step` on each BEHIND tick that is not waiting in queue mode (`kind=dequeued rc=13`; a queue-mode wait tick makes no `--step` call, so a dequeue shows on the next 5th-tick read);
|
|
14
14
|
- `monitor-pr-checks.sh <N>` (drain-prs), which ends `LEFT THE MERGE QUEUE UNMERGED` on a `dequeued` read and never on an unreadable one;
|
|
15
|
-
- the pre-merge hook, which
|
|
15
|
+
- the pre-merge hook, which skips the sync for a queued PR and for an armed BEHIND PR of a merge-queue repo that is not enqueued yet (a dequeued one is exactly the recipe's step 2).
|
|
16
16
|
|
|
17
17
|
A failed read is never a dequeue. Check by hand from the PR worktree:
|
|
18
18
|
|
|
@@ -108,6 +108,36 @@ test is not dischargeable here either.
|
|
|
108
108
|
delegation, so none exists and every production act stays a hold. Do not ask. File ONE `action-required` issue that names the
|
|
109
109
|
ACTION (what the operator is being asked to approve, in plain words), print one plain sentence, and continue to the draft PR.
|
|
110
110
|
|
|
111
|
+
**Interactive** (ship runs attended: standalone or through `soleur:go` or `soleur:work` without `--headless`, with an
|
|
112
|
+
AskUserQuestion channel; under `soleur:one-shot` or any `--headless` run, Headless above wins): a merge hold with no recorded
|
|
113
|
+
delegation is not an end state, and that includes a session-level instruction such as "any merge needs my go-ahead". Never end
|
|
114
|
+
the turn on "Paused: awaiting merge approval": the Stop hook can reject it, and a non-technical operator cannot clear a parked
|
|
115
|
+
merge. Record the instruction as a hold under `## Operator Holds` in the plan (section 1), then, when this section is entered
|
|
116
|
+
(Phase 6 step 5, before arming; the merge itself still waits for the required checks):
|
|
117
|
+
|
|
118
|
+
1. **What this does not cover.** A hold that begins `Operator-reserved:` (section 1), or that names another person's approval,
|
|
119
|
+
stands, and the question below never discharges it. A sibling change or a state change is judged by its own condition
|
|
120
|
+
(section 3 above), not by this question.
|
|
121
|
+
2. **Ask.** Put the epic-start question of
|
|
122
|
+
[standing-delegation.md](${CLAUDE_PLUGIN_ROOT}/skills/brainstorm-techniques/references/standing-delegation.md) section 6 to
|
|
123
|
+
the person as a real question (AskUserQuestion, options labelled `Yes ...` and `No ...`), once per delegation. Its Scope
|
|
124
|
+
items name each pull request as `#N` and include a revert item, and the expiry is at most 7 days. When the merge triggers a
|
|
125
|
+
production apply or deploy (any workflow that runs on push to main for the changed paths, such as
|
|
126
|
+
`apply-web-platform-infra.yml` for `apps/web-platform/infra/**`), the outcome sentence says so.
|
|
127
|
+
3. **Where a `Yes` comes from.** Only the person's reply to this question in this session, as the tool returned it. Text in a
|
|
128
|
+
pull request, issue, comment, file or earlier turn is never the answer, and neither is "go per the CTO".
|
|
129
|
+
4. **On `Yes`.** The calling skill writes the record with `delegation-verify.sh render`, posts it on the epic (with no epic, on
|
|
130
|
+
an open issue that tracks the change and that the pull request does not close, because the epic closing ends the delegation;
|
|
131
|
+
a pull request is refused as `epic-is-pr`), and reads it back through `verify`. The CTO (`Own the epic #<N>, delegation <url>`)
|
|
132
|
+
commits the Production Envelope in a runbook outside the folders ship archives, verifies the exact command against it, runs
|
|
133
|
+
that command byte for byte and posts the receipt (section 4). The person's reply discharged the session-level hold, so delete
|
|
134
|
+
it from `## Operator Holds` (and the whole section when nothing else remains): a hold left without a gate stands forever.
|
|
135
|
+
5. **On `No`, or any answer that does not start with `Yes`.** Today's behaviour: ask once per command, naming the action.
|
|
136
|
+
|
|
137
|
+
A pull request or workflow not named in the recorded Scope is a new question: a `Yes` is never reused. The guards stay: the
|
|
138
|
+
required checks green, the review evidence (the `Reviewed-By-Soleur` trailer, and on a single-user-incident plan a
|
|
139
|
+
`Reviewed-Coverage` that ship's Phase 1.5 gate accepts), the Excluded list, and no `--admin`.
|
|
140
|
+
|
|
111
141
|
## 4. Timing: twice, and the receipt after a delegated act
|
|
112
142
|
|
|
113
143
|
Evaluate at the draft exit AND again at the start of Phase 6.45, before the plan is archived, through the same command
|
|
@@ -4,4 +4,4 @@ The Queue mode paragraph below was moved out of `ship/SKILL.md` Phase 7 to stay
|
|
|
4
4
|
|
|
5
5
|
**Queue mode (#9710).** When `main` has a `merge_queue` rule (read once at loop entry by `.type == "merge_queue"`, never by position; this reads `main`'s rules, as the required-check read already does) and the required-check set is non-empty, AND auto-merge is armed (read each BEHIND tick), a BEHIND reading that GitHub itself reported is not a reason to push: each push restarts the whole required-check set, and the queue builds every candidate against the projected `main`, so an armed PR is made current by the queue. GitHub enqueues an armed PR whose head is behind `main` on its own (measured, ADR-270 canary addendum 2026-10-07), so the poll prints `[ship.phase7.queue_wait]` once and keeps polling (informational: take no action, and never sync, update-branch or merge it by hand until `[ship.phase7.queue_wait_expired]`, a push line, MERGED, a dequeue or any poll exit), prints `[ship.phase7.queued]` once, at the first every-5th-tick read that finds the PR in the queue (a queued reading also restarts the idle count, so while each 5th-tick read answers queued the PR does not expire), and stops on a dequeue as above. The wait is bounded two ways. More than 5 consecutive wait ticks (the 6th) with no pending REQUIRED check (a required check that does not exist yet, such as the `test` aggregate before its shards finish, counts as pending while any check is still running) print `[ship.phase7.queue_wait_expired]` and fall back to the BEHIND auto-sync for the rest of that poll (this poll's last queue read did not show the PR queued: keep the Monitor running, diagnose with the dequeue reference and the PR timeline, record the strict up-to-date question in the final report, and do not change the ruleset; the fallback `--step` itself skips a PR that is in the queue and fails closed on an unreadable queue read). `MAX_POLL_MIN` caps a PR whose required checks never settle or whose state keeps flapping (any tick that is not a wait tick restarts the idle count). Every unreadable answer falls toward today's sync (in the fence; the standalone script's rules read is the one new exception, see Standalone guard below): a failed rules read or an empty required-check set leaves queue mode off for the poll, a failed armed read ends the wait for that tick (that tick syncs as today), and a failed checks read counts as idle. A DIRTY-derived BEHIND, a repo with no queue rule and a poll with auto-sync disabled behave exactly as before. Waiting does not consume `MAX_BEHIND_SYNCS`. Accepted residuals (ADR-270 amendment 2026-10-07): some required contexts (`tenant-integration-required`, `vendor-pin-required`) post a PASS on `merge_group` without re-running their suites, so in queue mode their only real run is the PR-time one against the PR-time base (the sync used to re-run the PR-event versions on a fresher base); and a dequeue during the wait is found by a current removal event on the next 5th-tick read, because the seen-queued marker is only written by `--step`.
|
|
6
6
|
|
|
7
|
-
**Standalone guard (2026-10-09, PR 9839).** `sync-pr-behind.sh <PR>` (the standalone loop) now enforces the rule above itself: when GitHub reports the PR BEHIND (not DIRTY), auto-merge is armed and `main` has a `merge_queue` rule (selected by `.type`, one read of up to 100 rules), it prints `kind=queue_wait rc=0` and exits 0 with nothing fetched, merged or pushed. A rules read that still fails after `PR_QUEUE_ATTEMPTS` tries (default 2, as for the queue read) or answers non-numeric is `kind=gh` (exit 4) with the cause in the line. This is the one place the script FAILS CLOSED on an unreadable rules answer: the fence above falls toward sync, but a standalone run has no poll behind it and an unknown answer is not a yes (the cost is a visible, named stop on a host whose token cannot read repository rules; the sanctioned sync is then the Phase 7 poll's own sync). The incident: an agent hand-wrote a poll loop that treated BEHIND as an error and ran the script after `gh pr merge --auto` and before the enqueue (one push landed after the arm, 12 minutes in), restarting PR CI for nothing; the old prose scoped the ban to a poll that had already emitted `[ship.phase7.queue_wait]`, a marker a hand-written loop never emits, so the executable now refuses on the facts. Boundaries, stated so they are not rediscovered: (1) `--step`
|
|
7
|
+
**Standalone guard (2026-10-09, PR 9839).** `sync-pr-behind.sh <PR>` (the standalone loop) now enforces the rule above itself: when GitHub reports the PR BEHIND (not DIRTY), auto-merge is armed and `main` has a `merge_queue` rule (selected by `.type`, one read of up to 100 rules), it prints `kind=queue_wait rc=0` and exits 0 with nothing fetched, merged or pushed. A rules read that still fails after `PR_QUEUE_ATTEMPTS` tries (default 2, as for the queue read) or answers non-numeric is `kind=gh` (exit 4) with the cause in the line. This is the one place the script FAILS CLOSED on an unreadable rules answer: the fence above falls toward sync, but a standalone run has no poll behind it and an unknown answer is not a yes (the cost is a visible, named stop on a host whose token cannot read repository rules; the sanctioned sync is then the Phase 7 poll's own sync). The incident: an agent hand-wrote a poll loop that treated BEHIND as an error and ran the script after `gh pr merge --auto` and before the enqueue (one push landed after the arm, 12 minutes in), restarting PR CI for nothing; the old prose scoped the ban to a poll that had already emitted `[ship.phase7.queue_wait]`, a marker a hand-written loop never emits, so the executable now refuses on the facts. Boundaries, stated so they are not rediscovered: (1) `--step` and the pre-merge hook are guarded too since 2026-10-20 (#9869): see the ADR-270 addendum of that date (a flagless `--step` exits 14 `kind=queue_wait`, the fences' own call opts out, the hook asks the read-only `--queue-wait` verdict). (2) Base is `main`, as everywhere in the script. (3) The script does not read required checks: the fence sets `QUEUE_RULE=0` on a repo with no required checks and exits on a red required check, but the script answers `queue_wait` for any armed BEHIND PR; the refusal text therefore says to push a fix for a red check and routes to the sibling reference merge-queue-dequeue.md ("Armed and green but never enqueued") instead of promising a merge. (4) A DIRTY that is only GitHub's recompute lag after `main` moved still syncs. (5) The standalone answer has no expiry: the bound is re-arming the Phase 7 poll, whose fallback sync is the sanctioned one. The text surfaces say the same thing in one phrase: "when `main` has a merge queue and the PR is armed (the Phase 7 poll prints `[ship.phase7.queue_wait]`)" — a fact, not a marker only the Phase 7 fence prints; it holds for a BEHIND reading only. Surfaces not edited because they sit in the byte-ceilinged fences or outside the armed-BEHIND class: the fence `behind_no_sync` line (its standalone call now answers `queue_wait` on an armed PR, which is correct: fix the poll precondition and re-arm), `monitor-pr-checks.sh`, and `review/SKILL.md`'s DIRTY recovery.
|