forgedock 1.1.8 → 1.1.9

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 (84) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine-cli.mjs +137 -11
  4. package/bin/env-detect.mjs +249 -0
  5. package/bin/forge-utils.mjs +364 -4
  6. package/bin/forgedock.mjs +1064 -135
  7. package/bin/hooks/session-start.mjs +68 -3
  8. package/bin/init-detect.mjs +101 -12
  9. package/bin/journey.mjs +917 -16
  10. package/bin/recall.mjs +24 -7
  11. package/bin/registry.mjs +91 -3
  12. package/bin/runner.mjs +38 -2
  13. package/bin/tui.mjs +49 -3
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +8 -8
  16. package/commands/audit.md +1 -1
  17. package/commands/autopilot.md +1 -1
  18. package/commands/ci-audit.md +1 -1
  19. package/commands/cleanup.md +87 -52
  20. package/commands/compat-audit.md +1 -1
  21. package/commands/deploy-info.md +1 -1
  22. package/commands/deploy-pr.md +1 -1
  23. package/commands/diagnose.md +1 -1
  24. package/commands/explain.md +1 -1
  25. package/commands/fix-ci.md +1 -1
  26. package/commands/forgedock-init.md +1 -1
  27. package/commands/incident-response.md +3 -3
  28. package/commands/issue.md +1 -1
  29. package/commands/optimize.md +1 -1
  30. package/commands/orchestrate/config.md +13 -3
  31. package/commands/orchestrate/phase-1-resolve.md +1 -1
  32. package/commands/orchestrate/phase-2-triage.md +1 -1
  33. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  34. package/commands/orchestrate/phase-3-dependency.md +128 -20
  35. package/commands/orchestrate/phase-4-execution.md +598 -43
  36. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  37. package/commands/orchestrate/phase-6-report.md +51 -3
  38. package/commands/orchestrate/safety.md +2 -2
  39. package/commands/qa-sweep.md +1 -1
  40. package/commands/quality-gate.md +1 -1
  41. package/commands/recover-orphans.md +5 -5
  42. package/commands/replay.md +1 -1
  43. package/commands/review-pr-agents/api.md +2 -2
  44. package/commands/review-pr-agents/auth.md +2 -2
  45. package/commands/review-pr-agents/billing.md +2 -2
  46. package/commands/review-pr-agents/concurrency.md +2 -2
  47. package/commands/review-pr-agents/database.md +2 -2
  48. package/commands/review-pr-agents/frontend.md +2 -2
  49. package/commands/review-pr-agents/infra.md +2 -2
  50. package/commands/review-pr-agents/protocols.md +1 -1
  51. package/commands/review-pr-agents/scraper.md +2 -2
  52. package/commands/review-pr-agents/security.md +2 -2
  53. package/commands/review-pr-staging.md +41 -3
  54. package/commands/review-pr.md +71 -6
  55. package/commands/rollback.md +1 -1
  56. package/commands/scope.md +1 -1
  57. package/commands/security-audit.md +1 -1
  58. package/commands/test-gate.md +1 -1
  59. package/commands/validate.md +1 -1
  60. package/commands/work-on/build/architect.md +1 -1
  61. package/commands/work-on/build/context.md +9 -6
  62. package/commands/work-on/build/implement.md +1 -1
  63. package/commands/work-on/build/validate.md +1 -1
  64. package/commands/work-on/build.md +31 -6
  65. package/commands/work-on/close.md +3 -2
  66. package/commands/work-on/decompose.md +1 -1
  67. package/commands/work-on/investigate.md +14 -5
  68. package/commands/work-on/remediate.md +331 -0
  69. package/commands/work-on/review.md +1 -1
  70. package/commands/work-on-monolithic.md +1 -1
  71. package/commands/work-on.md +194 -56
  72. package/docs/CI.md +2 -2
  73. package/docs/CODEX.md +1 -1
  74. package/docs/CONFIG.md +186 -0
  75. package/docs/site/command-reference.md +31 -1
  76. package/docs/site/getting-started.md +9 -34
  77. package/docs/site/index.md +1 -1
  78. package/docs/spec/label-state-machine.md +1 -1
  79. package/package.json +2 -1
  80. package/packages/protocol/src/types.js +1 -1
  81. package/scripts/check-command-docs-drift.sh +4 -2
  82. package/scripts/danger-zones.mjs +19 -12
  83. package/scripts/transition-label.sh +101 -13
  84. package/scripts/refresh-bot-token.sh +0 -97
package/docs/CONFIG.md CHANGED
@@ -42,6 +42,7 @@ The default (no flags) is the annotated review screen: detection runs, AI enrich
42
42
  | [`project`](#project-required) | **Yes** | GitHub identity (owner, repo, name) |
43
43
  | [`paths`](#paths-required) | **Yes** | Local filesystem locations |
44
44
  | [`branches`](#branches-required) | **Yes** | Branch naming conventions |
45
+ | [`agents`](#agents-optional) | No | Default model for pipeline agents |
45
46
  | [`repos`](#repos-optional) | No | Multi-repository routing |
46
47
  | [`project_board`](#project_board-optional) | No | GitHub Projects v2 integration |
47
48
  | [`services`](#services-optional) | No | External service URLs and IDs |
@@ -125,6 +126,40 @@ branches:
125
126
 
126
127
  ---
127
128
 
129
+ ## `agents` (OPTIONAL)
130
+
131
+ Model configuration for pipeline agents. `default_model` governs the main agent — the one you invoke directly (`work-on`, `orchestrate`, `review-pr`, etc.), referenced by every command spec's "Agent model policy" line. `subagent_model` governs the **child** sub-agents that a top-level command spawns internally — e.g. `/orchestrate`'s per-issue `/work-on` agents, `/review-pr`'s domain persona reviewers, `/analytics`'s parallel data-collection agents, `/incident-response`'s validation/root-cause agents.
132
+
133
+ ```yaml
134
+ agents:
135
+ default_model: "sonnet"
136
+ subagent_model: "sonnet"
137
+ ```
138
+
139
+ | Field | Type | Required | Default | Description |
140
+ |-------|------|----------|---------|-------------|
141
+ | `default_model` | string | No | `"sonnet"` | Short alias — `"sonnet"`, `"opus"`, or `"haiku"` — the same values the Agent/Task tool's `model` parameter accepts. This is the only form that works for interactive command-spec sub-agent spawning. |
142
+ | `subagent_model` | string | No | `default_model`, else `"sonnet"` | Short alias, same accepted values as `default_model`. Overrides the model used for child agents spawned internally by a top-level command (see above). Omit to inherit `default_model`. |
143
+
144
+ **Resolution order** (highest precedence first):
145
+ 1. `--model <id>` — CLI flag, `npx forgedock run` only (headless; main agent only).
146
+ 2. `FORGEDOCK_MODEL` env var — headless runner only (main agent only).
147
+ 3. `agents.default_model` — this field. Governs the main agent.
148
+ 4. Hardcoded default — `"sonnet"` for interactive agents, `"claude-sonnet-5"` for the headless runner (`bin/runner.mjs`'s `DEFAULT_MODEL`).
149
+
150
+ **Sub-agent resolution order** (highest precedence first):
151
+ 1. `agents.subagent_model` — this field. Governs child agents spawned by a top-level command.
152
+ 2. `agents.default_model` — falls back here when `subagent_model` is unset.
153
+ 3. Hardcoded default — `"sonnet"`.
154
+
155
+ **Headless runner note**: `bin/runner.mjs` (`npx forgedock run`, used for CI/headless invocations) calls the Anthropic SDK directly and also accepts a full Anthropic model ID here as a pass-through escape hatch (e.g. `"claude-opus-4-6"`). A full model ID does **not** work for interactive Agent/Task tool calls in command specs — those only accept the short-alias enum (`sonnet`/`opus`/`haiku`/`fable`). Prefer the short alias unless you specifically need a headless-only override. `subagent_model` is interactive-only — it has no headless-runner equivalent, since the headless runner does not spawn child sub-agents.
156
+
157
+ Projects without this section see no behavior change — everything defaults exactly as it did before these fields existed.
158
+
159
+ **Commands that use this section**: all commands with an "Agent model policy" line (`default_model`, nearly every command spec) and all commands that spawn internal sub-agents — `orchestrate`, `review-pr`, `analytics`, `incident-response` (`subagent_model`).
160
+
161
+ ---
162
+
128
163
  ## `repos` (OPTIONAL)
129
164
 
130
165
  Multi-repository routing. Use this when your project spans multiple GitHub repositories and you want to route issues to satellite repos by prefix.
@@ -571,6 +606,26 @@ Colors are grouped by semantic meaning:
571
606
 
572
607
  ---
573
608
 
609
+ ## GitHub App Install
610
+
611
+ During `npx forgedock`, the installer prompts you to install the ForgeDock GitHub App (`rapiercraft-forgedock`) on your account or org.
612
+
613
+ **What installing it does today**: registers the app against the account/org you choose. That's it.
614
+
615
+ **What it does NOT do (yet)**:
616
+ - It does **not** create a bot token. GitHub App *installation* tokens can only be minted with the app's private key, and that key is held solely by RapierCraft Studios (the app's owner) — your installation grants *your* installation ID against *RapierCraft's* app, not a key you can use yourself.
617
+ - It does **not** auto-refresh anything, and there is no background process managing token expiry.
618
+ - It does **not** change which `gh` auth context pipeline commands (`/work-on`, `/orchestrate`, etc.) use. Every pipeline `gh` call always uses whatever auth is currently active in your shell — your personal token unless you've manually configured something else.
619
+ - It does **not** wire up webhook-driven "automatic pipeline triggers" — no webhook receiver exists yet.
620
+
621
+ **Why this matters**: GitHub App installation tokens get materially higher API rate limits than a personal access token, and actions taken with them are attributed to the bot (`rapiercraft-forgedock[bot]`) rather than your personal account. Those benefits are real, but delivering them to arbitrary installers requires a hosted token-minting backend service that doesn't exist yet (tracked as forge#1890). Installing the app today is safe and free, but on its own changes nothing about how the pipeline authenticates.
622
+
623
+ `npx forgedock doctor` reports this status explicitly (Check 12: "GitHub App / bot token") so you aren't left guessing whether a bot token is active.
624
+
625
+ **If you want bot-token behavior today**: you'd need to register your own GitHub App (with your own private key), and drive your own token-refresh loop (mint an installation token via the app's `/app/installations/{id}/access_tokens` API, then `gh auth login --with-token`, repeating before the ~1-hour expiry). This is an advanced, self-hosted setup — ForgeDock does not package a generic version of this for you.
626
+
627
+ ---
628
+
574
629
  ## `marketing` (OPTIONAL)
575
630
 
576
631
  Controls opt-in growth features such as 'Powered by ForgeDock' footers on PR descriptions created by the pipeline.
@@ -730,6 +785,53 @@ See [Per-Directory State Registry](#per-directory-state-registry) below for how
730
785
 
731
786
  ---
732
787
 
788
+ ## Persisted Toolset Home (`~/.forge/`)
789
+
790
+ `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.
791
+
792
+ As of this feature, every `npx forgedock` (or `npx forgedock update`) run copies ForgeDock's own installable payload into a stable, version-independent home:
793
+
794
+ ```
795
+ ~/.forge/
796
+ bin/ — the same bin/ tree bundled with the resolved package (hooks, CLI entry points)
797
+ commands/ — the slash-command specs (.md)
798
+ scripts/ — the universal pipeline-agent scripts (classify-lane.sh, etc.)
799
+ templates/ — scaffold templates (e.g. the demo repo)
800
+ version — plain-text file containing the source package's version (e.g. "1.7.2")
801
+ ```
802
+
803
+ `~/.claude/commands/` symlinks and the SessionStart hook's baked-in script path are then built from **this** copy, not the original ephemeral source — so they keep working even after the npm/npx cache that originally served them is cleared.
804
+
805
+ ### When It Runs
806
+
807
+ | Command | Behavior |
808
+ |---------|----------|
809
+ | `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 |
810
+ | `npx forgedock init` | Does not touch `~/.forge/` — `init` only writes `forge.yaml`, it never installs commands |
811
+ | `npx forgedock update` (npm/npx install) | Refreshes `~/.forge/` from whatever the currently-resolved package looks like, then relinks |
812
+ | `npx forgedock update` (git-clone install) | **Does not** touch `~/.forge/` at all — see the exemption below |
813
+ | `npx forgedock doctor` | Reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}` exist |
814
+
815
+ 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.
816
+
817
+ ### The Git-Clone Exemption
818
+
819
+ If you run ForgeDock from a git clone (or a git worktree) of the repo itself — the development/self-hosted install mode — nothing is copied into `~/.forge/`. A git clone is already a stable, user-owned location; copying it into `~/.forge/` and linking commands from the copy instead of the clone would silently disconnect `git pull` (or `npx forgedock update`'s git branch) from what's actually linked into `~/.claude/commands/`. So:
820
+
821
+ - `~/.claude/commands/` continues to symlink directly from the clone, exactly as before this feature.
822
+ - `~/.forge/{bin,commands,scripts,templates}` correctly does **not** exist for this install mode — `npx forgedock doctor` treats that absence as healthy, not a warning.
823
+
824
+ ### Not the Same as `~/.forge/{runs,index}`
825
+
826
+ `~/.forge/` also hosts two **unrelated, pre-existing** directories used by other parts of ForgeDock:
827
+
828
+ - `~/.forge/runs/` — durable engine run state for `npx forgedock run-issue` / `resume-stalled` (see `bin/engine.mjs`).
829
+ - `~/.forge/index/` — the `recall` knowledge index (see `bin/recall.mjs`, `docs/FORGE-PROTOCOL.md`).
830
+
831
+ These are **engine data**, not the toolset itself, and this feature does not read, write, or otherwise change them. `~/.forge/{bin,commands,scripts,templates,version}` (this section) and `~/.forge/{runs,index}` (engine data) are unrelated siblings that merely happen to live under the same `~/.forge/` root — don't conflate the two when reading `~/.forge/`'s contents.
832
+
833
+ ---
834
+
733
835
  ## Per-Directory State Registry
734
836
 
735
837
  ForgeDock tracks per-directory opt-out state in a central registry file on the local machine.
@@ -810,6 +912,90 @@ npx forgedock status [dir] # Show a directory's resolved state (default: cwd)
810
912
 
811
913
  ---
812
914
 
915
+ ## Install Receipt
916
+
917
+ ForgeDock writes a machine-readable receipt of what the last install or update actually did, so debugging drift (wrong commands installed, wrong tier, stale hooks) can start from a recorded fact instead of re-deriving state from scratch.
918
+
919
+ ### Receipt File Location
920
+
921
+ ```
922
+ ~/.forge/install-receipt.json
923
+ ```
924
+
925
+ The directory (`~/.forge/`) is created automatically on first write with `mkdir(..., { recursive: true })`. The file is never committed to version control — it is per-user, per-machine state.
926
+
927
+ ### When It's Written
928
+
929
+ - After every successful `npx forgedock install` (end of the onboarding journey, right after the "Forged." celebration screen).
930
+ - 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.
931
+
932
+ 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.
933
+
934
+ ### Receipt Schema
935
+
936
+ ```json
937
+ {
938
+ "schemaVersion": 1,
939
+ "timestamp": "2026-07-10T12:00:00.000Z",
940
+ "forgedockVersion": "1.1.6",
941
+ "installMode": "npm",
942
+ "forgeHome": "/home/user/.npm/_npx/abc123/node_modules/forgedock",
943
+ "cwd": "/home/user/projects/my-repo",
944
+ "platform": {
945
+ "platform": "linux",
946
+ "platformLabel": "Linux",
947
+ "isWSL": false,
948
+ "wslDistro": null,
949
+ "shell": "bash"
950
+ },
951
+ "tier": "core",
952
+ "commands": {
953
+ "count": 42,
954
+ "list": ["work-on", "review-pr", "quality-gate", "orchestrate/phase-1-resolve"]
955
+ },
956
+ "hooks": {
957
+ "sessionStart": "registered",
958
+ "preToolUse": "registered",
959
+ "subagentStopEnforce": null
960
+ },
961
+ "forgeYaml": {
962
+ "present": true,
963
+ "validShape": true
964
+ }
965
+ }
966
+ ```
967
+
968
+ | Field | Type | Description |
969
+ |-------|------|--------------|
970
+ | `schemaVersion` | number | Schema version — currently `1` |
971
+ | `timestamp` | string | ISO-8601 timestamp of this install/update |
972
+ | `forgedockVersion` | string | `version` field from `{forgeHome}/package.json` (empty string if unreadable) |
973
+ | `installMode` | `"npm"` \| `"git-clone"` | Detected from whether `{forgeHome}/.git` exists — same predicate `update()` already uses to choose its update strategy |
974
+ | `forgeHome` | string | Absolute path to the ForgeDock package install (`FORGE_HOME`) |
975
+ | `cwd` | string | Absolute path to the project directory the command was run in |
976
+ | `platform` | object | `detectEnvironment()`'s output — platform, human label, WSL status/distro, detected shell |
977
+ | `tier` | `"core"` \| `"extras"` | Whether `--extras` was passed (opt-in command tier, see `install: extras` frontmatter) |
978
+ | `commands` | object | `count` and `list` (relative slash-command names) of everything installed for the resolved tier, sourced from `findMarkdownFiles()` — never a separately maintained list |
979
+ | `hooks.sessionStart` | string \| null | SessionStart hook registration status (`"registered"`, `"already"`, `"skipped-malformed"`) |
980
+ | `hooks.preToolUse` | string \| null | PreToolUse enforcement hook status, or `null` when the installed Claude Code version doesn't support it |
981
+ | `hooks.subagentStopEnforce` | string \| null | Cleanup status of the (retired) SubagentStop enforce hook |
982
+ | `forgeYaml.present` | boolean | Whether `forge.yaml` exists in `cwd` at receipt-write time |
983
+ | `forgeYaml.validShape` | boolean | Whether `forge.yaml` contains the 3 required top-level sections (`project:`, `paths:`, `branches:`) — a lightweight regex check, not a full YAML parse |
984
+
985
+ ### No PII or Secrets
986
+
987
+ The receipt never contains `process.env` values, GitHub tokens, API keys, or the contents of `forge.yaml` — only the `present`/`validShape` booleans above. Absolute filesystem paths (`forgeHome`, `cwd`) are included for drift debugging; they typically contain the OS username but nothing more sensitive, consistent with other per-machine ForgeDock state (e.g. `registry.json`'s path-keyed entries, see above).
988
+
989
+ ### Reading the Receipt
990
+
991
+ ```bash
992
+ cat ~/.forge/install-receipt.json
993
+ ```
994
+
995
+ There is no CLI subcommand to read it yet — `doctor()` reading the receipt to detect drift is a deliberately deferred stretch goal, not part of this feature.
996
+
997
+ ---
998
+
813
999
  ## Complete Example
814
1000
 
815
1001
  See [`forge.yaml.example`](../forge.yaml.example) at the repository root for a fully annotated example covering all sections.
@@ -592,7 +592,37 @@ These are invoked automatically by `/work-on` but can also be run standalone.
592
592
  | `/work-on/review` | Phase 5 | Review only |
593
593
  | `/work-on/close` | Phase 6 | Close & cleanup only |
594
594
  | `/work-on/decompose` | Phase 2 | Decompose into sub-issues |
595
- | `/review-pr-agents` | PR review | Agent catalog read by `/review-pr` during dispatch (not user-invokable directly) |
595
+ | `/work-on/remediate` | Phase 0A.1 | Remediate a `needs-human` PR: fix findings, re-review, re-gate (`/work-on <pr> --remediate`) |
596
+ | `/review-pr-agents` | PR review | Agent catalog router read by `/review-pr` during dispatch (not user-invokable directly) |
597
+
598
+ `/review-pr` (core) reads the following per-persona and shared-protocol templates from `commands/review-pr-agents/` at runtime during Phase 3C dispatch. They ship as part of every install alongside `/review-pr` — not user-invokable directly.
599
+
600
+ | Command | Phase | Purpose |
601
+ |---------|-------|---------|
602
+ | `/review-pr-agents/protocols` | Phase 3C | Shared Evidence-Based/Structured-Findings/Input-Scoping protocols read before every persona template |
603
+ | `/review-pr-agents/security` | Phase 3C | General security & quality scan (always runs) |
604
+ | `/review-pr-agents/auth` | Phase 3C | Auth conventions auditor (AUTH domain) |
605
+ | `/review-pr-agents/billing` | Phase 3C | Billing/payments auditor (BILLING domain) |
606
+ | `/review-pr-agents/concurrency` | Phase 3C | Concurrency/locking auditor (CONCURRENCY domain) |
607
+ | `/review-pr-agents/database` | Phase 3C | Database/migration auditor (DATABASE domain) |
608
+ | `/review-pr-agents/frontend` | Phase 3C | Frontend conventions auditor (FRONTEND domain) |
609
+ | `/review-pr-agents/infra` | Phase 3C | Infra/deploy auditor (INFRA domain) |
610
+ | `/review-pr-agents/api` | Phase 3C | API/SDK contract auditor (API domain) |
611
+ | `/review-pr-agents/scraper` | Phase 3C | Scraping/anti-bot auditor (SCRAPING domain) |
612
+
613
+ `/orchestrate` (core) reads the following phase files from `commands/orchestrate/` at runtime. They ship as part of every install alongside `/orchestrate` — not user-invokable directly.
614
+
615
+ | Command | Phase | Purpose |
616
+ |---------|-------|---------|
617
+ | `/orchestrate/config` | Phase 0 | Hard rules, config resolution, multi-repo support — read first |
618
+ | `/orchestrate/phase-1-resolve` | Phase 1 | Resolve the issue set from input |
619
+ | `/orchestrate/phase-2-triage` | Phase 2 | Investigation-first triage, Wave 0 |
620
+ | `/orchestrate/phase-2.5-synthesis` | Phase 2.5 | Investigation synthesis and deconfliction |
621
+ | `/orchestrate/phase-3-dependency` | Phase 3 | Dependency analysis, DAG construction, execution plan |
622
+ | `/orchestrate/phase-4-execution` | Phase 4 | Streaming DAG execution, agent dispatch, stall detection |
623
+ | `/orchestrate/phase-5-cleanup` | Phase 5 | Post-batch cleanup sweep and agent audit |
624
+ | `/orchestrate/phase-6-report` | Phase 6 | Consolidated report and pipeline summary |
625
+ | `/orchestrate/safety` | Reference | Safety rules and examples |
596
626
 
597
627
  ---
598
628
 
@@ -49,9 +49,9 @@ Run the installer with `npx` from your project directory:
49
49
  npx forgedock
50
50
  ```
51
51
 
52
- This installs all 25+ ForgeDock command specs into the current project (project-scoped by default) — making them available as slash commands in Claude Code sessions for this repo.
52
+ This installs all 25+ ForgeDock command specs into `~/.claude/commands/` — making them available as slash commands in every Claude Code session on this machine, not just this repo.
53
53
 
54
- > **Want commands available across all projects on this machine?** Use `npx forgedock --global` to install into `~/.claude/commands/` instead. Project-scoped is the default for new installs; use `--global` as an explicit opt-in.
54
+ > Install is always global. `--global` is still accepted on the command line for backward compatibility, but it's a no-op — `npx forgedock` and `npx forgedock --global` do exactly the same thing.
55
55
 
56
56
  **Verify the install:**
57
57
 
@@ -75,7 +75,7 @@ ForgeDock Doctor — Installation Health Check
75
75
 
76
76
  It exits `0` when everything passes and `1` if any check fails, so you can also use it in CI.
77
77
 
78
- > Prefer a quick spot-check? `ls .claude/commands/ | grep -E "work-on|review-pr|quality-gate"` should list `work-on.md`, `review-pr.md`, and `quality-gate.md`. (For a `--global` install, use `ls ~/.claude/commands/` instead.)
78
+ > Prefer a quick spot-check? `ls ~/.claude/commands/ | grep -E "work-on|review-pr|quality-gate"` should list `work-on.md`, `review-pr.md`, and `quality-gate.md`.
79
79
 
80
80
  ---
81
81
 
@@ -205,7 +205,7 @@ The quick fixes below cover the most common first-run issues. For the full list
205
205
 
206
206
  **`/work-on` not found in Claude Code**
207
207
 
208
- The symlink wasn't created. Re-run `npx forgedock` and check `ls .claude/commands/` (project-scoped, default) or `ls ~/.claude/commands/` (if you used `--global`).
208
+ The symlink wasn't created. Re-run `npx forgedock` and check `ls ~/.claude/commands/`.
209
209
 
210
210
  **`forge.yaml` not found**
211
211
 
@@ -221,37 +221,12 @@ The issue may have been marked `workflow:invalid`. Check the issue comments for
221
221
 
222
222
  ---
223
223
 
224
- ## Migrating from a global install
224
+ ## A note on install location
225
225
 
226
- If you installed ForgeDock before project-scoped became the default (prior to v1.0.22), your commands live in `~/.claude/commands/`. You have two options:
227
-
228
- **Option A: Keep the global install (no action required)**
229
-
230
- Add `--global` to all future `npx forgedock install/update/uninstall/doctor` calls. Your existing setup is unchanged.
231
-
232
- ```bash
233
- npx forgedock update --global # re-link after a version bump
234
- ```
235
-
236
- **Option B: Switch to project-scoped (recommended for per-repo version control)**
237
-
238
- 1. Remove the global install:
239
- ```bash
240
- npx forgedock uninstall --global
241
- ```
242
- 2. In each repo you want ForgeDock in, run a fresh project-scoped install:
243
- ```bash
244
- cd /path/to/your-repo
245
- npx forgedock install
246
- ```
247
- 3. Verify:
248
- ```bash
249
- npx forgedock doctor # auto-detects project-scoped install
250
- ```
251
-
252
- **Detect your current mode:**
226
+ ForgeDock briefly shipped a project-scoped-by-default install mode. It was backed out after causing a "split-brain" bug (`doctor`/`status` assumed project-scoped while the installer still wrote globally — [#1589](https://github.com/RapierCraftStudios/ForgeDock/issues/1589)), so install has always been global (`~/.claude/commands/`) since. If you have scripts or CI that still pass `--global`, there's nothing to change — it's accepted for backward compatibility and does exactly what a plain `npx forgedock` does.
253
227
 
254
228
  ```bash
255
- npx forgedock doctor # prints "Mode: project-scoped" or "Mode: global"
256
- npx forgedock status # shows install path
229
+ npx forgedock update # re-link after a version bump
230
+ npx forgedock doctor # confirm the install is healthy
231
+ npx forgedock status # shows the install path (~/.claude/commands/)
257
232
  ```
@@ -56,7 +56,7 @@ No more coordinating between agent sessions. No more lost context. No more start
56
56
  npx forgedock
57
57
  ```
58
58
 
59
- Installs commands into the current project (project-scoped by default). Add `--global` to install into `~/.claude/commands/` for all projects on this machine.
59
+ Installs commands into `~/.claude/commands/`, available in every Claude Code session on this machine. `--global` is still accepted as a flag for backward compatibility, but install is always global — there's nothing to opt into.
60
60
 
61
61
  **Requirements**: Claude Code, Node.js 18+, `gh` CLI authenticated with GitHub.
62
62
 
@@ -20,7 +20,7 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
20
20
  | `workflow:decomposed` | Issue decomposed into sub-issues | Decomposer agent |
21
21
  | `needs-human` | Pipeline blocked, human intervention required | Any agent on error |
22
22
 
23
- `workflow:awaiting-merge` vs `needs-human`: both are terminal (the pipeline stops advancing the issue automatically), but they mean different things. `needs-human` means the pipeline hit a condition it cannot resolve on its own (genuinely blocked — conflicting PR, failed verdict, calibration/trust escalation, etc.) and a human must diagnose and act. `workflow:awaiting-merge` means the opposite: the PR was previously escalated to `needs-human`, has since been remediated and re-reviewed to a clean `APPROVED` verdict with no mergeability blockers, but does not yet meet the automated auto-land bar (see forge#1809 Q1) — a human only needs to click merge, not diagnose a problem. `scripts/transition-label.sh` clears `needs-human` (best-effort) on every transition, so moving an issue onto `workflow:awaiting-merge` also clears any stale `needs-human` label.
23
+ `workflow:awaiting-merge` vs `needs-human`: both are terminal (the pipeline stops advancing the issue automatically), but they mean different things. `needs-human` means the pipeline hit a condition it cannot resolve on its own (genuinely blocked — conflicting PR, failed verdict, calibration/trust escalation, etc.) and a human must diagnose and act. `workflow:awaiting-merge` means the opposite: the PR was previously escalated to `needs-human`, has since been remediated and re-reviewed to a clean `APPROVED` verdict with no mergeability blockers, but does not yet meet the automated auto-land bar (see forge#1809 Q1) — a human only needs to click merge, not diagnose a problem. `scripts/transition-label.sh` only clears `needs-human` (best-effort) when the target state is `workflow:awaiting-merge` — every other forward transition leaves a pre-existing `needs-human` label untouched, preserving its sticky/terminal semantics.
24
24
 
25
25
  ## Transitions
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -40,6 +40,7 @@
40
40
  "examples/",
41
41
  "packages/protocol/src/",
42
42
  "scripts/",
43
+ "!scripts/refresh-bot-token.sh",
43
44
  "templates/",
44
45
  "LICENSE",
45
46
  "README.md"
@@ -228,7 +228,7 @@ export const RESERVED_TYPES = {
228
228
  },
229
229
 
230
230
  // §4.2 Machine-surface cross-artifact annotation (Base64url encoded — design decision 2026-07-08)
231
- // Format: <!-- FORGE:CARD v1 sha:{sha8hex} b64:{base64url_of_canonical_json} -->
231
+ // Format: <!-- FORGE:CARD: v1 sha:{sha8hex} b64:{base64url_of_canonical_json} -->
232
232
  // The inline value carries the full encoding: "v1 sha:XXXXXXXX b64:YYYYYY..."
233
233
  // This is an inline-value type — parse() extracts the full encoding as inlineValue.
234
234
  CARD: {
@@ -96,7 +96,9 @@ declare -a DOCUMENTED_COMMANDS=()
96
96
 
97
97
  while IFS= read -r line; do
98
98
  # Match H3 heading: ### `/command-name` or ### `/command/sub`
99
- if [[ "$line" =~ ^###[[:space:]]+\`(/[a-zA-Z0-9_/-]+)\` ]]; then
99
+ # Character class includes `.` to support dotted phase filenames (e.g.
100
+ # commands/orchestrate/phase-2.5-synthesis.md -> /orchestrate/phase-2.5-synthesis).
101
+ if [[ "$line" =~ ^###[[:space:]]+\`(/[a-zA-Z0-9_./-]+)\` ]]; then
100
102
  cmd="${BASH_REMATCH[1]}"
101
103
  DOCUMENTED_COMMANDS+=("$cmd")
102
104
  # Match table cell containing a bare command path: | `/command-name` | ...
@@ -105,7 +107,7 @@ while IFS= read -r line; do
105
107
  # matching inline code examples in prose or fix-instruction blocks.
106
108
  elif [[ "$line" =~ ^\| ]]; then
107
109
  # Extract all backtick-wrapped tokens that look like command paths
108
- while [[ "$line" =~ \`(/[a-zA-Z0-9_/-]+)\` ]]; do
110
+ while [[ "$line" =~ \`(/[a-zA-Z0-9_./-]+)\` ]]; do
109
111
  cmd="${BASH_REMATCH[1]}"
110
112
  DOCUMENTED_COMMANDS+=("$cmd")
111
113
  # Remove the matched portion so the loop can find multiple tokens per line
@@ -333,21 +333,28 @@ function buildFileFindingStats(cards) {
333
333
  function collectCommits(repoPath, sinceDate) {
334
334
  const since = sinceDate || `${WINDOW_DAYS} days ago`;
335
335
 
336
- let gitOutput;
337
- try {
338
- // --name-only: list files changed per commit
339
- // --pretty=format:'COMMIT %H %ai': commit hash + author date (ISO)
340
- // No pathspec = full repo scan for matrix building (this is intentional —
341
- // we need the global commit stream to compute n(a) and N correctly)
342
- gitOutput = execSync(
343
- `git -C "${repoPath}" log --name-only --since="${since}" --pretty=format:"COMMIT %H %ai"`,
344
- { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024, timeout: 60000 },
345
- );
346
- } catch (e) {
347
- log(`WARNING: git log failed — ${e.message}`);
336
+ // --name-only: list files changed per commit
337
+ // --pretty=format:COMMIT %H %ai: commit hash + author date (ISO)
338
+ // No pathspec = full repo scan for matrix building (this is intentional —
339
+ // we need the global commit stream to compute n(a) and N correctly)
340
+ // spawnSync with an argument array avoids shell interpolation of repoPath
341
+ // (operator/env-controlled input) — see #1842.
342
+ const result = spawnSync(
343
+ 'git',
344
+ ['-C', repoPath, 'log', '--name-only', `--since=${since}`, '--pretty=format:COMMIT %H %ai'],
345
+ { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024, timeout: 60000 },
346
+ );
347
+
348
+ if (result.error || result.status !== 0) {
349
+ const reason = result.error
350
+ ? result.error.message
351
+ : `exit code ${result.status}${result.signal ? ` (signal ${result.signal})` : ''}`;
352
+ log(`WARNING: git log failed — ${reason}`);
348
353
  return [];
349
354
  }
350
355
 
356
+ const gitOutput = result.stdout;
357
+
351
358
  const commits = [];
352
359
  let current = null;
353
360
 
@@ -123,6 +123,13 @@ fi
123
123
  # in between is GH_FLAG (e.g. -R RapierCraftStudios/forgedock).
124
124
  # ---------------------------------------------------------------------------
125
125
 
126
+ # Deferred exit code (forge#1929) — set to 1 by the persistent stale-label
127
+ # removal failure branch below instead of exiting immediately, so that the
128
+ # unconditional needs-human clear step (scoped to awaiting-merge) always
129
+ # gets a chance to run before the script actually exits. Initialized here,
130
+ # before any conditional logic, so `set -u` never sees it unbound.
131
+ TRANSITION_EXIT_CODE=0
132
+
126
133
  if [ "$#" -lt 2 ]; then
127
134
  echo "ERROR: Usage: transition-label.sh <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>" >&2
128
135
  echo " Example: transition-label.sh 674 -R RapierCraftStudios/forgedock investigating" >&2
@@ -292,29 +299,110 @@ done
292
299
  if [ -n "$TO_REMOVE" ]; then
293
300
  echo "Removing stale workflow:* labels present on the issue ($TO_REMOVE)..."
294
301
  gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "$TO_REMOVE" 2>/dev/null || true
302
+
303
+ # -------------------------------------------------------------------------
304
+ # Post-condition verification + single retry (forge#1915)
305
+ #
306
+ # The intersection fix above (forge#1810) already prevents the "unbootstrapped
307
+ # label 404" all-or-nothing failure — every candidate in $TO_REMOVE is known
308
+ # to exist on the issue at the time it was computed. But the removal call
309
+ # itself can still fail for reasons unrelated to label existence (a transient
310
+ # `gh`/GitHub API hiccup, rate limiting, a dropped connection), and that
311
+ # failure was previously swallowed unconditionally by `2>/dev/null || true`
312
+ # with no verification — leaving stale workflow:* labels stacked alongside
313
+ # the newly-added terminal label with no diagnostic trail. Live evidence:
314
+ # issue #1892 closed with both workflow:in-review and workflow:merged applied
315
+ # simultaneously because this exact removal call silently failed once.
316
+ #
317
+ # Fix: re-fetch the issue's labels after the removal call. If any label in
318
+ # $TO_REMOVE is still present, retry the removal once. If it's *still*
319
+ # present after the retry, this is a real, persistent failure — surface it
320
+ # loudly (stderr ERROR + non-zero exit) instead of the unconditional "OK".
321
+ #
322
+ # forge#1929: the non-zero exit is DEFERRED (via $TRANSITION_EXIT_CODE)
323
+ # rather than immediate. This block runs before the needs-human clear step
324
+ # further down, which is scoped to TARGET_STATE=awaiting-merge and must
325
+ # run regardless of this failure — it is unrelated to stale-label removal.
326
+ # An immediate `exit 1` here would silently skip that unrelated cleanup
327
+ # step in the one case (persistent removal failure) it's most important
328
+ # for the caller to see a consistent, fully-applied label state.
329
+ # -------------------------------------------------------------------------
330
+ POST_REMOVE_LABELS=$(gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json labels \
331
+ --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
332
+
333
+ STILL_PRESENT=""
334
+ IFS=',' read -ra TO_REMOVE_CHECK <<< "$TO_REMOVE"
335
+ for candidate in "${TO_REMOVE_CHECK[@]}"; do
336
+ case ",$POST_REMOVE_LABELS," in
337
+ *",$candidate,"*)
338
+ STILL_PRESENT="${STILL_PRESENT:+$STILL_PRESENT,}$candidate"
339
+ ;;
340
+ esac
341
+ done
342
+
343
+ if [ -n "$STILL_PRESENT" ]; then
344
+ echo "WARNING: label removal did not take effect for ($STILL_PRESENT) — retrying once..." >&2
345
+ gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "$STILL_PRESENT" 2>/dev/null || true
346
+
347
+ POST_RETRY_LABELS=$(gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json labels \
348
+ --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
349
+
350
+ STILL_PRESENT_AFTER_RETRY=""
351
+ IFS=',' read -ra RETRY_CHECK <<< "$STILL_PRESENT"
352
+ for candidate in "${RETRY_CHECK[@]}"; do
353
+ case ",$POST_RETRY_LABELS," in
354
+ *",$candidate,"*)
355
+ STILL_PRESENT_AFTER_RETRY="${STILL_PRESENT_AFTER_RETRY:+$STILL_PRESENT_AFTER_RETRY,}$candidate"
356
+ ;;
357
+ esac
358
+ done
359
+
360
+ if [ -n "$STILL_PRESENT_AFTER_RETRY" ]; then
361
+ echo "ERROR: failed to remove stale label(s) ($STILL_PRESENT_AFTER_RETRY) from issue #$ISSUE_NUMBER after retry — label state machine now inconsistent (issue carries both '$EFFECTIVE_LABEL' and the stale label(s) above)." >&2
362
+ TRANSITION_EXIT_CODE=1
363
+ else
364
+ echo "Retry succeeded — stale label(s) removed."
365
+ fi
366
+ fi
295
367
  else
296
368
  echo "No stale workflow:* labels present on the issue — nothing to remove."
297
369
  fi
298
370
 
299
371
  # ---------------------------------------------------------------------------
300
- # Clear needs-human (best-effort)
372
+ # Clear needs-human (best-effort) — SCOPED to workflow:awaiting-merge only
301
373
  #
302
374
  # needs-human is NOT a workflow:* label, so it is never included in
303
- # VALID_STATES/REMOVE_LABELS above. Historically no code path ever cleared it
304
- # (forge#1809/#1810) — it was a write-only, sticky, terminal label even after
305
- # the pipeline made forward progress past the condition that set it.
375
+ # VALID_STATES/REMOVE_LABELS above. It is a write-only, sticky, terminal
376
+ # label: once a human flags an issue/PR as needing attention, no routine
377
+ # forward-progress transition (investigating, ready-to-build, building,
378
+ # in-review, merged, invalid, decomposed) should silently clear it.
379
+ #
380
+ # The ONE deliberate exception is workflow:awaiting-merge (forge#1809/#1810):
381
+ # a remediated + re-reviewed PR that has moved off needs-human without yet
382
+ # meeting the auto-land bar. That is the single code path allowed to clear
383
+ # needs-human — every other TARGET_STATE must leave a pre-existing
384
+ # needs-human label untouched.
306
385
  #
307
- # This script only runs at explicit forward-progress transition points (the
308
- # dispatcher STOPs on needs-human before ever reaching a transition-label.sh
309
- # call in normal operation — see commands/work-on.md's terminal-state checks),
310
- # so clearing needs-human here is safe: it only fires when something has
311
- # deliberately decided to move the issue/PR to a new state, most notably
312
- # workflow:awaiting-merge (a remediated + re-reviewed PR moving off
313
- # needs-human without yet meeting the auto-land bar — see #1809 Q1).
314
386
  # Best-effort: `|| true` so a missing label never fails the script under
315
387
  # `set -euo pipefail`.
316
388
  # ---------------------------------------------------------------------------
317
- echo "Clearing needs-human (best-effort)..."
318
- gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "needs-human" 2>/dev/null || true
389
+ if [ "$TARGET_STATE" = "awaiting-merge" ]; then
390
+ echo "Clearing needs-human (best-effort, scoped to awaiting-merge)..."
391
+ gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "needs-human" 2>/dev/null || true
392
+ else
393
+ echo "Skipping needs-human clear — TARGET_STATE is '$TARGET_STATE', not 'awaiting-merge'."
394
+ fi
395
+
396
+ # ---------------------------------------------------------------------------
397
+ # Final exit (forge#1929) — honor any deferred failure from the persistent
398
+ # stale-label removal check above. The needs-human clear step just above
399
+ # always ran first, regardless of that failure, so the caller-visible exit
400
+ # code still surfaces the same "fail loud" signal introduced by forge#1915,
401
+ # just after the unrelated cleanup step has had a chance to run.
402
+ # ---------------------------------------------------------------------------
403
+ if [ "$TRANSITION_EXIT_CODE" -ne 0 ]; then
404
+ echo "FAILED: $EFFECTIVE_LABEL set on issue #$ISSUE_NUMBER, but persistent stale-label removal failure occurred (see ERROR above)." >&2
405
+ exit "$TRANSITION_EXIT_CODE"
406
+ fi
319
407
 
320
408
  echo "OK: $EFFECTIVE_LABEL set on issue #$ISSUE_NUMBER"