@soleur/opencode 1.0.0 → 3.354.2

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.
Files changed (92) hide show
  1. package/commands/go.md +6 -5
  2. package/commands/sync.md +2 -2
  3. package/install.js +6 -85
  4. package/lib/harness.ts +1 -1
  5. package/lib/heartbeat-manifest.ts +19 -0
  6. package/lib/pr-merge-poll.ts +2 -2
  7. package/lib/workflow-fidelity.ts +2 -2
  8. package/opencode/INSTRUCTIONS.md +19 -0
  9. package/opencode/plugin.ts +343 -18
  10. package/package.json +1 -1
  11. package/scripts/admin-merge-ready.sh +1 -1
  12. package/scripts/admin-merge-ready.test.sh +0 -0
  13. package/scripts/affected-tests.sh +0 -0
  14. package/scripts/alpha-metrics.sh +0 -0
  15. package/scripts/alpha-metrics.test.sh +0 -0
  16. package/scripts/check-red-on-main.sh +0 -0
  17. package/scripts/ci-head-verdict.sh +54 -6
  18. package/scripts/cloud-detect.sh +0 -0
  19. package/scripts/delegation-verify.sh +0 -0
  20. package/scripts/delegation-verify.test.sh +0 -0
  21. package/scripts/deploy-arm.sh +0 -0
  22. package/scripts/domain-model-drift.sh +0 -0
  23. package/scripts/emit-decision.sh +0 -0
  24. package/scripts/emit-decision.test.sh +0 -0
  25. package/scripts/ext-filter-divergence.sh +0 -0
  26. package/scripts/grok-fidelity-gate.sh +0 -0
  27. package/scripts/grok-pre-push-gate.sh +0 -0
  28. package/scripts/install-opencode-adapter.ts +118 -129
  29. package/scripts/lib/affected-tests/common.sh +0 -0
  30. package/scripts/lib/affected-tests/go.sh +0 -0
  31. package/scripts/lib/affected-tests/js.sh +0 -0
  32. package/scripts/lib/affected-tests/nx-turbo.sh +0 -0
  33. package/scripts/lib/affected-tests/python.sh +0 -0
  34. package/scripts/lib/session-state.sh +0 -0
  35. package/scripts/lib/tmp-classify.sh +0 -0
  36. package/scripts/monitor-pr-checks.sh +1 -1
  37. package/scripts/pipeline-tally.sh +0 -0
  38. package/scripts/plugin-version.sh +0 -0
  39. package/scripts/precommit-guard.sh +3 -1
  40. package/scripts/precommit-guard.test.sh +0 -0
  41. package/scripts/render-c4-model.sh +0 -0
  42. package/scripts/resolve-regenerable-conflicts.sh +0 -0
  43. package/scripts/run-in-pid-namespace.sh +0 -0
  44. package/scripts/setup-ralph-loop.sh +0 -0
  45. package/scripts/sync-npm-package.ts +74 -87
  46. package/scripts/sync-opencode-adapter.ts +19 -0
  47. package/scripts/sync-pr-behind.sh +183 -70
  48. package/scripts/taste-profile-update.sh +0 -0
  49. package/scripts/taste-profile-update.test.sh +0 -0
  50. package/scripts/write-budget-marker.sh +0 -0
  51. package/skills/deepen-plan/SKILL.md +1 -1
  52. package/skills/deploy/references/hetzner-setup.md +5 -3
  53. package/skills/drain-prs/SKILL.md +3 -2
  54. package/skills/fix-issue/SKILL.md +8 -8
  55. package/skills/fix-issue/references/agent-authored-exclusion.md +4 -2
  56. package/skills/fix-issue/references/exclude-label-jq-snippet.md +4 -3
  57. package/skills/merge-pr/SKILL.md +5 -3
  58. package/skills/model-launch-review/SKILL.md +4 -4
  59. package/skills/one-shot/SKILL.md +1 -1
  60. package/skills/plan/SKILL.md +1 -1
  61. package/skills/plan/references/plan-sharp-edges.md +7 -1
  62. package/skills/postmerge/SKILL.md +2 -2
  63. package/skills/postmerge/references/deploy-status-debugging.md +3 -0
  64. package/skills/review/SKILL.md +14 -15
  65. package/skills/review/references/review-evidence-comment.md +36 -0
  66. package/skills/review/references/risk-tier-and-fix-rounds.md +2 -0
  67. package/skills/ship/SKILL.md +14 -13
  68. package/skills/ship/references/cleanup-merged-archival.md +39 -0
  69. package/skills/ship/references/delegated-act-authoring-form.md +7 -0
  70. package/skills/ship/references/epic-close-sweep.md +62 -0
  71. package/skills/ship/references/merge-queue-dequeue.md +1 -1
  72. package/skills/ship/references/operator-holds.md +30 -0
  73. package/skills/ship/references/queue-mode.md +1 -1
  74. package/skills/ship/references/ready-run-wait.md +3 -3
  75. package/skills/test-fix-loop/SKILL.md +1 -1
  76. package/skills/verification-scaffold/SKILL.md +73 -0
  77. package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
  78. package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
  79. package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
  80. package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
  81. package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
  82. package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
  83. package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
  84. package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
  85. package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
  86. package/skills/verification-scaffold/references/guards.tsv +21 -0
  87. package/skills/verification-scaffold/references/verification-readme.template +55 -0
  88. package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
  89. package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
  90. package/skills/verification-scaffold/test/generator.test.sh +319 -0
  91. package/skills/verification-scaffold/test/manifest.test.sh +144 -0
  92. package/skills/work/SKILL.md +2 -1
package/commands/go.md CHANGED
@@ -537,13 +537,14 @@ If the user wants to continue the current feature, delegate to `soleur:work`. **
537
537
 
538
538
  ## Step 1.5: Hold routing (pre-route)
539
539
 
540
- This paragraph runs before the routing table and sits outside the eval-gated block, so it never changes a routing row, and it is the one authority for the trigger (nothing else in the repository defines when holds are routed). It routes only when a resume or handoff brief carries BOTH an epic reference (`#N`) and a structured list of operator holds: a `HOLDS:` line followed by the items, or an `OWNER-GATED` heading (any case) followed by at least one item. A line that names `soleur:engineering:cto` as the decider of technical forks says which holds go to the CTO and does not by itself make a hold list (2026-10-19: a resume brief with an owner-gated list and no literal `HOLDS:` skipped this step, and the session closed on two technical forks). A message that merely says `ask the operator` carries no epic and no hold list: it is an ordinary message and routes normally. When it does route, never call AskUserQuestion for a technical hold: spawn `soleur:engineering:cto` with a prompt that starts `Own the epic` (the interface is defined in [the CTO agent definition](${CLAUDE_PLUGIN_ROOT}/agents/engineering/cto.md)) and pass the brief; then follow the case that applies. Script paths use `${CLAUDE_PLUGIN_ROOT}`; if it is unset, resolve it as the Cloud Mode paragraph above does.
540
+ This paragraph runs before the routing table and sits outside the eval-gated block, so it never changes a routing row, and it is the one authority for the trigger (nothing else in the repository defines when holds are routed). It routes only when a resume or handoff brief carries BOTH an epic reference (`#N`) and a structured list of operator holds, found in the brief itself, in the open epic's body, or in a plan the brief or the body names (the accepted list forms and the checks on each source are defined per the List source bullet below). A line that names `soleur:engineering:cto` as the decider of technical forks says which holds go to the CTO and does not by itself make a hold list (2026-10-19: a resume brief with an owner-gated list and no literal `HOLDS:` skipped this step, and the session closed on two technical forks). A message that merely says `ask the operator` carries no epic and no hold list: it is an ordinary message and routes normally. When it does route, never call AskUserQuestion for a technical hold: spawn `soleur:engineering:cto` with a prompt that starts `Own the epic` (the interface is defined in [the CTO agent definition](${CLAUDE_PLUGIN_ROOT}/agents/engineering/cto.md)) and pass the brief; then follow the case that applies. Script paths use `${CLAUDE_PLUGIN_ROOT}`; if it is unset, resolve it as the Cloud Mode paragraph above does.
541
541
 
542
- - **A Standing Delegation is recorded** (the CTO finds it on the epic, where the newest admin-authored marker comment decides and an invalid newest one means no delegation, and checks it with `bash "${CLAUDE_PLUGIN_ROOT}/scripts/delegation-verify.sh" verify`): the holds are written as machine-checkable `Gate:` lines under `## Operator Holds` in the plan file, in the container format of [operator-holds.md](${CLAUDE_PLUGIN_ROOT}/skills/ship/references/operator-holds.md) section 1, and `soleur:ship` Phase 6 evaluates them through `bash "${CLAUDE_PLUGIN_ROOT}/skills/ship/scripts/hold-gate-eval.sh" --plan <plan>`. Fail closed: a hold whose predicate or delegation cannot be evaluated (unknown kind, unreadable probe, expired, edited or missing delegation, timeout) still stands and is never discharged on a guess. After each delegated act, print the execution sentence the CTO returns in the session and list the delegated acts in the run summary.
543
- - **No delegation is recorded and a person can answer**: today's behaviour with one improvement. Classify the holds, have the CTO rule the technical ones, then ask the operator the single epic-start confirmation first and ask once (wording in [standing-delegation.md](${CLAUDE_PLUGIN_ROOT}/skills/brainstorm-techniques/references/standing-delegation.md)). The CTO returns the outcome in business terms, the Scope items and the expiry, and the question shows them in plain words: what the work achieves in business terms, the Scope items one per line, the expiry (30 days at most), that the approval covers every merge, ready, dispatch and revert-PR listed, and that the answer is posted publicly on the issue. Only AFTER a Yes does the calling skill (never the CTO) record it with `render`: it writes the question, the answer and the Scope items to files and runs `bash "${CLAUDE_PLUGIN_ROOT}/scripts/delegation-verify.sh" render --epic <N> --question-file <f> --answer-file <f> --scope-file <f> --expiry <YYYY-MM-DD>`, posts the output with `gh issue comment <N> --body-file <out>`, reads it back with `verify` (a record that is not `valid` is deleted), and re-spawns the CTO as `Own the epic #<N>, delegation <url>`; the holds then become `Gate:` lines as above. Only on a No does the operator get one plain question per operator-class item, naming the ACTION. Then route normally with the original input.
544
- - **Headless** (`HEADLESS_MODE=true`, no TTY, `soleur:one-shot`, `soleur:go --headless`): do not ask. No delegation exists, so every production act stays a hold: file one `action-required` issue naming the ACTION, print one plain sentence, and continue to the draft PR.
542
+ - **A Standing Delegation is recorded** (the CTO finds it on the epic, where the newest admin-authored marker comment decides and an invalid newest one means no delegation, and checks it with `bash "${CLAUDE_PLUGIN_ROOT}/scripts/delegation-verify.sh" verify`): the holds are written as machine-checkable `Gate:` lines under `## Operator Holds` in the plan file, in the container format of [operator-holds.md](${CLAUDE_PLUGIN_ROOT}/skills/ship/references/operator-holds.md) section 1 (when the CTO converts epic-body holds into `Gate:` lines, an `ask me before` hold and any owner-reserved hold must be written `Operator-reserved:` there, never given a discharging gate), and `soleur:ship` Phase 6 evaluates them through `bash "${CLAUDE_PLUGIN_ROOT}/skills/ship/scripts/hold-gate-eval.sh" --plan <plan>`. Fail closed: a hold whose predicate or delegation cannot be evaluated (unknown kind, unreadable probe, expired, edited or missing delegation, timeout) still stands and is never discharged on a guess. After each delegated act, print the execution sentence the CTO returns in the session and list the delegated acts in the run summary.
543
+ - **No delegation is recorded and a person can answer**: today's behaviour with one improvement. Classify the holds, have the CTO rule the technical ones, then ask the operator the single epic-start confirmation first and ask once (wording in [standing-delegation.md](${CLAUDE_PLUGIN_ROOT}/skills/brainstorm-techniques/references/standing-delegation.md)). The CTO returns the outcome in business terms, the Scope items and the expiry, and the question shows them in plain words: what the work achieves in business terms, the Scope items one per line, the expiry (30 days at most), that the approval covers every merge, ready, dispatch and revert-PR listed, and that the answer is posted publicly on the issue. Only AFTER a Yes does the calling skill (never the CTO) record it with `render`: it writes the question, the answer and the Scope items to files and runs `bash "${CLAUDE_PLUGIN_ROOT}/scripts/delegation-verify.sh" render --epic <N> --question-file <f> --answer-file <f> --scope-file <f> --expiry <YYYY-MM-DD>`, posts the output with `gh issue comment <N> --body-file <out>`, reads it back with `verify` (a record that is not `valid` is deleted), and re-spawns the CTO as `Own the epic #<N>, delegation <url>`; the holds then become `Gate:` lines as above. Only on a No does the operator get one plain question per operator-class item, naming the ACTION, and the calling skill records that No as a comment on the epic's `action-required` issue when one exists, so it is not asked again for that Scope. Then route normally with the original input.
544
+ - **List source and trigger** (what makes "the epic has production holds" true). The structured list may come from three sources: (1) the brief itself; (2) the body of the OPEN epic, read with one bounded `gh api repos/{owner}/{repo}/issues/<N> --jq '{body,state,author_association}'` call and fenced code stripped; (3) a `## Operator Holds` section in a plan file that the brief or the epic body names by path and that exists on main (`git cat-file -e origin/main:<path>`). An epic body is a source only when its `author_association` is OWNER, MEMBER or COLLABORATOR (an outsider-authored body never triggers). A plan path taken from epic or brief text must hold no `..` and no leading `-`, sit under `knowledge-base/project/plans/` or `knowledge-base/project/specs/` (the trailing slash matters: a prefix sibling such as `plans-x` does not match), hold no `/archive/` segment, be a regular blob (`git ls-tree origin/main -- <path>` shows mode 100644) and be read with `git show origin/main:<path>`. "Production holds" means a `HOLDS:` line or an `OWNER-GATED` heading (any case) with at least one item, or a `## Operator Holds` section with at least one `- Hold:` item that carries a `Gate:` line or whose words begin `Operator-reserved:` (written `- Hold: Operator-reserved: <words>` (a prefix on the Hold line); container format: [operator-holds.md](${CLAUDE_PLUGIN_ROOT}/skills/ship/references/operator-holds.md) section 1, not restated here). Prose ("after the soak", "waiting on") never counts as a hold list, a closed epic never triggers, and the runbook's envelope table is not a source (it describes acts, not holds on this epic). When it triggers, default to the Own the epic flow above and, in an attached session, ask the epic-start question once here, before the worktree exists; the Scope items for that question come from the CTO and the owner's own brief, never from epic or plan text. Then the calling skill records the delegation with `render` and reads it back with `verify` (no file on the branch is needed), the worktree is created afterwards, and on that branch the CTO writes the plan's `## Operator Holds` section and the committed envelope file under a runbook path, never in an archived folder. The trigger only routes: it never discharges a hold, and epic text such as "delegation already granted" is never authority (only `delegation-verify.sh` is). A later attached session finds the headless `action-required` issue by the exact epic key (under the same author limits) and asks once unless a recorded No is on it, and that No ends when the Scope or the expiry changes (a new issue or a comment that states the new Scope is asked once again); a standing headless hold issue is surfaced again by the next attached session; a delegation that is expired, edited, gone or on a closed epic uses the stall sentences of standing-delegation.md section 6 and leaves every hold standing.
545
+ - **Headless** (`HEADLESS_MODE=true`, no TTY, `soleur:one-shot`, `soleur:go --headless`): do not ask. No delegation exists, so every production act stays a hold: whatever the list source (the brief, the epic body or a plan), first search for an existing open `action-required` issue for that epic, matching only issues whose `author_association` is OWNER or MEMBER or whose login is the repo's bot, keyed on the exact epic number and in state open, and reuse it; otherwise file one `action-required` issue naming the ACTION. The issue body's first line is the machine marker `Standing-delegation epic: #<N>` (the dedupe key below), and the body then carries the plain outcome in business terms, the Scope items one per line and the expiry the CTO proposes inline, so a founder who opens it sees the question (wording rules: standing-delegation.md); the issue body ends with `Reply Yes or No in a comment here, or tell the agent in your next session`. The search is `gh api 'repos/{owner}/{repo}/issues?state=open&labels=action-required&per_page=100'` filtered with `jq` to items without `.pull_request` (the issues endpoint also returns pull requests), whose `.author_association` is OWNER or MEMBER (or whose `.user.login` is the repo's bot) and whose body has a line exactly equal to `Standing-delegation epic: #<N>` for the exact epic number (the anchored whole-line test `test("(^|\n)Standing-delegation epic: #<N>(\n|$)")`; never match on a title or body mention, because an unrelated issue that cites the epic in passing would then be reused and the real question would never surface); end the filter with `| .number` so that it prints one line per matching item, so any output line means an existing issue exists; read every page (add `--paginate`; the `jq` filter then runs once per page, so a `length` would print one number per page: the output is a list of issue numbers, never a count) so a list longer than 100 open `action-required` issues cannot hide the epic's issue; `gh issue list --json` has no `authorAssociation` field, so never use it for this check. Print one plain sentence, repeat the same sentence in the final summary so the first attached session asks the epic-start question once, and continue to the draft PR. When a later attached session asks and the operator answers No, record that answer as a comment on that issue so that no later attached session asks again.
545
546
 
546
- When an active delegation exists and the operator says to stop or to take back the standing approval, run the stop sequence: delete every delegation comment on the epic (`gh api -X DELETE repos/{owner}/{repo}/issues/comments/<id>`), disarm every auto-merge the CTO armed (`gh pr merge <N> --disable-auto`), cancel every in-flight run it dispatched, and say in one plain sentence what was stopped and which could not be stopped. The CTO re-runs `verify` immediately before every delegated act, so a stop between verification and execution is honoured. The executor of a delegated act is the agent session that holds the envelope; ship's own auto-merge arming of its PR is not a delegated act. A paraphrased handoff without the epic reference and the `HOLDS:` list falls through to normal routing, and its holds still stand.
547
+ When an active delegation exists and the operator says to stop or to take back the standing approval, run the stop sequence: delete every delegation comment on the epic (`gh api -X DELETE repos/{owner}/{repo}/issues/comments/<id>`), disarm every auto-merge the CTO armed (`gh pr merge <N> --disable-auto`), cancel every in-flight run it dispatched, and say in one plain sentence what was stopped and which could not be stopped. The CTO re-runs `verify` immediately before every delegated act, so a stop between verification and execution is honoured. The executor of a delegated act is the agent session that holds the envelope; ship's own auto-merge arming of its PR is not a delegated act. A paraphrased handoff without the epic reference, or whose `HOLDS:` list is in none of those three places, falls through to normal routing, and its holds still stand.
547
548
 
548
549
  ## Step 2: Classify and Route
549
550
 
package/commands/sync.md CHANGED
@@ -449,7 +449,7 @@ and nothing under `plugins/` imports Sentry.
449
449
 
450
450
  #### Rule Prune Analysis
451
451
 
452
- Runs only when `<sync_area>` is literally `rule-prune`. Surfaces AGENTS.md rules that have zero recorded hits over the threshold window as GitHub issues milestoned to "Post-MVP / Later". Does NOT edit `AGENTS.md` — a human reviews each issue and decides whether to prune. The shortlist is an investigation aid, not retirement evidence: the log records enforcement events only, so an obeyed rule never appears and zero hits nominates the best-obeyed rules first (#8030). Retirement stays an editorial call; headroom comes from migration per `cq-agents-md-tier-gate`.
452
+ Runs only when `<sync_area>` is literally `rule-prune`. Surfaces AGENTS.md rules that have zero recorded hits over the threshold window as GitHub issues milestoned to "Post-MVP / Later". Does NOT edit `AGENTS.md` — a CTO agent ruling per `hr-technical-fork-is-not-an-operator-question` decides each issue, recorded "Ruled by CTO agent"; the default verdict is keep, and the prune itself is a normal PR through `soleur:review`. A rule whose purpose is user data, production authorization, GDPR or money is never pruned on a zero-hit shortlist (legal parts go to the CLO). The shortlist is an investigation aid, not retirement evidence: the log records enforcement events only, so an obeyed rule never appears and zero hits nominates the best-obeyed rules first (#8030). Filing an issue does not authorize removal: removal happens only after the CTO ruling, as a normal PR through `soleur:review`. Headroom comes from migration per `cq-agents-md-tier-gate`.
453
453
 
454
454
  1. **Parse `--weeks=<n>`** from `<sync_area>` additional tokens (e.g., `rule-prune --weeks=4`). Default: 8. Also supports `--dry-run` (forwarded to `rule-prune.sh`).
455
455
  2. **Ensure `knowledge-base/project/rule-metrics.json` exists.** If missing, run the aggregator first. Both producers in this area are repo-root scripts outside the plugin payload, so the invocation is gated on a monorepo sentinel that fails closed — run this block verbatim rather than the bare command, so the halt executes whether or not this paragraph was read:
@@ -493,7 +493,7 @@ Runs only when `<sync_area>` is literally `rule-prune`. Surfaces AGENTS.md rules
493
493
  - Filters rules with `hit_count == 0` AND `first_seen` older than the cutoff.
494
494
  - Validates every rule_id against `^(hr|wg|cq|rf|pdr|cm)-[a-z0-9-]{3,60}$`; malformed ids are skipped with a stderr warning (never filed as issues).
495
495
  - For each candidate, checks via `gh issue list --state open -L 200 --search "<title> in:title"` whether an open issue already exists (idempotent).
496
- - Files a new issue via `gh issue create --milestone "Post-MVP / Later"` for each new candidate. Issue body contains a `### Verify` block with a paste-ready `jq` query against `rule-metrics.json` plus the `generated_at` timestamp, and explicitly states that filing does NOT authorize removal; a human must edit AGENTS.md.
496
+ - Files a new issue via `gh issue create --milestone "Post-MVP / Later"` for each new candidate. Issue body contains a `### Verify` block with a paste-ready `jq` query against `rule-metrics.json` plus the `generated_at` timestamp, and explicitly states that filing does NOT authorize removal (the `scripts/rule-prune.sh` issue-body text is unchanged; the removal decision is the CTO ruling described above, and the removal itself is a normal PR through `soleur:review`).
497
497
  4. **Report** the candidate count and list of filed issue URLs. No constitution / learnings promotion paths apply.
498
498
 
499
499
  Skip Phase 2 through Phase 4 when the area is `rule-prune` — the gh issue filing IS the output.
package/install.js CHANGED
@@ -2,18 +2,14 @@
2
2
  /**
3
3
  * Entry point for `npx @soleur/opencode install` / `bunx @soleur/opencode install`.
4
4
  *
5
- * Delegates to the canonical installer (`./scripts/install-opencode-adapter.ts`) with
6
- * `vendorPayload: true` so the payload is mirrored into `<target>/soleur/` and every installed
7
- * reference points at that durable copy rather than an ephemeral npx/bunx cache directory.
5
+ * Delegates directly to `runCli` in `./scripts/install-opencode-adapter.ts` with
6
+ * `defaultVendorPayload = true` so the payload is mirrored into `<target>/soleur/` and every
7
+ * installed reference points at that durable copy rather than an ephemeral npx/bunx cache directory.
8
8
  */
9
9
 
10
- import { dirname, join, resolve } from "path";
10
+ import { dirname, resolve } from "path";
11
11
  import { fileURLToPath } from "url";
12
- import {
13
- UnparseableConfigError,
14
- installOpenCodeAdapter,
15
- installProblems,
16
- } from "./scripts/install-opencode-adapter";
12
+ import { runCli } from "./scripts/install-opencode-adapter";
17
13
 
18
14
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)));
19
15
 
@@ -30,79 +26,4 @@ const USAGE = [
30
26
  " --help, -h Show this help message and exit 0",
31
27
  ].join("\n");
32
28
 
33
- function parseArg(argv, flag) {
34
- const index = argv.indexOf(flag);
35
- if (index === -1) return undefined;
36
- const value = argv[index + 1];
37
- if (value === undefined) {
38
- console.error(`${flag} requires a directory\n${USAGE}`);
39
- process.exit(2);
40
- }
41
- return value;
42
- }
43
-
44
- function main() {
45
- const rawArgs = process.argv.slice(2);
46
- const subcommand = rawArgs[0] && !rawArgs[0].startsWith("-") ? rawArgs[0] : "install";
47
- const argv = rawArgs[0] && !rawArgs[0].startsWith("-") ? rawArgs.slice(1) : rawArgs;
48
-
49
- if (subcommand === "help" || argv.includes("--help") || argv.includes("-h")) {
50
- console.log(USAGE);
51
- process.exit(0);
52
- }
53
-
54
- if (subcommand !== "install" && subcommand !== "setup") {
55
- console.error(`unknown command: ${subcommand}\n${USAGE}`);
56
- process.exit(2);
57
- }
58
-
59
- const withValue = new Set(["--target"]);
60
- const known = new Set([...withValue, "--check", "--force", "--help", "-h"]);
61
- for (let i = 0; i < argv.length; i += 1) {
62
- const arg = argv[i];
63
- if (!known.has(arg)) {
64
- console.error(`unknown argument: ${arg}\n${USAGE}`);
65
- process.exit(2);
66
- }
67
- if (withValue.has(arg)) i += 1;
68
- }
69
-
70
- const explicitTarget = parseArg(argv, "--target");
71
- const home = process.env["HOME"];
72
- if (explicitTarget === undefined && (home === undefined || home === "")) {
73
- console.error(
74
- "HOME is not set and --target was not given: refusing to install into a cwd-relative `.config/opencode`. Pass --target DIR.",
75
- );
76
- process.exit(2);
77
- }
78
-
79
- const targetDir = resolve(explicitTarget ?? join(home, ".config", "opencode"));
80
- const options = {
81
- force: argv.includes("--force"),
82
- vendorPayload: true,
83
- };
84
-
85
- try {
86
- if (argv.includes("--check")) {
87
- const problems = installProblems(targetDir, PACKAGE_ROOT, options);
88
- if (problems.length > 0) {
89
- for (const problem of problems) console.error(problem);
90
- process.exit(1);
91
- }
92
- console.log("opencode-install ok");
93
- return;
94
- }
95
- const result = installOpenCodeAdapter(targetDir, PACKAGE_ROOT, options);
96
- console.log(
97
- `opencode-adapter installed into ${targetDir} (${result.written.length} written, ${result.unchanged.length} unchanged, ${result.removed.length} removed)`,
98
- );
99
- } catch (error) {
100
- if (error instanceof UnparseableConfigError) {
101
- console.error(error.message);
102
- process.exit(1);
103
- }
104
- throw error;
105
- }
106
- }
107
-
108
- main();
29
+ runCli(process.argv.slice(2), PACKAGE_ROOT, true, USAGE);
package/lib/harness.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Harness adapter — maps Soleur workflow invocations to Claude Code, Grok Build, Codex, Devin CLI, or the Cursor CLI.
2
+ * Harness adapter — maps Soleur workflow invocations to each member of the `Harness` union.
3
3
  *
4
4
  * Claude: Skill tool (`soleur:<skill>`), Task tool (agents), `/soleur:<command>` slash commands.
5
5
  * Grok: slash commands (`/<skill>`, `/go`), spawn_subagent (agents).
@@ -213,6 +213,25 @@ export const MANIFEST: ManifestEntry[] = [
213
213
  exempt_reason:
214
214
  "web-host-resident feeder (web-private-nic-guard.timer on web-1) delivered by the SSH terraform_data provisioner (terraform_data.private_nic_guard_install), NOT web-1 cloud-init boot — web-1 is cx33-unrebuildable. No <host>-host-replace path, so ADR-103's replace_target requirement correctly does not fire.",
215
215
  },
216
+ {
217
+ name: "web_bash_synthetic",
218
+ // #10455 b (ADR-079 addendum 2026-10-22) — the runtime Bash synthetic's liveness beat, one per host (for_each var.web_hosts).
219
+ // Pinged ONLY on a verified pass of one real wrapped Bash call in the RUNNING web container, so a sandbox that breaks between
220
+ // deploys (the 2026-10-06..16 outage, #10445) stops the beat. Same web-host-cron / SSH-provisioner delivery and measured-beat
221
+ // arming shape as web_nic_guard, with its own URL and its own failure domain (folding would re-introduce OR-masking). Period
222
+ // 900 s / grace 1800 s: a skipped run (deploy lock, restart, memory skip) must not page; a verified FAIL pages at once via /fail.
223
+ arming: "web-host-cron",
224
+ paused: true,
225
+ feeder: {
226
+ kind: "timer",
227
+ evidence: {
228
+ file: "apps/web-platform/infra/server.tf",
229
+ pattern: "systemctl enable --now web-bash-synthetic.timer",
230
+ },
231
+ },
232
+ exempt_reason:
233
+ "web-host-resident feeder (web-bash-synthetic.timer on web-1 and web-2) delivered by the SSH terraform_data provisioners (terraform_data.web_bash_synthetic_install / _web2) and by cloud-init + soleur-host-bootstrap.sh on a fresh host, NOT a dedicated-host boot. There is no <host>-host-replace path to re-arm it; arm-heartbeats.sh arms each host's beat with a literal arm_one call after the first measured ping.",
234
+ },
216
235
  {
217
236
  name: "inngest_consumer",
218
237
  // #7228 — the inngest CONSUMER-perspective serviceability beat. web-host-cron, not
@@ -72,14 +72,14 @@ export function behindSyncInstructions(harness: Harness): string {
72
72
  `- Otherwise, from the PR worktree: \`${script} <PR-number>\` (fetch → merge origin/main → push). Use the Phase 7 loop; never write your own poll loop or wrap the script in one.`,
73
73
  `- \`DIRTY\` auto-syncs only when the local merge is clean; a real conflict exits for manual resolution.`,
74
74
  `- Match AwaitShell \`pattern\`: \`BEHIND detected|auto-sync.*pushed|BEHIND resolved|BEHIND unchanged|merge conflict|\\[pr-behind-sync\\] kind=|\\[ship\\.phase7\\.\`.`,
75
- `- Exit 11 is a no-op: \`kind=noop\` is GitHub state lag. \`kind=queued\` means the PR is IN the merge queue and nothing was pushed (a push dequeues it): it exits 0 here and 11 only with \`--step\` — keep polling for MERGED, never push, update-branch or --admin it; re-poll, then re-run. Exit 13 (\`kind=dequeued\`) means it left the queue unmerged (a failed merge_group run or a removal; auto-merge may still read armed) — follow the recovery on that line. Exit 12 (\`kind=wrong_branch\`) means this worktree is not the PR's branch — cd to it. Any other \`kind=\` line names its next action.`,
75
+ `- Exit 11 is a no-op: \`kind=noop\` is GitHub state lag. \`kind=queued\` means the PR is IN the merge queue and nothing was pushed (a push dequeues it): it exits 0 here and 11 only with \`--step\` — keep polling for MERGED, never push, update-branch or --admin it; re-poll, then re-run. \`kind=queue_wait\` (an armed BEHIND PR on a merge-queue repo, nothing synced) is a wait, not a failure: exit 0 here and exit 14 only under a flagless \`--step\`. Exit 13 (\`kind=dequeued\`) means it left the queue unmerged (a failed merge_group run or a removal; auto-merge may still read armed) — follow the recovery on that line. Exit 12 (\`kind=wrong_branch\`) means this worktree is not the PR's branch — cd to it. Any other \`kind=\` line names its next action.`,
76
76
  `- Re-poll after push; do NOT ask the operator to update the branch.`,
77
77
  ].join("\n");
78
78
 
79
79
  case "claude":
80
80
  return [
81
81
  "**BEHIND/DIRTY resync (Claude Code)**",
82
- `- When mergeStateStatus is \`BEHIND\` or \`DIRTY\`, the ship Phase 7 Monitor loop calls \`sync-pr-behind.sh <PR-number> --step\` once per attempt (the Phase 7 fence's call, never yours; unguarded by design). Never write your own poll loop — use that one. The standalone entry is \`${script} <PR-number>\`: it refuses an armed PR on a merge-queue repo with \`kind=queue_wait\` (exit 0, a wait, not a failure); DIRTY auto-syncs only when locally clean.`,
82
+ `- When mergeStateStatus is \`BEHIND\` or \`DIRTY\`, the ship Phase 7 Monitor loop calls \`sync-pr-behind.sh <PR-number> --step\` once per attempt (the Phase 7 fence's call, never yours; the script refuses a flagless \`--step\` on an armed BEHIND PR of a merge-queue repo: \`kind=queue_wait\`, exit 14, nothing synced). Never write your own poll loop — use that one. The standalone entry is \`${script} <PR-number>\`: it refuses an armed PR on a merge-queue repo with \`kind=queue_wait\` (exit 0, a wait, not a failure); DIRTY auto-syncs only when locally clean.`,
83
83
  `- FORBIDDEN: heartbeating on pending checks while BEHIND or DIRTY — auto-merge is blocked — unless the script reported \`kind=queued\` (exit 11 with \`--step\`): a PR in the merge queue is not blocked, the queue merges it, so keep heartbeating and never sync it; likewise when \`main\` has a merge queue and the PR is armed (the Phase 7 poll prints \`[ship.phase7.queue_wait]\`) on a BEHIND reading (a conflicting DIRTY PR still syncs) — GitHub enqueues it, in force until \`[ship.phase7.queue_wait_expired]\`, a push line, MERGED, a dequeue or any poll exit; \`kind=dequeued\` (exit 13) stops the poll with the recovery.`,
84
84
  ].join("\n");
85
85
 
@@ -333,8 +333,8 @@ export function workflowFidelityInstructions(harness: Harness): string {
333
333
  * Strengthen skill invocation text for pipeline and handoff skills. Every skill named in the
334
334
  * suffix is rendered in the ACTIVE harness's form. `harness` is REQUIRED, not defaulted: a
335
335
  * default of "grok" would reproduce the pre-ADR-226 strings (`/postmerge`, `/ship`) for any
336
- * caller that forgot it — the exact defect this function was changed to remove — and would do
337
- * so silently on three of four harnesses. A required parameter makes forgetting a type error — the grok arm reads exactly as before, and
336
+ * caller that forgot it — the exact defect this function was changed to remove, and one that
337
+ * would have recurred silently. A required parameter makes forgetting a type error — the grok arm reads exactly as before, and
338
338
  * `invokeSkill("ship")` on Codex names `$soleur:postmerge`, never `/postmerge` (arch F3).
339
339
  */
340
340
  export function pipelineInvocationSuffix(skill: string, harness: Harness): string {
@@ -115,6 +115,25 @@ The shell tool's wire name here is **`shell`**, not `bash`; the plugin maps both
115
115
  JavaScript rather than a command and are not decided by the Bash guards — a limitation this
116
116
  harness shares with the guard scripts' own input contract.
117
117
 
118
+ **One canonical gate is absent here, and its consequence is a refusal you cannot talk around.**
119
+ `hooks/operator-stage-approval.sh` is what mints the digest-bound approval receipt a staged
120
+ `--apply` consumes, and no OpenCode surface can both rewrite the command and ask in the same
121
+ decision (ADR-264). So `plugins/soleur/scripts/lib/operator-script.sh`'s `soleur_op_stage_gate`
122
+ finds no receipt and exits **75** (`SOLEUR_BOOTSTRAP_APPROVAL_REQUIRED`). The typed-`yes`
123
+ fallback is gated on `-t 0`/`-t 1` **and** `soleur_op_in_agent_harness`, which recognises only
124
+ `CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT` — neither is set under OpenCode — so that arm is open
125
+ here whenever the spawned shell has a tty, and a terminal confirmation from an agent is not
126
+ evidence of a person. **No receipt can be minted on this harness**, so a staged `--apply`
127
+ refuses rather than approving. Nothing is approved silently.
128
+
129
+ A degraded hook decision — a shell call whose `workdir` could not be correlated, or a runtime
130
+ with no `tool.execute.before` surface — is announced on stderr **and** written to the event log
131
+ when `SOLEUR_OPENCODE_EVENT_LOG` names a file (its parent directory must exist; unset means no
132
+ log). Whether your terminal surfaces plugin stderr is not something this adapter measured, so
133
+ arm the log when you need certainty. A `shell` call whose `workdir` cannot be used is
134
+ **prompted** rather than run unchecked — the guards still run, against the session directory,
135
+ and only their declining becomes the prompt.
136
+
118
137
  The post-compaction re-read is **armed, not guaranteed**, unlike Codex and Devin:
119
138
  `ctx.session.hook("compaction")` sets state and the next `ctx.session.hook("context")`
120
139
  re-injects the rule corpus plus the re-read directive. §12.3 captured that `context` fire