forgedock 1.7.0 → 1.8.0

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 (49) hide show
  1. package/README.md +2 -1
  2. package/bin/engine/admission.mjs +242 -7
  3. package/bin/engine/admission.test.mjs +183 -2
  4. package/bin/engine/phases.mjs +31 -22
  5. package/bin/engine/resolve.mjs +8 -1
  6. package/bin/engine/resolve.test.mjs +5 -0
  7. package/bin/engine-cli.mjs +2 -0
  8. package/bin/engine.mjs +26 -17
  9. package/bin/forgedock.mjs +124 -48
  10. package/bin/hooks/pre-tool-use.mjs +573 -2
  11. package/bin/journey.mjs +11 -7
  12. package/bin/labels.json +5 -0
  13. package/bin/opencode-adapter.mjs +652 -92
  14. package/bin/orchestrate-preflight.mjs +434 -0
  15. package/bin/runner.mjs +77 -9
  16. package/commands/audit-agents.md +75 -26
  17. package/commands/autopilot.md +4 -4
  18. package/commands/cleanup.md +10 -5
  19. package/commands/issue.md +95 -35
  20. package/commands/orchestrate/phase-1-resolve.md +70 -32
  21. package/commands/orchestrate/phase-3-dependency.md +167 -22
  22. package/commands/orchestrate/phase-4-execution.md +691 -62
  23. package/commands/orchestrate/phase-5-cleanup.md +5 -11
  24. package/commands/orchestrate/phase-6-report.md +27 -1
  25. package/commands/orchestrate/safety.md +1 -1
  26. package/commands/orchestrate.md +39 -1
  27. package/commands/quality-gate.md +30 -10
  28. package/commands/review-pr-agents/protocols.md +13 -0
  29. package/commands/review-pr-agents.md +8 -0
  30. package/commands/review-pr-staging.md +117 -17
  31. package/commands/review-pr.md +95 -23
  32. package/commands/work-on/build/validate.md +25 -4
  33. package/commands/work-on/close.md +40 -10
  34. package/commands/work-on/decompose.md +33 -1
  35. package/commands/work-on/investigate.md +33 -0
  36. package/commands/work-on/remediate.md +40 -5
  37. package/commands/work-on/review.md +27 -0
  38. package/commands/work-on.md +48 -5
  39. package/docs/CONFIG.md +51 -15
  40. package/docs/OPENCODE.md +128 -25
  41. package/docs/spec/review-protocol.md +7 -0
  42. package/package.json +1 -1
  43. package/scripts/check-admission-parity.mjs +1 -0
  44. package/scripts/check-command-side-effects.sh +14 -13
  45. package/scripts/check-shell-portability.sh +36 -0
  46. package/scripts/check-spec-markers.sh +21 -11
  47. package/scripts/checkbox-sections.test.sh +67 -0
  48. package/scripts/extract-affected-files.sh +85 -5
  49. package/scripts/extract-affected-files.test.sh +154 -2
@@ -273,7 +273,7 @@ If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's
273
273
  Skill(skill="work-on/remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
274
274
  ```
275
275
 
276
- **After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: when `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
276
+ **After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
277
277
 
278
278
  This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
279
279
 
@@ -358,9 +358,21 @@ fi
358
358
 
359
359
  **Batch issue pipeline rules** (when `IS_BATCH=true`):
360
360
  - Build phases execute exactly as normal (the batch issue body IS the spec for what to fix)
361
- - After successful merge, auto-close all member issues with a cross-reference:
361
+ - Batch members are referenced in the PR body with `Refs #N`, never `Closes #N`; the batch issue is the only issue the PR may close.
362
+ - After successful merge, re-read every member's live state and labels before closing it. A member that is `needs-human`, `blocked`, or `operator-only` remains open and is reported as a split outcome:
362
363
  ```bash
363
364
  for MEMBER in "${BATCH_MEMBERS[@]}"; do
365
+ MEMBER_SNAPSHOT=$(gh issue view "$MEMBER" {GH_FLAG} --json state,labels \
366
+ --jq '{state: .state, labels: [.labels[].name]}' 2>/dev/null) || {
367
+ echo "WARNING: could not verify batch member #${MEMBER}; leaving it open"
368
+ continue
369
+ }
370
+ MEMBER_GATED=$(echo "$MEMBER_SNAPSHOT" | jq -r \
371
+ '(.state != "OPEN") or ([.labels[] | select(. == "needs-human" or . == "blocked" or . == "operator-only")] | length > 0)')
372
+ if [ "$MEMBER_GATED" = "true" ]; then
373
+ echo "SPLIT OUTCOME: #${MEMBER} remains open because it requires a human or operator action."
374
+ continue
375
+ fi
364
376
  gh issue close "$MEMBER" {GH_FLAG} \
365
377
  --comment "Resolved as part of batch PR #{PR_NUMBER} (#{ISSUE_NUMBER}). See batch issue for details."
366
378
  gh issue edit "$MEMBER" {GH_FLAG} --add-label "workflow:merged" 2>/dev/null || true
@@ -2081,11 +2093,42 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
2081
2093
 
2082
2094
  ```bash
2083
2095
  BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
2084
- REMAINING_BEFORE=$(echo "$BODY" | grep -c '^- \[ \]' || true)
2085
- HAS_PHASE_HEADINGS=$(echo "$BODY" | grep -cP '^#{2,3} ' || true)
2096
+ REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -cE '^[-*+] \[ \]' || true)
2097
+
2098
+ # Structural test: count heading-delimited sections that contain checkbox items.
2099
+ # Multi-phase == 2+ checkbox-bearing sections. A single checkbox group is
2100
+ # single-phase regardless of how many prose headings surround it.
2101
+ #
2102
+ # - Fenced code blocks are stripped first: issue bodies routinely embed fenced
2103
+ # blocks whose lines start with '#' or contain a literal '- [ ]'. An
2104
+ # unterminated fence keeps the original body so later work is never hidden.
2105
+ # - ATX headings and setext underlines both delimit sections. The ATX pattern
2106
+ # avoids awk interval-quantifier variance across awk implementations.
2107
+ # - grep -E / awk only — no PCRE. '^#+ ' needs none.
2108
+ FENCE_COUNT=$(printf '%s\n' "$BODY" | grep -cE '^(```+|~~~+)' || true)
2109
+ if [ $(( ${FENCE_COUNT:-0} % 2 )) -ne 0 ]; then
2110
+ BODY_STRIPPED="$BODY"
2111
+ else
2112
+ BODY_STRIPPED=$(printf '%s\n' "$BODY" | awk '/^(```+|~~~+)/{f=!f; next} !f')
2113
+ fi
2114
+
2115
+ CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
2116
+ /^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
2117
+ /^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
2118
+ { if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
2119
+ END { if (in_section && has) n++; print n+0 }
2120
+ ')
2121
+
2122
+ # Sub-issue-tracker guard: a decompose parent whose only checkbox group is
2123
+ # '## Sub-Issue Tracker' counts 1 section. Checking those off would mark open
2124
+ # sub-issues done and close the tracker, so any unchecked GFM task item for an
2125
+ # issue forces multi-phase.
2126
+ SUBISSUE_ITEMS=$(printf '%s\n' "$BODY_STRIPPED" | grep -cE '^[-*+] \[ \] #[0-9]+' || true)
2086
2127
  ```
2087
2128
 
2088
- If multi-phase (`HAS_PHASE_HEADINGS > 0` AND `REMAINING_BEFORE > 0`): do NOT check off future phase items. Add PR reference only.
2129
+ **Sync invariant:** Keep the structural computation above and its consuming multi-phase guard in `commands/work-on/close.md` Phase C1 synchronized. The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`; this Phase 6A path is reached only when `REMAINING_BEFORE > 0`. <!-- Fixed: forge#2840, #2874 -->
2130
+
2131
+ If multi-phase (`CHECKBOX_SECTIONS >= 2` OR a `- [ ] #NNN` sub-issue item is present): do NOT check off future phase items. Add PR reference only. A heading count is **not** the test — every templated issue carries `## Problem`/`## Evidence`/`## Context`, so it is `> 0` universally.
2089
2132
 
2090
2133
  If single-phase or final phase: check off all `[ ]` items, add PR reference.
2091
2134
 
package/docs/CONFIG.md CHANGED
@@ -298,9 +298,13 @@ Concurrency limits and cascade admission policy for `/orchestrate`'s batch engin
298
298
 
299
299
  ```yaml
300
300
  orchestration:
301
- # Maximum number of /work-on sub-agents dispatched concurrently. Guards against
302
- # saturating the Anthropic API rate limit in one burst on a large ready set.
303
- # Default: 12.
301
+ # Maximum number of top-level /work-on agents dispatched concurrently. Each
302
+ # worker normally fans out to about 8 total subagent spawns (build, quality
303
+ # gate, and review included), so set this to no more than session_budget / 8.
304
+ # Default: 12; use 25 or fewer for a 200-spawn session budget. This limits
305
+ # in-flight workers only: under cascade.policy: all, raising it increases
306
+ # both completion and finding-spawn throughput and does not itself bound or
307
+ # shrink cascade queue growth.
304
308
  max_concurrent: 12
305
309
 
306
310
  cascade:
@@ -324,13 +328,26 @@ orchestration:
324
328
  # spawned from a review-finding. Accepts a positive integer or the literal
325
329
  # string "unlimited". Governs Phase 1 resolve-time admission only (the
326
330
  # `cascade`/`review-findings`/`findings` resolution in phase-1-resolve.md)
327
- # — it does NOT relax Step 4C's absolute autonomous-cascade defer, which
328
- # stays hardcoded regardless of this value (see phase-4-execution.md Step
329
- # 4C rule 1). This is what makes "admit gen-2, stop at gen-3" expressible,
330
- # which the pre-#2234 --allow-gen2 flag (all-or-nothing) could not say.
331
+ # — it does NOT individually dispatch deferred generation-2+ findings in
332
+ # Step 4C. Bounded P3 batching is the sole automated exception: it may
333
+ # aggregate a finite generation-2 member set under batch_max_generation,
334
+ # as implemented by #2849. This is what makes "admit gen-2, stop at gen-3"
335
+ # expressible, which the pre-#2234 --allow-gen2 flag could not say.
331
336
  # Default: 1 (from the "balanced" preset); "unlimited" under "all".
332
337
  max_generation: 1
333
338
 
339
+ # Maximum generation a P3 batch may aggregate. Batching is a sanctioned,
340
+ # auditable exception to Step 4C's autonomous cascade cap: it can fold
341
+ # several deferred low-priority findings into one reviewed unit, but it
342
+ # must never make recursion unbounded. Every batch records the maximum
343
+ # member generation in FORGE:BATCH_MAX_GENERATION. This value is always
344
+ # finite, even under policy: all. Default: 2.
345
+ batch_max_generation: 2
346
+
347
+ # Permit closure only of mechanically identical bot/app alerts: same
348
+ # normalized title, generator, and trigger condition, while retaining one
349
+ # canonical issue. Human reports are never included. Default: false.
350
+ dedup_automated: false
334
351
  # Per-batch token ceiling for Step 4C's review-finding cascade dispatch.
335
352
  # New home for the same lever previously read only as
336
353
  # `pipeline.token_budget_per_batch` (kept working as a deprecated alias —
@@ -353,17 +370,30 @@ orchestration:
353
370
  # (avoids serializing agents on same-file predecessor edges). Default:
354
371
  # true (from "balanced"); false under "all".
355
372
  p3_same_file_defer: true
373
+
374
+ # Running findings-spawned / merged-units ceiling. "off" (the default)
375
+ # only reports amplification; a positive decimal defers further
376
+ # same-lineage refinements to the completion sweep once exceeded.
377
+ max_amplification: "off"
378
+
379
+ # Number of consecutive merged-unit observations at ratio >= 1.0 before
380
+ # printing a convergence warning. Default: 3.
381
+ convergence_window: 3
356
382
  ```
357
383
 
358
384
  | Field | Type | Required | Description |
359
385
  |-------|------|----------|-------------|
360
- | `max_concurrent` | integer | No | Max concurrent `/work-on` sub-agent dispatch. Default: 12 |
386
+ | `max_concurrent` | integer | No | Max concurrent top-level `/work-on` dispatches. It does not bound cascade growth; under `policy: all` it raises drain and spawn throughput together. Default: 12. |
361
387
  | `cascade.policy` | string | No | `all` \| `balanced` \| `conservative`. Default: `balanced` |
362
388
  | `cascade.max_generation` | integer or `"unlimited"` | No | Max cascade generation admitted at Phase 1 resolve time. Default: 1 |
389
+ | `cascade.batch_max_generation` | integer | No | Finite maximum member generation that an automated P3 batch may aggregate. Default: 2 |
390
+ | `cascade.dedup_automated` | boolean | No | Allow only mechanically identical bot/app alerts to be closed with a retained canonical issue. Default: `false` |
363
391
  | `cascade.token_budget` | integer or `"unlimited"` | No | Per-batch token ceiling for Step 4C cascade dispatch. Default: 900000 (deprecated alias: `pipeline.token_budget_per_batch`) |
364
392
  | `cascade.defer_on_batch_gated` | boolean | No | Suppress cascade dispatch when the original batch is fully human-gated. Default: `true` |
365
393
  | `cascade.keyword_heuristic` | boolean | No | Defer P3-and-below comment/typo-titled findings. Default: `true` |
366
394
  | `cascade.p3_same_file_defer` | boolean | No | Defer P3 findings sharing a file with the active batch. Default: `true` |
395
+ | `cascade.max_amplification` | positive number or `"off"` | No | Opt-in ceiling for same-lineage refinement dispatch. Default: `"off"` |
396
+ | `cascade.convergence_window` | positive integer | No | Consecutive ratio observations at or above 1.0 before warning. Default: 3 |
367
397
 
368
398
  **Hard invariant — not configurable**: safety exclusions (findings whose `## Problem` section indicates security/billing/anti-bot/auth concerns) are never batched and never auto-admitted by any `cascade.policy`, including `all`. That exclusion is enforced upstream of this section (the P3 batching eligibility check) and has no corresponding key here by design.
369
399
 
@@ -375,10 +405,14 @@ orchestration:
375
405
 
376
406
  ## `pipeline` (OPTIONAL)
377
407
 
378
- Tuning knobs for `/orchestrate`'s batch engine — stall detection, narration verbosity, and (deprecated alias only, see `orchestration.cascade` above) the review-finding cascade token budget. All keys optional; sane defaults apply when omitted.
408
+ Tuning knobs for the CLI backend and `/orchestrate`'s batch engine — invocation and stall timeouts, narration verbosity, and (deprecated alias only, see `orchestration.cascade` above) the review-finding cascade token budget. All keys optional; sane defaults apply when omitted.
379
409
 
380
410
  ```yaml
381
411
  pipeline:
412
+ # Wall-clock limit for one CLI backend invocation. The environment variable
413
+ # FORGEDOCK_CLI_TIMEOUT_MS (milliseconds) takes precedence. Default: 60.
414
+ cli_timeout_minutes: 60
415
+
382
416
  # Minutes an agent may sit idle (no FORGE:HEARTBEAT / label change) before
383
417
  # /orchestrate's stall detector treats it as stuck and attempts an
384
418
  # auto-resume. Default: 15.
@@ -401,12 +435,13 @@ pipeline:
401
435
 
402
436
  | Field | Type | Required | Description |
403
437
  |-------|------|----------|-------------|
438
+ | `cli_timeout_minutes` | integer | No | Wall-clock minutes for one CLI backend invocation. Default: 60. `FORGEDOCK_CLI_TIMEOUT_MS` (milliseconds) takes precedence. |
404
439
  | `stall_timeout_minutes` | integer | No | Idle minutes before the stall detector attempts auto-resume. Default: 15 |
405
440
  | `token_budget_per_batch` | integer | No | **Deprecated** — use `orchestration.cascade.token_budget`. Default: 900000 |
406
441
  | `token_estimate_per_finding` | integer | No | Estimated tokens per P3-or-lower cascade finding. Default: 150000 |
407
442
  | `narration` | string | No | `terse` \| `verbose`. Default: `terse` |
408
443
 
409
- **Commands that use this section**: `orchestrate`
444
+ **Commands that use this section**: CLI backend (`cli_timeout_minutes`); `orchestrate` (remaining keys)
410
445
 
411
446
  ---
412
447
 
@@ -927,7 +962,7 @@ The hook entry is registered idempotently: re-installing (`npx forgedock update`
927
962
  | Command | Action |
928
963
  |---------|--------|
929
964
  | `npx forgedock` / `npx forgedock install` | Registers the SessionStart hook (along with installing commands and writing `forge.yaml`) |
930
- | `npx forgedock update` | Re-registers the hook (relink only — does not touch `forge.yaml`) |
965
+ | `npx forgedock update` | Re-registers the hook when the resolved source can be safely refreshed (relink only — does not touch `forge.yaml`) |
931
966
  | `npx forgedock uninstall` | Removes the hook entry from `settings.json` |
932
967
 
933
968
  ### Opting Out
@@ -948,7 +983,7 @@ See [Per-Directory State Registry](#per-directory-state-registry) below for how
948
983
 
949
984
  `npx`/`npm` never installs `forgedock` to a stable location on your machine — it resolves to wherever `npx`'s cache (or a global npm install, or a git clone) happens to physically live. Historically, `~/.claude/commands/` and the SessionStart hook's script path were symlinked **directly** from that ephemeral location. When npm/npx (or the OS) later evicted its cache, those symlinks silently dangled — Claude Code sessions stopped getting ForgeDock context with no error shown.
950
985
 
951
- As of this feature, every `npx forgedock` (or `npx forgedock update`) run copies ForgeDock's own installable payload into a stable, version-independent home:
986
+ For npm/npx sources, every `npx forgedock` (or `npx forgedock update`) run copies ForgeDock's own installable payload into a stable, version-independent home:
952
987
 
953
988
  ```
954
989
  ~/.forge/
@@ -968,8 +1003,8 @@ As of this feature, every `npx forgedock` (or `npx forgedock update`) run copies
968
1003
  | `npx forgedock` / `npx forgedock install` | Copies the resolved package's payload into `~/.forge/` (content-compared — unchanged files are never rewritten) before linking commands, then links from `~/.forge/`, not the original source |
969
1004
  | `npx forgedock init` | Does not touch `~/.forge/` — `init` only writes `forge.yaml`, it never installs commands |
970
1005
  | `npx forgedock update` (npm/npx install) | Refreshes `~/.forge/` from whatever the currently-resolved package looks like, then relinks |
971
- | `npx forgedock update` (git-clone install) | **Does not** touch `~/.forge/` at all — see the exemption below |
972
- | `npx forgedock doctor` | Reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}` exist |
1006
+ | `npx forgedock update` (git-clone install) | Updates and relinks a `main` checkout; a non-main checkout is left untouched and gets actionable guidance because the current source must remain the source used after the command returns |
1007
+ | `npx forgedock doctor` | Reports the effective source path, version, and git branch, then reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}` for npm installs |
973
1008
 
974
1009
  The copy is idempotent: files whose content is already byte-identical are never rewritten, so steady-state re-runs are fast and don't generate false "updated" noise.
975
1010
 
@@ -979,6 +1014,7 @@ If you run ForgeDock from a git clone (or a git worktree) of the repo itself —
979
1014
 
980
1015
  - `~/.claude/commands/` continues to symlink directly from the clone, exactly as before this feature.
981
1016
  - `~/.forge/{bin,commands,scripts,templates}` correctly does **not** exist for this install mode — `npx forgedock doctor` treats that absence as healthy, not a warning.
1017
+ - `npx forgedock update` never switches a non-main checkout to `main` and then restores it. That transient update would leave the next invocation running the old branch source, so the command reports the effective path, branch, and version and tells you to run from a main checkout instead.
982
1018
 
983
1019
  ### Not the Same as `~/.forge/{runs,index}`
984
1020
 
@@ -1086,7 +1122,7 @@ The directory (`~/.forge/`) is created automatically on first write with `mkdir(
1086
1122
  ### When It's Written
1087
1123
 
1088
1124
  - After every successful `npx forgedock install` (end of the onboarding journey, right after the "Forged." celebration screen).
1089
- - After every `npx forgedock update` — both the git-clone branch (fast-forward pull) and the npm branch (version-check advisory) route through the same repair step (`relinkAndHint()`), so the receipt is refreshed either way.
1125
+ - After every successful `npx forgedock update` — the main git-clone branch and npm branch route through the same repair step (`relinkAndHint()`), so the receipt is refreshed either way. A non-main git-clone advisory intentionally does not relink or rewrite the receipt.
1090
1126
 
1091
1127
  The write is best-effort and never blocks or fails the install/update it records: any error (permission denied, disk full, unusual `FORGE_HOME` layout) degrades silently to a no-op, matching the fail-open contract already used by the command-manifest and hook-registration steps.
1092
1128
 
package/docs/OPENCODE.md CHANGED
@@ -19,12 +19,12 @@ For the optional command tier:
19
19
  npx forgedock opencode install --extras
20
20
  ```
21
21
 
22
- Restart OpenCode after install or update. OpenCode loads commands and plugins
23
- at startup.
22
+ Restart OpenCode after install or update. OpenCode loads commands, skills, and
23
+ plugins at startup.
24
24
 
25
- The installer does not edit `opencode.json` or `opencode.jsonc`. It writes only
26
- ForgeDock-owned files under OpenCode's config directory and records them in
27
- `forgedock/manifest.json` for safe updates and removal.
25
+ The native adapter does not add or modify user-owned OpenCode settings. It
26
+ writes only ForgeDock-owned files under OpenCode's config directory and records
27
+ them in `forgedock/manifest.json` for safe updates and removal.
28
28
 
29
29
  ## Usage
30
30
 
@@ -62,9 +62,29 @@ The generated plugin has no prompt text. It:
62
62
  - injects `FORGE_HOME` into OpenCode shell environments;
63
63
  - defaults `subagent_depth` to 2 when the user has not configured it, while
64
64
  preserving explicit lower limits;
65
+ - grants the built-in `general` subagent permission to invoke native `task`
66
+ unless the user explicitly configured a task permission;
67
+ - opts into OpenCode background subagents for the orchestrator's explicit
68
+ ready-issue dispatches so each completed issue can wake the parent independently; set
69
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` to opt out;
65
70
  - selects Git Bash on Windows only when the user has not explicitly configured
66
71
  an OpenCode shell, because the shared workflows and helper scripts use Bash.
67
72
 
73
+ The adapter also registers every eligible ForgeDock workflow as a native
74
+ OpenCode skill. Top-level and nested source paths are normalized to valid
75
+ hyphenated skill names while the wrapper continues to point at the authoritative
76
+ source file:
77
+
78
+ ```text
79
+ commands/work-on.md -> skills/work-on/SKILL.md
80
+ commands/work-on/investigate.md -> skills/work-on-investigate/SKILL.md
81
+ commands/review-pr.md -> skills/review-pr/SKILL.md
82
+ ```
83
+
84
+ The wrapper is intentionally thin. It carries the workflow's `name` and
85
+ `description` frontmatter, preserves the current-context arguments, and loads
86
+ only the referenced `commands/**/*.md` spec.
87
+
68
88
  OpenCode's provider configuration remains entirely user-owned. ForgeDock does
69
89
  not require Anthropic when invoked through OpenCode; any provider and model
70
90
  supported by the user's OpenCode configuration can execute the commands.
@@ -74,19 +94,54 @@ supported by the user's OpenCode configuration can execute the commands.
74
94
  The adapter follows these rules:
75
95
 
76
96
  - No global ForgeDock `instructions` entry.
77
- - No generated skill for every source file. Large skill catalogs add metadata
78
- to every session even when ForgeDock is unused.
79
- - Only top-level user entry commands are registered.
97
+ - Top-level user entry commands are registered under `/forge/*`.
98
+ - Every eligible workflow is registered as a native skill so nested
99
+ `Skill(...)` dispatch resolves through OpenCode's `skill` tool instead of
100
+ filesystem guessing.
80
101
  - Nested phase specs are loaded only when their dispatcher reaches them.
81
102
  - Task/Agent work uses OpenCode subagents only for the parallelism, isolation,
82
103
  or context-pressure cases required by the shared workflow.
104
+ - Orchestration dispatches independent issues with `task(background=true)` and
105
+ processes each injected task-result event immediately; it does not wait for a
106
+ wave or the slowest sibling.
107
+ - The `/orchestrate` entrypoint reads only runtime config, then runs
108
+ `bin/orchestrate-preflight.mjs` before loading the phase specs. The helper batches the initial GitHub
109
+ snapshot and emits only the compact ready queue needed for the first native task.
110
+ Interactive runs keep the confirmation gate; `--auto` or `--confirm` is the
111
+ explicit headless authorization.
112
+ - The full Phase 3/4 prose is reserved for investigations, unsupported or multi-repo
113
+ queries, explicit deep planning, and recovery after a task-result event. It remains
114
+ authoritative for cascade handling, leases, cleanup, and reporting.
83
115
  - GitHub state and the durable engine remain the recovery source instead of
84
116
  replaying prior prompt context.
85
117
 
86
118
  The generated adapter preamble is intentionally small. It maps Claude Code's
87
- in-conversation `Skill(...)` loading to a lazy read of the corresponding shared
88
- spec. It maps isolated `Task(...)` and permitted `Agent(...)` calls to
89
- OpenCode's native `task` tool.
119
+ in-conversation `Skill(...)` loading to the normalized native skill name and
120
+ authoritative shared spec. It maps isolated `Task(...)` and permitted
121
+ `Agent(...)` calls to OpenCode's native `task` tool. Before the shared review
122
+ specs evaluate Claude's literal `Task`/`Agent` names, an OpenCode runtime marker
123
+ selects `DISPATCH_TOOL=task`; the absence of those Claude names must not produce
124
+ a false `FORGE:REVIEW_BLOCKED` result.
125
+
126
+ Every isolated review dispatch uses a foreground native task. Every
127
+ native task call must include the schema-required `subagent_type` at the top
128
+ level alongside its `description` and `prompt`:
129
+
130
+ ```js
131
+ { description: "...", prompt: "...", subagent_type: "general", background: false }
132
+ ```
133
+
134
+ Implementation and review work uses `general`; read-only discovery uses
135
+ `explore`. Claude `general-purpose` maps to `general` and
136
+ `codebase-explorer` maps to `explore`. When a source task omits its type, the
137
+ generated adapter safely defaults it to `general`; unsupported types stop with
138
+ `FORGE_OPENCODE_CAPABILITY_ERROR` instead of reaching schema validation.
139
+ If the native `task` capability itself is unavailable, the workflow posts
140
+ `FORGE:REVIEW_BLOCKED` and stops rather than falling back to inline review or
141
+ another pipeline controller.
142
+ The generated plugin defaults omitted `background` to `false`. Only the
143
+ orchestrator's ready-issue dispatcher sets `background: true`; setting
144
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` forces foreground execution.
90
145
 
91
146
  `commands/work-on.md` is still a large entry dispatcher and is loaded in full,
92
147
  matching the Claude Code path. The adapter prevents additional eager loading,
@@ -102,13 +157,27 @@ npx forgedock opencode install --extras
102
157
  npx forgedock opencode uninstall
103
158
  ```
104
159
 
160
+ `npx forgedock update` also refreshes an existing managed OpenCode adapter,
161
+ including its plugin and generated skills. It preserves the installed core vs.
162
+ `--extras` tier, migrates the sentinel-marked legacy adapter, and does not
163
+ install OpenCode files when no ForgeDock adapter is already registered.
164
+
105
165
  Updates are deterministic and prune stale ForgeDock-owned command files.
106
166
  Uninstall removes only files listed in the ownership manifest and still marked
107
167
  with a ForgeDock sentinel. User-owned commands, plugins, and files placed in
108
168
  the `forgedock/` namespace are never removed; that namespace is pruned only
109
169
  when it is empty.
110
- Install also removes the previous ForgeDock-managed `~/.opencode-forge.md`
111
- instructions file and its exact legacy config entries when they are present.
170
+ For backward compatibility, install and uninstall may rewrite `opencode.json`
171
+ only to remove legacy entries whose two-key `description` and `template` still
172
+ exactly match one of the definitions emitted by the retired adapter and point to
173
+ the active ForgeDock home (or the home recorded in the ownership manifest during
174
+ uninstall), and the sentinel-marked legacy instructions file is present. It also
175
+ removes references to that managed `~/.opencode-forge.md` instructions file. The
176
+ migration does not rewrite `opencode.jsonc`. User-owned settings and customized commands are preserved;
177
+ definitions with extra keys, edited fields, different
178
+ paths, a missing ownership marker, or ambiguous ownership are left in place. If
179
+ a legacy config cannot be parsed or written, the migration leaves the legacy
180
+ artifacts in place.
112
181
 
113
182
  ## Locations
114
183
 
@@ -117,6 +186,7 @@ Default global location:
117
186
  ```text
118
187
  ~/.config/opencode/
119
188
  commands/forge/*.md
189
+ skills/<workflow-name>/SKILL.md
120
190
  plugins/forgedock.js
121
191
  forgedock/manifest.json
122
192
  ```
@@ -133,25 +203,43 @@ The separate `forgedock run` and `forgedock run-issue` backend still supports
133
203
  only Claude CLI and the Anthropic API. An OpenCode engine backend must be added
134
204
  and validated before ForgeDock can claim provider-neutral headless parity.
135
205
 
136
- Claude's `PreToolUse` safety hook has not yet been ported to an OpenCode plugin.
137
- The shared workflow rules and deterministic scripts still apply, but equivalent
138
- mechanical enforcement must be implemented and tested before claiming complete
139
- Claude-runtime safety parity.
140
-
141
- OpenCode 1.18.4 keeps background subagents experimental. When they are not
142
- enabled, the command adapter requires independent foreground tasks to be
143
- launched concurrently where supported and uses ForgeDock's GitHub-label polling
144
- fallback. This preserves isolated review contexts but does not yet prove equal
145
- wall-clock behavior for `/forge/orchestrate`.
206
+ The ForgeDock plugin provides the OpenCode-specific runtime boundary for shell
207
+ and task execution. Its `tool.execute.before` hook rejects Claude-backed and
208
+ recursive ForgeDock controller commands, and normalizes native task arguments
209
+ before execution; the shared workflow rules and deterministic scripts remain
210
+ the source of truth for all other behavior.
211
+
212
+ OpenCode 1.18.4 keeps background subagents experimental. ForgeDock opts into
213
+ that feature by default because the streaming DAG depends on the parent session
214
+ receiving one completion event per child. A native background task returns a
215
+ `<task id="..." state="running">` marker immediately and later injects a
216
+ `state="completed"` or `state="error"` result into the parent session. The
217
+ orchestrator maps that id to the issue, re-reads GitHub state, and dispatches
218
+ newly unlocked successors in the same response.
219
+
220
+ If `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is explicitly set,
221
+ ForgeDock uses independent foreground tasks where OpenCode can execute them
222
+ concurrently and labels the run as degraded. Foreground tasks cannot provide
223
+ the same per-completion wake behavior, so this opt-out intentionally restores a
224
+ wave-like fallback rather than silently claiming Claude-equivalent throughput.
146
225
 
147
226
  Commands that inspect Claude-specific transcripts or Claude installation state
148
227
  remain runtime-specific and should not be represented as portable until they
149
228
  receive dedicated implementations.
150
229
 
230
+ The preflight is intentionally bounded. It handles explicit issue sets and the
231
+ common single-repository `fast-lane`, `milestone`, `next`, `priority`, and
232
+ `no:milestone` queries, plus explicit dependencies and scoped issue-body file
233
+ overlap. Unsupported or complex inputs fall back to the shared phase specs rather
234
+ than silently weakening their safety rules.
235
+
151
236
  ## Orchestration Runtime
152
237
 
153
238
  OpenCode orchestration uses the native OpenCode `task` tool for isolated issue
154
- work and follows the shared `commands/work-on.md` state machine. It must not
239
+ work and follows the shared `commands/work-on.md` state machine. Each ready
240
+ issue is launched as `task(subagent_type="general", background=true)` and its
241
+ task-result event is treated as the completion notification for that issue. It
242
+ must not
155
243
  route through `forgedock run-issue` when that would select the Claude CLI or
156
244
  Anthropic API backend. If the host does not expose an OpenCode runtime marker,
157
245
  set the runtime explicitly before launching a headless command:
@@ -162,10 +250,25 @@ FORGE_RUNTIME=opencode opencode run --command forge/orchestrate "fast-lane"
162
250
 
163
251
  Each task re-reads GitHub labels and `FORGE:*` comments after every phase and
164
252
  continues until `workflow:merged`, `workflow:invalid`, `needs-human`, or
165
- `workflow:awaiting-merge`. If native task dispatch is unavailable, post a
253
+ `workflow:awaiting-merge`. The parent orchestrator processes every child
254
+ completion independently, so a completed predecessor can unlock and dispatch
255
+ its successors while unrelated issues continue running. If native task
256
+ dispatch is unavailable, post a
166
257
  `FORGE:OPENCODE_BLOCKED` diagnostic naming the missing capability and add
167
258
  `needs-human`; do not leave an issue stranded at `workflow:engine-error`.
168
259
 
260
+ If a generated command or skill cannot load its authoritative workflow, it must
261
+ stop with an actionable `FORGE_OPENCODE_CAPABILITY_ERROR`. It must not recover
262
+ by invoking `forgedock run-issue`, `npx forgedock run-issue`, or a recursive
263
+ `opencode run`; those paths select a competing controller or Claude-backed
264
+ backend.
265
+
266
+ The same boundary is enforced below the plugin for direct CLI callers. With
267
+ `FORGE_RUNTIME=opencode`, `forgedock run`, `forgedock run-issue`, and Claude
268
+ backend preflight fail before selecting a provider. The stable error is
269
+ `FORGE_OPENCODE_CAPABILITY_ERROR`; continue by using the registered native
270
+ Skill and Task workflow instead.
271
+
169
272
  This runtime branch is additive. Claude keeps its existing engine and
170
273
  background-agent paths, and Codex keeps its installed namespaced skills and
171
274
  repo-local adapters.
@@ -122,6 +122,12 @@ Every finding must include:
122
122
 
123
123
  **All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot create GitHub issues, and findings die as unread PR comments. Every finding that doesn't become a GitHub issue is a finding that will never be addressed.
124
124
 
125
+ ### Persist Before Post
126
+
127
+ GitHub is a delivery channel, not the sole record of a review. Before the first `gh pr comment` attempt, write the finalized complete review body, including its `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker and structured findings block, to a uniquely named durable file such as `${TMPDIR:-/tmp}/forge-review-${PR_NUMBER}-${DOMAIN}-$$.md`. Use `gh pr comment --body-file "$REVIEW_BODY_PATH"`; do not construct a retry loop around a failed write.
128
+
129
+ Every agent MUST return its verdict, finding count, and one line per finding to the orchestrator even when posting succeeds. If the post fails, return the durable body path and the same finding summary, then stop. A 403 or other write failure is a failed delivery, not a clean review and not a reason to retry in the background.
130
+
125
131
  ### Format
126
132
 
127
133
  Append this block at the very end of your comment (after the `---` footer line, still inside the EOF heredoc). It uses HTML comments so it's invisible in rendered markdown:
@@ -140,6 +146,7 @@ Append this block at the very end of your comment (after the `---` footer line,
140
146
  6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
141
147
  7. **Empty block**: If no findings at all, include just the START/END markers
142
148
  8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
149
+ 9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the lowercase dispatched domain.
143
150
 
144
151
  ### Domain Prefixes
145
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -47,6 +47,7 @@ function parseArmBody(body) {
47
47
  };
48
48
  return {
49
49
  maxGeneration: get("PRESET_MAX_GEN"),
50
+ batchMaxGeneration: get("PRESET_BATCH_MAX_GEN"),
50
51
  tokenBudget: get("PRESET_TOKEN_BUDGET"),
51
52
  deferOnBatchGated: get("PRESET_DEFER_GATED"),
52
53
  keywordHeuristic: get("PRESET_KEYWORD"),
@@ -120,7 +120,7 @@ class_a_scan() {
120
120
  # accumulated violation silently. (forge#2289 added the equivalent
121
121
  # end-of-file flush for Class B only; this closes the same gap in Class A.)
122
122
  flush_a_block() {
123
- if [ "$HAS_GIST" -eq 1 ] && echo "$BLOCK_LINES" | grep -qE '^[[:space:]]*--public([[:space:]]|$)'; then
123
+ if [ "$HAS_GIST" -eq 1 ] && [[ "$BLOCK_LINES" =~ (^|$'\n')[[:space:]]*--public([[:space:]]|$) ]]; then
124
124
  # Find the line number of --public within the block
125
125
  PUBLIC_LN=$(echo "$BLOCK_LINES" | grep -n '^[[:space:]]*--public' | head -1 | cut -d: -f1)
126
126
  ACTUAL_LINE=$((BLOCK_START + PUBLIC_LN))
@@ -149,9 +149,9 @@ class_a_scan() {
149
149
  # content; forge#2288 adds true depth so 2+ nesting levels close correctly.)
150
150
  IS_FENCE=0
151
151
  IS_BARE=0
152
- if echo "$line" | grep -qE "^[[:space:]]*${FENCE}"; then
152
+ if [[ "$line" =~ ^[[:space:]]*${FENCE} ]]; then
153
153
  IS_FENCE=1
154
- if echo "$line" | grep -qE "^[[:space:]]*${FENCE}[[:space:]]*\$"; then
154
+ if [[ "$line" =~ ^[[:space:]]*${FENCE}[[:space:]]*$ ]]; then
155
155
  IS_BARE=1
156
156
  fi
157
157
  fi
@@ -188,7 +188,7 @@ class_a_scan() {
188
188
  BLOCK_LINES="${BLOCK_LINES}${line}
189
189
  "
190
190
  # Check if this line has gh gist create/edit (even with line continuation \)
191
- if echo "$line" | grep -qE 'gh[[:space:]]+gist[[:space:]]+(create|edit)'; then
191
+ if [[ "$line" =~ gh[[:space:]]+gist[[:space:]]+(create|edit) ]]; then
192
192
  HAS_GIST=1
193
193
  fi
194
194
  done < "$file"
@@ -296,9 +296,9 @@ else
296
296
  # 2+ nesting levels close correctly.)
297
297
  IS_FENCE=0
298
298
  IS_BARE=0
299
- if echo "$line" | grep -qE "^[[:space:]]*${FENCE}"; then
299
+ if [[ "$line" =~ ^[[:space:]]*${FENCE} ]]; then
300
300
  IS_FENCE=1
301
- if echo "$line" | grep -qE "^[[:space:]]*${FENCE}[[:space:]]*\$"; then
301
+ if [[ "$line" =~ ^[[:space:]]*${FENCE}[[:space:]]*$ ]]; then
302
302
  IS_BARE=1
303
303
  fi
304
304
  fi
@@ -314,8 +314,9 @@ else
314
314
  # (forge#2289) and is always already at rest here, since this branch
315
315
  # is only reached when IN_CB is 0 — i.e. any block that was open has
316
316
  # already been closed and flushed at its own fence-close below.
317
- if echo "$line" | grep -qE '^#{1,6}[[:space:]]+'; then
318
- heading=$(echo "$line" | sed 's/^#*[[:space:]]*//' | sed 's/[[:space:]]*$//')
317
+ if [[ "$line" =~ ^#{1,6}[[:space:]]+(.*)$ ]]; then
318
+ heading="${BASH_REMATCH[1]}"
319
+ heading="${heading%"${heading##*[![:space:]]}"}"
319
320
  SECTION="$heading"
320
321
  continue
321
322
  fi
@@ -338,18 +339,18 @@ else
338
339
  fi
339
340
 
340
341
  # Content line while inside (at any depth).
341
- if echo "$line" | grep -qF "$ALLOWLIST_TOKEN"; then continue; fi
342
+ if [[ "$line" == *"$ALLOWLIST_TOKEN"* ]]; then continue; fi
342
343
 
343
344
  # Check for guard — scoped to THIS code block only (forge#2289)
344
- echo "$line" | grep -qE "$GUARD_PATTERN" && BLOCK_HAS_GUARD=1
345
+ [[ "$line" =~ $GUARD_PATTERN ]] && BLOCK_HAS_GUARD=1
345
346
 
346
347
  # Check if this line is in the diff's added lines AND has a side-effect verb
347
- if [ "$BLOCK_HAS_ADDED_SE" -eq 0 ] && echo "$line" | grep -qE "$SIDE_EFFECT_PATTERN"; then
348
+ if [ "$BLOCK_HAS_ADDED_SE" -eq 0 ] && [[ "$line" =~ $SIDE_EFFECT_PATTERN ]]; then
348
349
  # Is this specific line in the added content?
349
- if echo "$ADDED_CONTENT" | grep -qF "${line:0:80}" 2>/dev/null; then
350
+ if [[ "$ADDED_CONTENT" == *"${line:0:80}"* ]]; then
350
351
  BLOCK_HAS_ADDED_SE=1
351
352
  BLOCK_SE_LINE=$LN
352
- BLOCK_SE_VERB=$(echo "$line" | grep -oE "$SIDE_EFFECT_PATTERN" | head -1 || echo "side-effect")
353
+ BLOCK_SE_VERB="${BASH_REMATCH[0]:-side-effect}"
353
354
  fi
354
355
  fi
355
356
  done < "$file"
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # check-shell-portability.sh — Verify shipped shell scripts have an explicit
6
+ # Bash interpreter and parse successfully before a quality gate executes them.
7
+
8
+ set -euo pipefail
9
+
10
+ SCRIPTS_DIR="${1:-./scripts}"
11
+
12
+ if [ ! -d "$SCRIPTS_DIR" ]; then
13
+ echo "ERROR: scripts directory not found: $SCRIPTS_DIR" >&2
14
+ exit 2
15
+ fi
16
+
17
+ violations=0
18
+ while IFS= read -r -d '' script; do
19
+ first_line=$(IFS= read -r line < "$script" || true; printf '%s' "$line")
20
+ if [ "$first_line" != "#!/usr/bin/env bash" ]; then
21
+ echo "HIGH | $script | shell scripts must declare #!/usr/bin/env bash" >&2
22
+ violations=$((violations + 1))
23
+ continue
24
+ fi
25
+ if ! bash -n "$script"; then
26
+ echo "HIGH | $script | shell syntax validation failed" >&2
27
+ violations=$((violations + 1))
28
+ fi
29
+ done < <(find "$SCRIPTS_DIR" -type f -name '*.sh' -print0 | sort -z)
30
+
31
+ if [ "$violations" -gt 0 ]; then
32
+ echo "check-shell-portability: $violations violation(s) found. See stderr for details." >&2
33
+ exit 1
34
+ fi
35
+
36
+ echo "OK: Shell scripts declare Bash and pass syntax validation"