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.
- package/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +10 -0
- package/bin/engine/telemetry-seam.mjs +37 -8
- package/bin/engine/telemetry-seam.test.mjs +31 -0
- package/bin/engine-cli.mjs +542 -38
- package/bin/engine.mjs +8 -4
- package/bin/forgedock.mjs +75 -9
- package/bin/gc.mjs +580 -0
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +22 -4
- package/bin/lean/batch.test.mjs +5 -3
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +190 -58
- package/bin/lean/conformance.test.mjs +116 -7
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +85 -8
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +572 -139
- package/bin/lean/lean.test.mjs +1193 -31
- package/bin/lean/ledger.mjs +27 -2
- package/bin/lean/ledger.test.mjs +36 -1
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +312 -19
- package/bin/lean/prompts.mjs +89 -15
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +24 -5
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +20 -9
- package/bin/lean/verify.test.mjs +26 -3
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +7 -0
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +177 -2
- package/docs/OUTCOME-CODES.md +8 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +36 -2
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +9 -4
- package/scripts/check-devdocs-freshness.test.sh +11 -1
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +3 -3
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +1 -1
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/structure-delta.sh +10 -5
- package/scripts/structure-delta.test.sh +3 -2
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
package/docs/OUTCOME-CODES.md
CHANGED
|
@@ -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.
|
|
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 ]
|