forgedock 1.27.0 → 1.28.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 (127) hide show
  1. package/README.md +4 -1
  2. package/bin/bootstrap.mjs +55 -0
  3. package/bin/engine/outcome-codes.mjs +10 -0
  4. package/bin/engine/telemetry-seam.mjs +37 -8
  5. package/bin/engine/telemetry-seam.test.mjs +31 -0
  6. package/bin/engine-cli.mjs +542 -38
  7. package/bin/engine.mjs +8 -4
  8. package/bin/forgedock.mjs +75 -9
  9. package/bin/gc.mjs +580 -0
  10. package/bin/lean/admission.mjs +33 -3
  11. package/bin/lean/batch.mjs +22 -4
  12. package/bin/lean/batch.test.mjs +5 -3
  13. package/bin/lean/checks.mjs +199 -0
  14. package/bin/lean/checks.test.mjs +183 -0
  15. package/bin/lean/conformance.mjs +190 -58
  16. package/bin/lean/conformance.test.mjs +116 -7
  17. package/bin/lean/credentials.mjs +29 -0
  18. package/bin/lean/depcache.mjs +312 -0
  19. package/bin/lean/depcache.test.mjs +298 -0
  20. package/bin/lean/drift.mjs +198 -0
  21. package/bin/lean/drift.test.mjs +223 -0
  22. package/bin/lean/effects.mjs +85 -8
  23. package/bin/lean/execution.e2e.test.mjs +81 -0
  24. package/bin/lean/execution.mjs +431 -0
  25. package/bin/lean/execution.test.mjs +583 -0
  26. package/bin/lean/heavy.mjs +289 -0
  27. package/bin/lean/heavy.test.mjs +207 -0
  28. package/bin/lean/lean.mjs +572 -139
  29. package/bin/lean/lean.test.mjs +1193 -31
  30. package/bin/lean/ledger.mjs +27 -2
  31. package/bin/lean/ledger.test.mjs +36 -1
  32. package/bin/lean/limits.mjs +58 -0
  33. package/bin/lean/limits.test.mjs +150 -0
  34. package/bin/lean/orchestrate.mjs +312 -19
  35. package/bin/lean/prompts.mjs +89 -15
  36. package/bin/lean/refactor.mjs +276 -0
  37. package/bin/lean/refactor.test.mjs +359 -0
  38. package/bin/lean/render.mjs +24 -5
  39. package/bin/lean/review-trigger.mjs +134 -0
  40. package/bin/lean/review-trigger.test.mjs +104 -0
  41. package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
  42. package/bin/lean/sandbox-remote.mjs +142 -0
  43. package/bin/lean/sched.mjs +221 -0
  44. package/bin/lean/sched.test.mjs +310 -0
  45. package/bin/lean/schedd.mjs +247 -0
  46. package/bin/lean/schedd.test.mjs +180 -0
  47. package/bin/lean/toolchain.mjs +228 -0
  48. package/bin/lean/toolchain.test.mjs +154 -0
  49. package/bin/lean/verify.mjs +20 -9
  50. package/bin/lean/verify.test.mjs +26 -3
  51. package/bin/lean/worktree.mjs +69 -0
  52. package/bin/lean/worktree.test.mjs +141 -0
  53. package/bin/runner.mjs +53 -10
  54. package/bin/stats/github-source.mjs +1 -1
  55. package/bin/stats/index.mjs +14 -2
  56. package/bin/stats/query.mjs +72 -0
  57. package/bin/stats/stats.test.mjs +54 -0
  58. package/commands/audit.md +2 -0
  59. package/commands/autopilot.md +2 -0
  60. package/commands/bootstrap.md +35 -0
  61. package/commands/cleanup.md +7 -22
  62. package/commands/forge-stats.md +2 -1
  63. package/commands/forgedock-init.md +7 -0
  64. package/commands/issue.md +89 -830
  65. package/commands/milestone.md +55 -6
  66. package/commands/orchestrate.md +10 -2
  67. package/commands/upgrade-deps.md +3 -3
  68. package/commands/work-on/build/validate.md +4 -0
  69. package/commands/work-on/close.md +2 -0
  70. package/commands/work-on/decompose.md +24 -6
  71. package/docs/CI.md +3 -3
  72. package/docs/CONFIG.md +177 -2
  73. package/docs/OUTCOME-CODES.md +8 -0
  74. package/docs/TELEMETRY.md +1 -1
  75. package/docs/site/command-reference.md +22 -0
  76. package/docs/site/getting-started.md +2 -0
  77. package/docs/site/start-a-new-project.md +327 -0
  78. package/package.json +1 -1
  79. package/scripts/affected-files-check.sh +91 -0
  80. package/scripts/affected-files-check.test.sh +80 -0
  81. package/scripts/bootstrap-create.mjs +393 -0
  82. package/scripts/bootstrap-validate.mjs +36 -2
  83. package/scripts/calibration.mjs +116 -0
  84. package/scripts/check-devdocs-freshness.sh +9 -4
  85. package/scripts/check-devdocs-freshness.test.sh +11 -1
  86. package/scripts/check-spec-markers.sh +3 -0
  87. package/scripts/check-spec-size.sh +29 -11
  88. package/scripts/check-spec-size.test.sh +27 -4
  89. package/scripts/ci-local/lib.mjs +8 -1
  90. package/scripts/ci-local/parallelism.test.mjs +27 -0
  91. package/scripts/ci-local/run.mjs +3 -3
  92. package/scripts/ci-local/scoping.mjs +82 -0
  93. package/scripts/ci-local.test.sh +1 -1
  94. package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
  95. package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
  96. package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
  97. package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
  98. package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
  99. package/scripts/fixtures/symbol-index/py/sample.py +41 -0
  100. package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
  101. package/scripts/fixtures/symbol-move/gen_big.py +201 -0
  102. package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
  103. package/scripts/forge-root.test.sh +1 -1
  104. package/scripts/hotspots.sh +100 -0
  105. package/scripts/hotspots.test.sh +101 -0
  106. package/scripts/issue-create.sh +197 -0
  107. package/scripts/issue-create.test.sh +166 -0
  108. package/scripts/maintenance-scan.sh +185 -0
  109. package/scripts/maintenance-scan.test.sh +89 -0
  110. package/scripts/milestone-drift.sh +206 -0
  111. package/scripts/milestone-drift.test.sh +120 -0
  112. package/scripts/refactor-verify.sh +368 -0
  113. package/scripts/refactor-verify.test.sh +202 -0
  114. package/scripts/structure-delta.sh +10 -5
  115. package/scripts/structure-delta.test.sh +3 -2
  116. package/scripts/symbol-index.mjs +253 -0
  117. package/scripts/symbol-index.py +354 -0
  118. package/scripts/symbol-index.test.sh +119 -0
  119. package/scripts/symbol-move-ts.test.sh +161 -0
  120. package/scripts/symbol-move.mjs +379 -0
  121. package/scripts/symbol-move.py +383 -0
  122. package/scripts/symbol-move.test.sh +168 -0
  123. package/templates/bootstrap/interview.md +34 -0
  124. package/templates/bootstrap/issue-body.md +42 -0
  125. package/templates/bootstrap/needs-human-issue.md +45 -0
  126. package/templates/bootstrap/stack-profiles.md +34 -0
  127. package/templates/bootstrap/technical-spike.md +75 -0
@@ -46,6 +46,9 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
46
46
  | `infra.cli_spawn_transient` | CLI spawn failed transiently before the child ran (ETIMEDOUT, EAGAIN, ENOMEM, EMFILE); retried with backoff on a separate bounded counter that does not consume a phase attempt | engine | `err.code` `CLI_SPAWN_TRANSIENT` |
47
47
  | `cli.timeout` | CLI child ran and was killed at the wall-clock limit; a normal attempt (consumes one of `maxAttempts`), usage recorded when the CLI emitted any | engine | `err.code` `CLI_TIMEOUT` |
48
48
  | `cli.spawn_failed` | The CLI could not be started for a reason that repeats on every rerun (`E2BIG` argv too large, `ENOENT`/`EACCES`/`ENOEXEC` binary missing or not executable, `ENAMETOOLONG`). Core work-on ends `needs-human` with this code instead of `engine-error`, so `forgedock orchestrate` does not spend its relaunch on it | lean, engine | `err.code` `CLI_SPAWN_FAILED` (E2BIG) or `err.spawnDeterministic` (other errnos keep their `err.code`); `err.spawnErrno` holds the errno |
49
+ | `env.setup_failed` | forge.yaml `env.setup` (run once per fresh lane worktree, before the builder) exited non-zero, timed out or could not start. An environment error, not a build failure: the builder never ran. Core work-on ends `needs-human` with this code instead of `engine-error`, so `forgedock orchestrate` does not relaunch it; fix the setup command or the environment and rerun | lean | `io.runSetup` exit code / spawn error |
50
+ | `sandbox.start_failed` | A `forgedock orchestrate` lane sandbox (forge.yaml `execution`, provider `container` or `remote`) could not start: bad image, missing endpoint or credential, refused launch. The lane never ran. Final `needs-human` row: not relaunched and not counted by the engine-error circuit breaker | orchestrate | `bin/lean/execution.mjs` `providerLanes`: `provider.start` threw without `retryable` |
51
+ | `sandbox.capacity` | The sandbox provider had no capacity for the lane (HTTP 429/503 from the remote transport). `deferred` with backoff (`Retry-After` when sent), bounded by `orchestration.max_deferrals` | orchestrate | `provider.start` threw with `err.retryable: true` |
49
52
  | `lease.held` | Another live agent holds the issue lease | engine | Foreign live lease on acquire |
50
53
  | `dependency.open` | A dependency is still open: a trusted `**Depends on**: #N` line refused the run before any state write, or the core builder reported `block.kind: "dependency"` naming an issue that is open. Carries `dependencies`; never `needs-human`. `forgedock orchestrate` adds the edge and requeues the issue behind it, without skipping dependents | engine, lean | Trusted `**Depends on**` line, or `result.json` `block.issues`, + the dependency issue's live open state |
51
54
  | `dependency.unreadable` | The issue body or a declared dependency could not be read, so the run was refused (fail closed) | engine, lean | Failed or unparsable `gh api` / `gh issue view` read |
@@ -72,9 +75,14 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
72
75
  | `conformance.unjustified_deferral` | Core work-on: the reviewer judged a deferred or post-deploy requirement unjustified (it belongs in this change), after one builder resume; nothing is filed or merged | lean | Review requirement row `deferral_ok: false` |
73
76
  | `conformance.preserve_violated` | Core work-on: a preserve constraint is violated or unverified, after one builder resume | lean | Review `preserve` row not `intact` |
74
77
  | `conformance.regression` | Core work-on: the reviewer named a concrete regression, after one builder resume | lean | Review `regressions` row |
78
+ | `docs.stale` | Core work-on: the target repo's own `scripts/check-devdocs-freshness.sh` (docs must move with `bin/lean/` / `scripts/` behaviour) still fails after one builder resume; caught before the PR instead of by its required check | lean | the repo's `scripts/check-devdocs-freshness.sh <base>` exit 1 |
75
79
  | `structure.undeclared_edge` | Core work-on: the change adds a package.json dependency between workspace packages that the frozen contract does not name, still present after one builder resume | lean | `unnamedEdges` over `scripts/structure-delta.sh` `package_edges_added` |
80
+ | `maintenance.hotspot_growth` | Core work-on: the change grows a file already over `maintenance.hotspot.file_lines` by more than `growth_lines` without a `hotspot-exempt: <reason>` in the contract, still present after one builder resume; blocks in shadow and enforce | lean | `hotspotGrowth` precheck over `scripts/hotspots.sh --base` `growth` |
76
81
  | `followup.unavailable` | Core work-on: filing or verifying the tracking issue for a deferral failed; the run is deferred with nothing merged, never needs-human | lean | `file-deferred-followups.sh` exit 3/4 or an unreadable tracking issue |
77
82
  | `merge.head_mismatch` | Core work-on: CI passed for a head other than the reviewed head; merge refused | lean | Gate head differs from the reviewed head |
83
+ | `split.unsupported` | Core work-on `refactor:split` lane (forge#4211): no codemod or index tool for the target's language (Python and TypeScript have movers), or libcst / ts-morph is not available; refused before any edit | lean | target extension, `scripts/symbol-index.py`/`symbol-index.mjs` `unsupported`, `scripts/symbol-move.py` exit 3 naming libcst, `scripts/symbol-move.mjs` exit 3 `no codemod tool for ts` (forge#4212), `scripts/refactor-verify.sh` exit 2 |
84
+ | `split.plan_invalid` | Core work-on `refactor:split` lane: no target or test command in the issue, an unparseable grouping reply, a failed baseline, or a plan `scripts/symbol-move.py` / `scripts/symbol-move.mjs` refuses; nothing written | lean | `bin/lean/refactor.mjs` parsers, `scripts/symbol-move.py` / `scripts/symbol-move.mjs` exit 3 |
85
+ | `split.verify_failed` | Core work-on `refactor:split` lane: the move did not prove location-only (logic diff, missing symbol, changed pass/skip counts or a new import cycle); the branch is reset and nothing is pushed | lean | `scripts/refactor-verify.sh verify` exit 1 `failures[].kind` |
78
86
  | `completed` | Non-failure terminal (merged, phase-complete, investigation) | engine | Terminal reason |
79
87
  | `awaiting_merge` | Clean result held for a merge decision | remediate | Terminal reason `awaiting-merge` |
80
88
  | `unclassified` | Fallback for an absent or unknown code | engine | The raw reason is kept alongside |
package/docs/TELEMETRY.md CHANGED
@@ -40,7 +40,7 @@ is dropped. Payload keys are `[A-Za-z0-9_]+` and cannot shadow the shared keys;
40
40
  | `ci_local` | `scripts/ci-local.sh run` (validate V4.8), one per run | `result` = `PASS` \| `FAIL` \| `DEGRADED`, `jobs_run`, `jobs_deferred`, `jobs_failed`, `duration_ms` (execution), `wait_ms` (time queued for a machine slot), `fix_iterations` (0 first run, 1 and 2 after fixes), `checks`, `cache_hits`, `narrowed` (checks run on a subset or skipped for no impact), `widened` (checks widened to the full command), `selected` / `total` (summed scope) |
41
41
  | `ci_local_check` | `scripts/ci-local.sh run`, one per scoped, widened or executed check (at most 20 per run) | `job`, `selector`, `selected`, `total`, `cache` = `hit` \| `miss` \| `dedup` \| `na`, `duration_ms`, `wait_ms` (job slot wait), `widen` (reason or `none`). A remote-CI failure at merge on a check recorded here as narrowed is a selector bug |
42
42
  | `liveness` | `scripts/remediate-liveness.sh` | `decision` = `PROCEED` \| `DEFER` \| `EXHAUSTED`, `why` |
43
- | `phase_attempt` | the engine (`bin/engine/telemetry-seam.mjs`, one per phase attempt; `phase` = the phase id) | `attempt`, `model`, `turns`, `duration_ms`, `cost_usd`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_creation_tokens` (any value the runner did not report is omitted) |
43
+ | `phase_attempt` | the engine (`bin/engine/telemetry-seam.mjs`, one per phase attempt; `phase` = the phase id) | `attempt`, `model`, `backend` (`cli`/`api`), `turns`, `duration_ms`, `cost_usd`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_creation_tokens` (any value the runner did not report is omitted) |
44
44
  | `run_terminal` | the engine (`terminate()`, one per run segment) | `reason` (terminal reason), `code` (see `docs/OUTCOME-CODES.md`), `wall_ms` (this engine process, start to terminal) |
45
45
 
46
46
  The engine records carry `run` = `r_<issue>_<start epoch seconds>` (one per `forgedock run-issue` process) so `forgedock stats`
@@ -553,6 +553,28 @@ Onboards an existing repo into the ForgeDock pipeline — scans open issues, cla
553
553
 
554
554
  ---
555
555
 
556
+ ### `/bootstrap`
557
+
558
+ **Idea-to-backlog setup for a new project.**
559
+
560
+ Guided setup for a brand-new project — interviews you about the idea, proposes defaults, surfaces doubts, writes a technical spike doc, and files a validated, orchestrate-ready issue backlog. The plan is validated before any GitHub write.
561
+
562
+ ```bash
563
+ /bootstrap "a habit tracker for teams" # Start from a free-text idea
564
+ /bootstrap docs/idea.md # Start from existing docs
565
+ /bootstrap "<idea>" --dry-run # Preview the backlog without GitHub writes
566
+ ```
567
+
568
+ Headless, from a terminal or CI:
569
+
570
+ ```bash
571
+ npx forgedock bootstrap "<idea>" [--dry-run] # Run /bootstrap without an interactive session
572
+ npx forgedock bootstrap --plan plan.json --dry-run # Preview milestones, titles, DAG and estimated agent cost; no GitHub access
573
+ npx forgedock bootstrap --plan plan.json # Validate and create; re-run the same command to resume from GitHub
574
+ ```
575
+
576
+ ---
577
+
556
578
  #### `/sync-ecosystem`
557
579
 
558
580
  **Cross-repo sync.**
@@ -10,6 +10,8 @@ ForgeDock turns GitHub issues into shipped code — automatically. You open an i
10
10
 
11
11
  This tutorial gets you from zero to your first autonomous pipeline run in under 5 minutes.
12
12
 
13
+ No repo or issues yet? [Start a New Project with ForgeDock](./start-a-new-project.md) turns an idea into a ready-to-build backlog with `/bootstrap`.
14
+
13
15
  ---
14
16
 
15
17
  ## Prerequisites
@@ -0,0 +1,327 @@
1
+ ---
2
+ title: "Start a New Project with ForgeDock"
3
+ description: "Go from a one-line idea to a validated, orchestrate-ready GitHub backlog with /bootstrap: interview, spike doc, dry-run preview with estimated cost, creation and the /orchestrate hand-off. Includes a worked example."
4
+ keywords: ["forgedock bootstrap", "new project backlog", "claude code new project", "ai project setup", "orchestrate"]
5
+ ---
6
+
7
+ # Start a New Project with ForgeDock
8
+
9
+ [Getting Started](./getting-started.md) assumes a repo that already has issues. This guide covers a brand-new project. You start with an idea, `/bootstrap` turns it into a backlog of GitHub issues in build order, and `/orchestrate` builds that backlog one milestone at a time.
10
+
11
+ ```text
12
+ idea ──▶ /bootstrap ──▶ spike doc + plan ──▶ dry-run preview ──▶ backlog on GitHub ──▶ /orchestrate per milestone
13
+ ```
14
+
15
+ All the GitHub writes happen in one deterministic script (`scripts/bootstrap-create.mjs`). The agent does the judgment work: the interview, the defaults and the plan. The script validates the plan and creates it. Flags are listed in the [command reference](./command-reference.md#bootstrap).
16
+
17
+ ---
18
+
19
+ ## Prerequisites
20
+
21
+ - A GitHub repo for the new project (it can be empty). Set it in `forge.yaml` (`project.owner` / `project.repo`, from `npx forgedock init`) or pass `--repo OWNER/NAME`.
22
+ - GitHub CLI (`gh`) authenticated (`gh auth login`).
23
+ - Node.js 18 or higher.
24
+ - Claude Code with ForgeDock installed for `/bootstrap`, or just `npx forgedock` for the headless path.
25
+
26
+ ## 1. From idea to `/bootstrap`
27
+
28
+ Start from a free-text idea, or from docs you already have:
29
+
30
+ ```bash
31
+ /bootstrap "a shared grocery list for households" # in a Claude Code session
32
+ /bootstrap docs/idea.md # start from existing docs
33
+ /bootstrap "<idea>" --dry-run # preview only, no GitHub writes
34
+ ```
35
+
36
+ Without an interactive session:
37
+
38
+ ```bash
39
+ npx forgedock bootstrap "a shared grocery list for households" --dry-run
40
+ ```
41
+
42
+ Headless mode can't ask you questions. It takes the recommended option for each one and lists it under "Here's what I assumed", so check that list before you create anything.
43
+
44
+ ## 2. What `/bootstrap` does
45
+
46
+ 1. **Interview.** It covers the aspects in `templates/bootstrap/interview.md` (users, core jobs, scope, platforms, stack, auth, data and tenancy, integrations, payments, compliance, deployment, budget and more) and skips anything your idea already answers.
47
+ 2. **Fills the blanks.** Every unanswered aspect gets a sensible default with a one-line rationale. All of them appear in one editable "Here's what I assumed" list.
48
+ 3. **Asks only genuine doubts.** These are choices that change architecture or cost, such as native vs PWA, data residency or self-hosted vs managed DB, plus the product name and repo owner, which are always asked. It asks at most 4 multiple-choice questions per batch, each with a recommended option.
49
+ 4. **Writes the spike doc** to `docs/engineering/technical-spike.md` (or the path in `--spike <path>`). It must have the sections Stack, Repo layout, Runtime topology, Data model, Security and privacy, Design system and quality gates, Route/API map, Credentials and Milestone plan. Each section needs a `**Rationale**:` line.
50
+ 5. **Writes a plan JSON** with milestones in build order (foundation first), a `SPIKE` parent issue, and sub-issues in the pipeline issue template. Each sub-issue lists planned files and acceptance criteria with `[type:*]` tags.
51
+ - Every credential, account or external input gets its own **needs-human** issue. Code issues may mention it, but they never depend on it. A mock keeps dev and CI green until the real thing exists.
52
+ 6. **Validates** the plan with `scripts/bootstrap-validate.mjs` and fixes it until it passes. Nothing is ever created from an invalid plan.
53
+
54
+ ## 3. Preview the backlog (dry run)
55
+
56
+ If you already have a plan file, you can preview it without an LLM call or any GitHub access:
57
+
58
+ ```bash
59
+ npx forgedock bootstrap --plan plan.json --dry-run
60
+ ```
61
+
62
+ The preview lists each milestone with its `milestone/<slug>` branch, the issues in build order and their dependencies, a DAG summary, and an **estimated agent cost**. The estimate is a flat rate per build issue: `$3` by default, or `costPerIssueUsd` in the plan, or `--cost-per-issue <usd>`. The spike and needs-human issues are excluded. It's a planning estimate, not a measured figure.
63
+
64
+ The spike doc has to exist on disk even for a dry run. If it's missing a section or a rationale, the script prints the error and writes nothing.
65
+
66
+ ## 4. Create the backlog
67
+
68
+ ```bash
69
+ npx forgedock bootstrap --plan plan.json
70
+ ```
71
+
72
+ This creates:
73
+
74
+ - the spike parent issue
75
+ - the milestones, plus a `milestone/<slug>` branch for each
76
+ - every sub-issue, with a `**Parent**` link to the spike, the plan's guardrails and domain rules added to its Context, and <code v-pre>#{{KEY}}</code> placeholders resolved to real issue numbers
77
+ - a board item for each issue (Status=Todo, Priority, Component)
78
+ - `FORGE:BOOTSTRAP_PLAN` and `FORGE:DECOMPOSED` comments on the spike, which is then labelled `workflow:decomposed`
79
+
80
+ Exit codes: `0` created (or previewed), `1` invalid plan or spike doc (nothing written), `2` usage error, `3` GitHub error.
81
+
82
+ **Resume by re-running.** GitHub is the state. Issues are reused by exact title plus their `**Parent**` link, milestones by title, branches if they already exist, and comments if their marker is already there. After a network or rate-limit failure (exit `3`), run the same command again.
83
+
84
+ ## 5. The hand-off
85
+
86
+ Creation ends with a hand-off in three parts:
87
+
88
+ - **Created**: links to the build issues.
89
+ - **Needs a human**: links to the credential and account issues. Do these when you can. They block nothing, because the code issues use mocks until then.
90
+ - **Next commands**: one `/orchestrate milestone "<title>"` per milestone, in build order.
91
+
92
+ Run the orchestrate commands in that order. Each one builds its milestone's issues through the normal `/work-on` pipeline.
93
+
94
+ ---
95
+
96
+ ## Worked example: Shelf, a shared grocery list
97
+
98
+ The idea: *"Shelf, a shared grocery list for households. Members invite each other by email and see items checked off live."*
99
+
100
+ ### Assumptions and doubts
101
+
102
+ Headless, `/bootstrap` takes the recommended options and lists them:
103
+
104
+ ```text
105
+ Here's what I assumed
106
+ - Platform: responsive web app (PWA later): a household list is used on phones, and a PWA avoids app-store review.
107
+ - Stack: Next.js + TypeScript + Postgres (Prisma): the closest preset for a small web app.
108
+ - Auth: email magic link: members already arrive through an email invite.
109
+ - Email: Resend, behind a mock mailer in dev and CI.
110
+ - Hosting: one web service plus managed Postgres.
111
+ ```
112
+
113
+ ### The spike doc
114
+
115
+ `docs/engineering/technical-spike.md` has every required section, each with a rationale:
116
+
117
+ ```markdown
118
+ # Shelf — technical spike
119
+
120
+ ## Stack
121
+ Next.js (App Router) + TypeScript, Postgres via Prisma, pnpm.
122
+ **Rationale**: one language end to end; the closest stack preset for a small web app.
123
+
124
+ ## Repo layout
125
+ Single app: `app/`, `lib/`, `prisma/`.
126
+ **Rationale**: one deployable; no monorepo until a second app exists.
127
+
128
+ ## Runtime topology
129
+ One web service plus managed Postgres.
130
+ **Rationale**: the cheapest setup that still gives live updates for a few households.
131
+
132
+ ## Data model
133
+ `Household`, `Member`, `List`, `Item`; every row carries `householdId`.
134
+ **Rationale**: the household is the sharing and access boundary.
135
+
136
+ ## Security and privacy
137
+ Members only ever read their own household's rows; invites expire after 7 days.
138
+ **Rationale**: grocery lists are personal data.
139
+
140
+ ## Design system and quality gates
141
+ shadcn/ui; `pnpm build`, lint and unit tests in CI.
142
+ **Rationale**: accessible defaults and fast feedback.
143
+
144
+ ## Route/API map
145
+ `/api/lists`, `/api/invites`.
146
+ **Rationale**: one route per milestone feature.
147
+
148
+ ## Credentials
149
+ Resend API key (needs-human); the mock mailer is used until it exists.
150
+ **Rationale**: agents cannot create third-party accounts.
151
+
152
+ ## Milestone plan
153
+ 1. Foundation: scaffold, schema. 2. Sharing: list API, invites.
154
+ **Rationale**: sharing needs the schema first.
155
+ ```
156
+
157
+ ### The plan
158
+
159
+ Two milestones, a spike, four build issues and one needs-human issue. `INVITES` mentions <code v-pre>#{{RESEND_KEY}}</code> but doesn't depend on it, so it can be built and tested against the mock mailer.
160
+
161
+ ```json
162
+ {
163
+ "labels": [
164
+ "frontend",
165
+ "backend",
166
+ "infra",
167
+ "needs-human"
168
+ ],
169
+ "milestones": [
170
+ {
171
+ "key": "foundation",
172
+ "title": "Foundation"
173
+ },
174
+ {
175
+ "key": "sharing",
176
+ "title": "Sharing"
177
+ }
178
+ ],
179
+ "spikeDoc": "docs/engineering/technical-spike.md",
180
+ "guardrails": [
181
+ "no email is sent without an explicit invite"
182
+ ],
183
+ "domainRules": [
184
+ "one household per list"
185
+ ],
186
+ "issues": [
187
+ {
188
+ "key": "SPIKE",
189
+ "title": "spike: Shelf stack, layout and milestone plan",
190
+ "milestone": "foundation",
191
+ "labels": [
192
+ "infra"
193
+ ],
194
+ "deps": [],
195
+ "body": "## Problem\n\nDecide Shelf's stack, repo layout and milestone plan.\n\n## Root Cause (if known)\n\nGreenfield: not built yet.\n\n## Affected Files\n\n1. `docs/engineering/technical-spike.md` — new\n\n## Acceptance Criteria\n\n- [ ] Spike doc covers every section with a rationale [type:manual]\n\n## Context\n\n**Parent**: none (spike)\n\n## Dependencies\n\nNone.\n"
196
+ },
197
+ {
198
+ "key": "SCAFFOLD",
199
+ "title": "scaffold: Next.js app with pnpm and CI",
200
+ "milestone": "foundation",
201
+ "labels": [
202
+ "frontend",
203
+ "infra"
204
+ ],
205
+ "deps": [
206
+ "SPIKE"
207
+ ],
208
+ "priority": "p0",
209
+ "body": "## Problem\n\nScaffold the Next.js app with pnpm and a CI workflow.\n\n## Root Cause (if known)\n\nGreenfield: not built yet.\n\n## Affected Files\n\n1. `package.json` — new\n2. `app/layout.tsx` — new\n3. `.github/workflows/ci.yml` — new\n\n## Acceptance Criteria\n\n- [ ] `pnpm build` succeeds [type:unit]\n\n## Context\n\n**Parent**: #{{SPIKE}}\n\n## Dependencies\n\nDepends on #{{SPIKE}} — needs the stack decision\n"
210
+ },
211
+ {
212
+ "key": "DB",
213
+ "title": "db: Postgres schema for households, lists and items",
214
+ "milestone": "foundation",
215
+ "labels": [
216
+ "backend"
217
+ ],
218
+ "deps": [
219
+ "SCAFFOLD"
220
+ ],
221
+ "priority": "p0",
222
+ "body": "## Problem\n\nPostgres schema for households, lists and items via Prisma.\n\n## Root Cause (if known)\n\nGreenfield: not built yet.\n\n## Affected Files\n\n1. `prisma/schema.prisma` — new\n\n## Acceptance Criteria\n\n- [ ] A member reads only their own household's lists [type:unit]\n\n## Context\n\n**Parent**: #{{SPIKE}}\n\n## Dependencies\n\nDepends on #{{SCAFFOLD}} — needs the app and package setup\n"
223
+ },
224
+ {
225
+ "key": "LISTS_API",
226
+ "title": "lists: Shared list API with live updates",
227
+ "milestone": "sharing",
228
+ "labels": [
229
+ "backend"
230
+ ],
231
+ "deps": [
232
+ "DB"
233
+ ],
234
+ "priority": "p1",
235
+ "body": "## Problem\n\nAPI to add, check off and remove items, with live updates for everyone in the household.\n\n## Root Cause (if known)\n\nGreenfield: not built yet.\n\n## Affected Files\n\n1. `app/api/lists/route.ts` — new\n\n## Acceptance Criteria\n\n- [ ] Checking an item off shows up for a second member [type:api]\n\n## Context\n\n**Parent**: #{{SPIKE}}\n\n## Dependencies\n\nDepends on #{{DB}} — needs the schema\n"
236
+ },
237
+ {
238
+ "key": "INVITES",
239
+ "title": "invites: Email invites to join a household",
240
+ "milestone": "sharing",
241
+ "labels": [
242
+ "backend"
243
+ ],
244
+ "deps": [
245
+ "LISTS_API"
246
+ ],
247
+ "priority": "p1",
248
+ "body": "## Problem\n\nInvite a member by email. Sending uses Resend; the key comes from #{{RESEND_KEY}}, and until it exists dev and CI use the mock mailer.\n\n## Root Cause (if known)\n\nGreenfield: not built yet.\n\n## Affected Files\n\n1. `app/api/invites/route.ts` — new\n2. `lib/mailer.ts` — new\n\n## Acceptance Criteria\n\n- [ ] An invite link adds the member to the household [type:api]\n\n## Context\n\n**Parent**: #{{SPIKE}}\n\n## Dependencies\n\nDepends on #{{LISTS_API}} — needs households and lists\n"
249
+ },
250
+ {
251
+ "key": "RESEND_KEY",
252
+ "title": "resend: Create the Resend API key and store it",
253
+ "milestone": "sharing",
254
+ "labels": [
255
+ "needs-human"
256
+ ],
257
+ "deps": [],
258
+ "needsHuman": true,
259
+ "body": "## Problem\n\nCreate the Resend API key that #{{INVITES}} uses to send email.\n\n### Setup steps\n\n1. Create an API key at https://resend.com/api-keys with sending access only.\n2. Store it as the `RESEND_API_KEY` deploy secret.\n\n### Environment variables\n\n- `RESEND_API_KEY` — Resend sending key\n\n### Secret handling\n\nNever commit the key or paste it into issues. Until it exists, dev and CI use the mock mailer.\n\n## Root Cause (if known)\n\nGreenfield: not built yet.\n\n## Affected Files\n\n1. `docs/setup/resend.md` — new\n\n## Acceptance Criteria\n\n- [ ] Key stored as a deploy secret [type:manual]\n- [ ] The real mailer replaces the mock in staging [type:manual]\n\n## Context\n\n**Parent**: #{{SPIKE}}\n\n## Dependencies\n\nNone.\n"
260
+ }
261
+ ]
262
+ }
263
+ ```
264
+
265
+ `node scripts/bootstrap-validate.mjs plan.json` prints `bootstrap-validate: OK (6 issues, 2 milestones)`.
266
+
267
+ ### The dry-run preview
268
+
269
+ `npx forgedock bootstrap --plan plan.json --dry-run --repo acme/shelf` prints:
270
+
271
+ ```text
272
+ ## Bootstrap plan preview (dry run: nothing written to GitHub)
273
+
274
+ Spike: spike: Shelf stack, layout and milestone plan — doc `docs/engineering/technical-spike.md`
275
+
276
+ ### 1. Foundation (`milestone/foundation`, 2 issues)
277
+ - scaffold: Next.js app with pnpm and CI
278
+ - db: Postgres schema for households, lists and items — depends on SCAFFOLD
279
+
280
+ ### 2. Sharing (`milestone/sharing`, 3 issues)
281
+ - lists: Shared list API with live updates — depends on DB
282
+ - invites: Email invites to join a household — depends on LISTS_API
283
+ - resend: Create the Resend API key and store it [needs-human]
284
+
285
+ DAG: 5 sub-issues (4 build, 1 needs-human), 3 dependency edges, 2 milestones.
286
+ Estimated agent cost: ~$12.00 (4 build issues × $3/issue; needs-human and spike excluded).
287
+ ```
288
+
289
+ ### Create and hand off
290
+
291
+ `npx forgedock bootstrap --plan plan.json --repo acme/shelf` creates the backlog and prints the hand-off. The issue numbers below are illustrative:
292
+
293
+ ```text
294
+ ## Bootstrap hand-off
295
+
296
+ Spike: #1 spike: Shelf stack, layout and milestone plan — https://github.com/acme/shelf/issues/1
297
+
298
+ ### Created
299
+ - #2 scaffold: Next.js app with pnpm and CI — https://github.com/acme/shelf/issues/2
300
+ - #3 db: Postgres schema for households, lists and items — https://github.com/acme/shelf/issues/3
301
+ - #4 lists: Shared list API with live updates — https://github.com/acme/shelf/issues/4
302
+ - #5 invites: Email invites to join a household — https://github.com/acme/shelf/issues/5
303
+
304
+ ### Needs a human
305
+ - #6 resend: Create the Resend API key and store it — https://github.com/acme/shelf/issues/6
306
+
307
+ ### Next commands (in build order)
308
+ 1. /orchestrate milestone "Foundation"
309
+ 2. /orchestrate milestone "Sharing"
310
+ ```
311
+
312
+ Then:
313
+
314
+ ```bash
315
+ /orchestrate milestone "Foundation" # scaffold, then the schema
316
+ /orchestrate milestone "Sharing" # list API, then invites (against the mock mailer until #6 is done)
317
+ ```
318
+
319
+ If creation fails halfway, for example at issue #4, re-run `npx forgedock bootstrap --plan plan.json --repo acme/shelf`. Issues #1–#3 are reused and creation carries on from #4.
320
+
321
+ ---
322
+
323
+ ## Next
324
+
325
+ - [Command Reference: `/bootstrap`](./command-reference.md#bootstrap)
326
+ - [How It Works](./how-it-works.md): what `/orchestrate` and `/work-on` do with each issue
327
+ - [Troubleshooting & Recovery](./troubleshooting.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.27.0",
3
+ "version": "1.28.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"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # affected-files-check.sh — /issue Phase 3E rule 2: are the draft's Affected Files real? (forge#4172)
6
+ #
7
+ # Usage:
8
+ # affected-files-check.sh <body-file> [--ref <git-ref>]... [--spike <spike-doc>]
9
+ #
10
+ # <body-file> issue body (markdown); paths are the backticked tokens containing '/' or '.'
11
+ # inside its `## Affected Files` section (up to the next `## ` heading).
12
+ # --ref git ref to check existence against (repeatable). Default: origin/staging then
13
+ # origin/main; refs that do not resolve are skipped. Run from the target repo.
14
+ # --spike greenfield mode: a path that exists at no ref still passes as PLANNED when it
15
+ # equals an entry of the spike doc's repo-layout section, or starts with an entry
16
+ # ending in '/'. The section is the first heading matching /repo layout/i; its
17
+ # entries are backticked paths plus the lines of fenced blocks (tree glyphs and
18
+ # `#` comments stripped). Entries are full repo-relative paths: indentation-based
19
+ # nesting is not reconstructed.
20
+ #
21
+ # Output: one `OK <path>` | `PLANNED <path>` | `MISSING <path>` line per path.
22
+ # Exit: 0 no MISSING | 1 any MISSING, or no paths found | 2 usage error (no body file,
23
+ # --spike file absent, spike doc without a repo-layout section or entries).
24
+ # Deterministic, no LLM. bash 3.2 portable.
25
+
26
+ set -u
27
+ BODY_FILE=""
28
+ SPIKE=""
29
+ REFS=""
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --ref) [ -n "${2:-}" ] || { echo "affected-files-check: --ref requires a value" >&2; exit 2; }
33
+ REFS="$REFS
34
+ $2"; shift 2 ;;
35
+ --spike) [ -n "${2:-}" ] || { echo "affected-files-check: --spike requires a value" >&2; exit 2; }
36
+ SPIKE="$2"; shift 2 ;;
37
+ --*) echo "affected-files-check: unknown argument: $1" >&2; exit 2 ;;
38
+ *) [ -z "$BODY_FILE" ] || { echo "affected-files-check: more than one body file" >&2; exit 2; }
39
+ BODY_FILE="$1"; shift ;;
40
+ esac
41
+ done
42
+ [ -n "$BODY_FILE" ] && [ -r "$BODY_FILE" ] || { echo "usage: affected-files-check.sh <body-file> [--ref <git-ref>]... [--spike <spike-doc>]" >&2; exit 2; }
43
+ [ -n "$REFS" ] || REFS="
44
+ origin/staging
45
+ origin/main"
46
+
47
+ # Backticked path-like tokens from the `## Affected Files` section.
48
+ PATHS=$(awk '/^## /{ins=($0 ~ /^## Affected Files[[:space:]]*$/); next} ins' "$BODY_FILE" \
49
+ | grep -oE '`[^` ]+`' | tr -d '`' | grep -E '[/.]' | sed 's|^\./||; s|:[0-9][0-9,-]*$||' | awk '!seen[$0]++' || true)
50
+
51
+ LAYOUT=""
52
+ if [ -n "$SPIKE" ]; then
53
+ [ -r "$SPIKE" ] || { echo "affected-files-check: spike doc not found: $SPIKE" >&2; exit 2; }
54
+ grep -qiE '^#+[[:space:]].*repo layout' "$SPIKE" || { echo "affected-files-check: spike doc has no 'Repo layout' section: $SPIKE" >&2; exit 2; }
55
+ LAYOUT=$(awk '
56
+ /^[[:space:]]*```/ { fence=!fence; next }
57
+ !fence && /^#+[[:space:]]/ { if (ins) exit; if (tolower($0) ~ /repo layout/) ins=1; next }
58
+ !ins { next }
59
+ fence { sub(/#.*/, ""); gsub(/├──|└──|│|─/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); if ($0 != "") print; next }
60
+ { while (match($0, /`[^` ]+`/)) { print substr($0, RSTART+1, RLENGTH-2); $0 = substr($0, RSTART+RLENGTH) } }
61
+ ' "$SPIKE" | sed 's|^\./||' | grep -E '[/.]' | awk '!seen[$0]++' || true)
62
+ [ -n "$LAYOUT" ] || { echo "affected-files-check: spike doc 'Repo layout' section declares no paths: $SPIKE" >&2; exit 2; }
63
+ fi
64
+
65
+ [ -n "$PATHS" ] || { echo "MISSING (no backticked paths in ## Affected Files)"; exit 1; }
66
+
67
+ exists_at_ref() { # path -> 0 when it exists at any resolvable ref
68
+ printf '%s\n' "$REFS" | while IFS= read -r r; do
69
+ [ -n "$r" ] || continue
70
+ git cat-file -e "$r:$1" 2>/dev/null && { echo y; break; }
71
+ done | grep -q y
72
+ }
73
+ in_layout() { # path -> 0 when it equals a declared entry or sits under a declared directory
74
+ printf '%s\n' "$LAYOUT" | while IFS= read -r e; do
75
+ [ -n "$e" ] || continue
76
+ if [ "$1" = "$e" ]; then echo y; break; fi
77
+ case "$e" in */) case "$1" in "$e"*) echo y; break ;; esac ;; esac
78
+ done | grep -q y
79
+ }
80
+
81
+ FAIL=0
82
+ while IFS= read -r p; do
83
+ [ -n "$p" ] || continue
84
+ if exists_at_ref "$p"; then echo "OK $p"
85
+ elif [ -n "$LAYOUT" ] && in_layout "$p"; then echo "PLANNED $p"
86
+ else echo "MISSING $p"; FAIL=1
87
+ fi
88
+ done <<EOF
89
+ $PATHS
90
+ EOF
91
+ exit $FAIL
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # affected-files-check.test.sh — tests for scripts/affected-files-check.sh (forge#4172)
3
+ #
4
+ # Builds a throwaway git repo under $TMPDIR; no network or GitHub access.
5
+ # Usage: bash scripts/affected-files-check.test.sh
6
+ # Exit code: 0 if all assertions pass, 1 if any fail.
7
+ #
8
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
9
+ # SPDX-License-Identifier: AGPL-3.0-or-later
10
+
11
+ set -uo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ CHECK="$SCRIPT_DIR/affected-files-check.sh"
15
+ T=$(mktemp -d "${TMPDIR:-/tmp}/afc-test.XXXXXX")
16
+ trap 'rm -rf "$T"' EXIT
17
+
18
+ PASS=0; FAIL=0
19
+ expect() { # label expected actual
20
+ if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); printf 'FAIL %s\n expected: %s\n actual: %s\n' "$1" "$2" "$3"; fi
21
+ }
22
+ run() { # args... -> "<exit>|<stdout lines joined by ;>"
23
+ local out rc
24
+ out=$(cd "$T/repo" && bash "$CHECK" "$@" 2>/dev/null); rc=$?
25
+ printf '%s|%s' "$rc" "$(printf '%s' "$out" | tr '\n' ';')"
26
+ }
27
+ body() { # name lines... -> body file path
28
+ local f="$T/$1.md"; shift
29
+ { printf '## Problem\n\nx `not/listed.ts`\n\n## Affected Files\n\n'; printf '%s\n' "$@"; printf '\n## Acceptance Criteria\n\n- [ ] y\n'; } > "$f"
30
+ printf '%s' "$f"
31
+ }
32
+
33
+ mkdir -p "$T/repo/src"
34
+ git -C "$T/repo" init -q -b main
35
+ printf 'x\n' > "$T/repo/src/existing.ts"
36
+ git -C "$T/repo" add -A && git -C "$T/repo" -c user.name=t -c user.email=t@t commit -qm init
37
+
38
+ cat > "$T/spike.md" <<'EOF'
39
+ # Technical spike
40
+
41
+ ## Repo layout
42
+
43
+ Apps live in `apps/web/` and the shared schema is `packages/db/schema.sql`.
44
+
45
+ ```
46
+ apps/api/ # backend
47
+ ├── apps/api/src/main.ts
48
+ └── docs/README.md
49
+ ```
50
+
51
+ ## Data model
52
+
53
+ `packages/billing/` is mentioned outside the layout section.
54
+ EOF
55
+ printf '# Spike\n\n## Stack\n\n`apps/web/`\n' > "$T/nolayout.md"
56
+
57
+ R="--ref main"
58
+
59
+ # Brownfield (no --spike): unchanged semantics — existing OK, missing MISSING.
60
+ expect "brownfield existing path" "0|OK src/existing.ts" "$(run "$(body b1 '1. `src/existing.ts`: the file')" $R)"
61
+ expect "brownfield missing path" "1|MISSING apps/web/page.tsx" "$(run "$(body b2 '1. `apps/web/page.tsx`: planned')" $R)"
62
+ expect "brownfield ignores paths outside the section" "0|OK src/existing.ts" "$(run "$(body b3 '- `src/existing.ts`')" $R)"
63
+ expect "brownfield unresolvable default refs -> MISSING" "1|MISSING src/existing.ts" "$(run "$(body b4 '- `src/existing.ts`')")"
64
+
65
+ # Greenfield (--spike): planned paths validated against the declared layout.
66
+ expect "greenfield path under declared dir" "0|PLANNED apps/web/app/page.tsx" "$(run "$(body g1 '- `apps/web/app/page.tsx`')" $R --spike "$T/spike.md")"
67
+ expect "greenfield path equal to declared file" "0|PLANNED packages/db/schema.sql" "$(run "$(body g2 '- `packages/db/schema.sql`')" $R --spike "$T/spike.md")"
68
+ expect "greenfield path from fenced tree" "0|PLANNED apps/api/src/main.ts;PLANNED apps/api/routes.ts" "$(run "$(body g3 '- `apps/api/src/main.ts`' '- `apps/api/routes.ts`')" $R --spike "$T/spike.md")"
69
+ expect "greenfield path outside layout" "1|PLANNED apps/web/x.ts;MISSING packages/billing/x.ts" "$(run "$(body g4 '- `apps/web/x.ts`' '- `packages/billing/x.ts`')" $R --spike "$T/spike.md")"
70
+ expect "greenfield existing path still OK" "0|OK src/existing.ts" "$(run "$(body g5 '- `src/existing.ts`')" $R --spike "$T/spike.md")"
71
+ expect "greenfield file prefix is not a directory" "1|MISSING packages/db/schema.sql.bak" "$(run "$(body g6 '- `packages/db/schema.sql.bak`')" $R --spike "$T/spike.md")"
72
+
73
+ # Usage errors.
74
+ expect "spike doc missing" "2|" "$(run "$(body u1 '- `apps/web/x.ts`')" $R --spike "$T/absent.md")"
75
+ expect "spike doc without repo layout" "2|" "$(run "$(body u2 '- `apps/web/x.ts`')" $R --spike "$T/nolayout.md")"
76
+ expect "no body file" "2|" "$(run)"
77
+ expect "body without Affected Files paths" "1|MISSING (no backticked paths in ## Affected Files)" "$(run "$(body e1 'Files to be identified during investigation.')" $R)"
78
+
79
+ echo "affected-files-check.test.sh: $PASS passed, $FAIL failed"
80
+ [ "$FAIL" -eq 0 ]