forgedock 1.0.22 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -0,0 +1,189 @@
1
+ # Signed, Replayable Provenance Trail — Design Spec
2
+
3
+ **Date:** 2026-07-04
4
+ **Status:** Design — ready to build
5
+ **Tracking:** #1318 (provenance layer), epic #1320 (five foundations of autonomy)
6
+ **Depends on:** #1256 (durable engine run-log — replayability requires a committed step log)
7
+ **Scope:** Integrity guarantee + replayability layer on top of the existing FORGE annotation protocol (#1291–1294). Every autonomous PR carries a signed attestation linking decisions → verification evidence → alternatives rejected.
8
+
9
+ ---
10
+
11
+ ## 1. Motivation
12
+
13
+ For an organization to allow autonomous merges to production, every change needs a complete, verifiable record of *why it was made, what was verified, and what alternatives were considered and rejected* — reproducibly.
14
+
15
+ Today provenance is scattered `FORGE:*` comments with no integrity guarantee:
16
+
17
+ | Gap | Current state | Problem |
18
+ |-----|--------------|---------|
19
+ | Integrity | Comments are plain Markdown, mutable | Any party can edit or delete them after the fact |
20
+ | Completeness | Each annotation covers one phase | No single artifact links decision → evidence → alternatives |
21
+ | Replayability | State is prose — humans can read it but tools cannot deterministically replay it | Resume re-parses prose heuristically |
22
+ | Attestation | No signed artifact exists | Human reviewers have no cryptographic basis to trust "the agent said it was safe" |
23
+
24
+ **Trust, not capability, is the blocker to auto-merge.** This spec adds integrity + replayability + a signed attestation on top of the existing FORGE protocol, without replacing it.
25
+
26
+ ---
27
+
28
+ ## 2. Goals / Non-goals
29
+
30
+ **Goals**
31
+ - Each autonomous PR carries a `FORGE:PROVENANCE` attestation that links: investigation decision → build decisions → verification evidence → rejected alternatives.
32
+ - A run is replayable from its provenance record + durable engine run-log (#1256).
33
+ - The provenance record validates against the FORGE protocol conformance suite (#1291).
34
+ - Integrity is achieved via a content hash over the structured annotation chain — no external PKI required for v1.
35
+
36
+ **Non-goals**
37
+ - Hardware-rooted signing (TPM, HSM) — out of scope for v1; the content hash gives tamper-evidence without it.
38
+ - Retroactive provenance for runs completed before this spec ships.
39
+ - Per-turn provenance (this spec is per-run / per-PR granularity, matching the durable engine's per-phase checkpoint).
40
+ - Provenance for interactive `/work-on` runs (headless/orchestrated first, interactive later).
41
+
42
+ ---
43
+
44
+ ## 3. Locked Decisions
45
+
46
+ 1. **Attestation format: structured HTML comment (`FORGE:PROVENANCE`) on the PR.** Consistent with the existing annotation protocol. Readable by humans in the PR thread and queryable by the same text-contains filters used today.
47
+ 2. **Integrity mechanism: SHA-256 content hash over a canonical serialization of the annotation chain.** The hash is computed over the ordered sequence of `FORGE:*` annotation bodies (canonically serialized as JSON). Anyone with the run-log can recompute it.
48
+ 3. **Replayability: the durable engine run-log is the replay substrate.** The `FORGE:PROVENANCE` record carries a pointer to the run-log location (issue comment ID sequence). A conformant replayer reads those comments in order and reconstructs the pipeline state.
49
+ 4. **Chain of custody: each annotation carries a `seq` (sequence number) and `prev_hash` (hash of the prior annotation).** This forms a tamper-evident linked list. Adding, removing, or reordering annotations breaks the hash chain.
50
+ 5. **The attestation is written by the durable engine, not by individual phase agents.** Agents write their phase annotations as today; the engine writes the final `FORGE:PROVENANCE` after the merge gate passes.
51
+
52
+ ---
53
+
54
+ ## 4. Architecture
55
+
56
+ ### 4.1 Annotation Chain Extension
57
+
58
+ Existing FORGE annotations gain two new fields (backward-compatible — consumers that do not understand them ignore them):
59
+
60
+ ```
61
+ <!-- FORGE:INVESTIGATOR
62
+ seq: 1
63
+ prev_hash: "genesis"
64
+ ...existing fields...
65
+ body_hash: "sha256:a1b2c3..."
66
+ -->
67
+ ```
68
+
69
+ | New field | Type | Description |
70
+ |-----------|------|-------------|
71
+ | `seq` | integer | Position in the annotation chain for this run (1-indexed) |
72
+ | `prev_hash` | string | SHA-256 of the prior annotation's canonical body, or `"genesis"` for the first |
73
+ | `body_hash` | string | SHA-256 of this annotation's canonical body (self-hash, written last) |
74
+
75
+ **Canonical body**: the annotation content between the `<!-- FORGE:TAG` opening and the `-->` closing, with leading/trailing whitespace stripped and line endings normalized to `\n`, serialized as a UTF-8 JSON string.
76
+
77
+ ### 4.2 FORGE:PROVENANCE Annotation
78
+
79
+ Written to the PR by the durable engine after the merge gate passes, before the merge commit:
80
+
81
+ ```html
82
+ <!-- FORGE:PROVENANCE
83
+ run_id: "run-abc123"
84
+ issue: 1317
85
+ pr: 1460
86
+ pipeline_version: "1.0.21"
87
+ agent_model: "claude-sonnet-4-6"
88
+
89
+ chain:
90
+ - seq: 1 tag: INVESTIGATOR comment_id: 2345678901 body_hash: "sha256:a1b2..."
91
+ - seq: 2 tag: ARCHITECT comment_id: 2345678910 body_hash: "sha256:b2c3..."
92
+ - seq: 3 tag: BUILDER comment_id: 2345678920 body_hash: "sha256:c3d4..."
93
+ - seq: 4 tag: REVIEWER comment_id: 2345678930 body_hash: "sha256:d4e5..."
94
+ - seq: 5 tag: QUALITY_GATE comment_id: 2345678940 body_hash: "sha256:e5f6..."
95
+ - seq: 6 tag: ECONOMICS comment_id: 2345678950 body_hash: "sha256:f6a7..."
96
+
97
+ chain_hash: "sha256:0123456789abcdef..."
98
+ verification_passed: true
99
+ alternatives_considered:
100
+ - "Inline fix without issue — rejected: no traceability"
101
+ - "Defer to manual review — rejected: low-risk score 0.82"
102
+ merge_authorized_by: "economic-self-governance"
103
+ timestamp: 2026-07-04T18:00:00Z
104
+ forge_protocol_version: "1.0"
105
+ -->
106
+ <!-- FORGE:PROVENANCE:COMPLETE -->
107
+ ```
108
+
109
+ **`chain_hash`**: SHA-256 of the concatenated `body_hash` values in `seq` order, joined with `\n`. Recomputable from the listed comment IDs alone.
110
+
111
+ ### 4.3 Replayability Contract
112
+
113
+ A conformant replayer MUST be able to reconstruct the pipeline state for a given `run_id` by:
114
+
115
+ 1. Reading the `FORGE:PROVENANCE` annotation from the PR to get the `chain` (comment ID list).
116
+ 2. Fetching each comment in `seq` order from the GitHub API.
117
+ 3. Parsing each `FORGE:*` annotation body.
118
+ 4. Recomputing the `chain_hash` and asserting it matches the recorded value.
119
+ 5. Replaying the durable engine run-log (from `#1256`) for per-phase step details.
120
+
121
+ Step 4 is the integrity check. A mismatch means the annotation chain was tampered with after the run.
122
+
123
+ ### 4.4 Conformance Suite Integration (#1291)
124
+
125
+ The FORGE protocol conformance suite gains a new test category: **Provenance Conformance**.
126
+
127
+ | Test | Pass condition |
128
+ |------|---------------|
129
+ | `P-01` | PR has exactly one `FORGE:PROVENANCE` annotation followed by `FORGE:PROVENANCE:COMPLETE` |
130
+ | `P-02` | All `seq` values in the chain are contiguous starting from 1 |
131
+ | `P-03` | Each `prev_hash` matches the `body_hash` of the prior annotation (or `"genesis"` for seq=1) |
132
+ | `P-04` | `chain_hash` recomputes correctly from the listed `body_hash` values |
133
+ | `P-05` | All annotations referenced in the chain are fetchable and parse as valid FORGE annotations |
134
+ | `P-06` | `timestamp` is an ISO 8601 UTC datetime |
135
+ | `P-07` | `forge_protocol_version` is present and a known value |
136
+
137
+ ### 4.5 Integration Points
138
+
139
+ **Durable engine (`#1256`)**: After merge gate passes, the engine invokes `write_provenance()`:
140
+ - Collects all annotation comment IDs from the run-log.
141
+ - Fetches each annotation body and computes `body_hash` values.
142
+ - Computes `chain_hash`.
143
+ - Posts `FORGE:PROVENANCE` to the PR.
144
+
145
+ **Quality gate (`/quality-gate`)**: Gains a new check: `PROVENANCE` — verifies `P-01` through `P-07`. Fails the gate if any check fails on an autonomous run. Human-initiated runs emit a warning (not a hard failure) if provenance is absent.
146
+
147
+ **`/replay` command**: Uses the `chain` from `FORGE:PROVENANCE` as the replay index instead of scanning all comments for `FORGE:*` annotations. This is faster and guaranteed-ordered.
148
+
149
+ ---
150
+
151
+ ## 5. Implementation Plan
152
+
153
+ ### Increment 1: Chain fields on existing annotations (non-breaking)
154
+ - Modify durable engine to write `seq`, `prev_hash`, and `body_hash` on each annotation it posts.
155
+ - No consumer changes required — fields are additive.
156
+ - Validate that existing annotations without these fields are still parseable.
157
+
158
+ ### Increment 2: FORGE:PROVENANCE annotation
159
+ - Engine writes `FORGE:PROVENANCE` to the PR after merge gate passes.
160
+ - Includes `chain`, `chain_hash`, `verification_passed`, `timestamp`.
161
+ - `/replay` updated to use the chain index when present.
162
+
163
+ ### Increment 3: Conformance tests + quality gate check
164
+ - Add `P-01` through `P-07` tests to the conformance suite (#1291).
165
+ - Add `PROVENANCE` check to `/quality-gate` (warning on human runs, hard failure on autonomous runs).
166
+
167
+ ### Increment 4: Replayability validation
168
+ - Add a `verify_provenance(run_id)` script to `scripts/`.
169
+ - Script fetches the PR, reads `FORGE:PROVENANCE`, fetches all chained comments, recomputes `chain_hash`, and reports pass/fail.
170
+
171
+ ---
172
+
173
+ ## 6. Open Questions
174
+
175
+ - **Comment mutability**: GitHub allows editing comments. The hash chain detects tampering but does not prevent it. For v2, consider posting annotations to an immutable artifact (e.g., a GitHub Release asset or a signed commit note) as an optional hardened mode.
176
+ - **`body_hash` for legacy annotations**: If an annotation was written before this spec (no `body_hash` field), the chain is broken. Acceptable for v1: the `FORGE:PROVENANCE` chain only covers annotations written by the durable engine (which will be updated atomically). Legacy runs have no provenance claim.
177
+ - **`alternatives_considered` source**: In v1 this is written by the engine based on the economics decision (#1317) and the quality-gate outcome. A richer alternatives record (e.g., architect alternatives from `FORGE:ARCHITECT`) can be pulled in a later increment.
178
+
179
+ ---
180
+
181
+ ## 7. References
182
+
183
+ - #1318 — this issue
184
+ - #1291–1294 — FORGE protocol conformance suite (seed schema)
185
+ - #1256 — durable engine (run-log substrate; required for replayability)
186
+ - #1315 — outcome-based acceptance gate (supplies "what was verified" evidence)
187
+ - #1317 — economic self-governance (supplies `merge_authorized_by` and `alternatives_considered`)
188
+ - #1320 — five foundations epic (provenance layer)
189
+ - `docs/spec/forge-protocol-v1.md` — FORGE annotation protocol v1.0
@@ -1,6 +1,6 @@
1
1
  # ForgeDock Demo — A Risk-Free First Pipeline Run
2
2
 
3
- A tiny **Notes API** (~300 lines, zero dependencies) with five pre-written
3
+ A tiny **Notes API** (~400 lines, zero dependencies) with twenty pre-written
4
4
  issues. It exists so you can watch ForgeDock investigate, build, review, and
5
5
  merge a change **without touching your real codebase**.
6
6
 
@@ -14,9 +14,10 @@ Trying ForgeDock on your own project means trusting an AI pipeline with real
14
14
  code on your very first run. This demo removes that risk: it's a self-contained
15
15
  sandbox where every issue is small, fast, and safe to break.
16
16
 
17
- The API has a couple of **intentional, clearly-labelled flaws** (a missing auth
18
- check, an unsafe query path) so that ForgeDock's `/review-pr` agents have real
19
- findings to surface — that's the part that makes the review feel impressive.
17
+ The API has a handful of **intentional, clearly-labelled flaws** (a missing auth
18
+ check, two injection-vulnerable query paths, a mass-assignment gap) so that
19
+ ForgeDock's `/review-pr` agents have real findings to surface — that's the part
20
+ that makes the review feel impressive.
20
21
 
21
22
  ---
22
23
 
@@ -27,11 +28,11 @@ forgedock-demo/
27
28
  ├── src/
28
29
  │ ├── server.js # built-in http server + router (no Express)
29
30
  │ ├── router.js # (added by Issue #3)
30
- │ ├── routes/notes.js # CRUD handlers — has the intentional flaws
31
- │ ├── db.js # in-memory store
31
+ │ ├── routes/notes.js # CRUD + count/tags/patch handlers — has intentional flaws
32
+ │ ├── db.js # in-memory store with tags, createdAt, archived fields
32
33
  │ └── auth.js # bearer-token check
33
34
  ├── scripts/smoke.js # dependency-free smoke test (npm run smoke)
34
- ├── issues/ # the 5 pre-written issue specs
35
+ ├── issues/ # 20 graded issue specs
35
36
  ├── forge.yaml # minimal working ForgeDock config
36
37
  ├── labels.json # workflow + priority labels
37
38
  └── bootstrap.sh # one command to stand up the live repo
@@ -52,20 +53,37 @@ npm run smoke # runs the smoke test
52
53
 
53
54
  ---
54
55
 
55
- ## The five demo issues
56
-
57
- Each issue targets a different pipeline strength and is scoped to finish fast.
58
-
59
- | # | Type | Title | What it shows |
60
- |---|------|-------|---------------|
61
- | 1 | Bug / security | DELETE is missing an auth check | Simplest fix — fastest run |
62
- | 2 | Feature / security | Safe filtering for `GET /notes` | Investigation + architecture |
63
- | 3 | Refactor | Extract the router module | "No behavior change" review |
64
- | 4 | Performance | O(1) `findById` | Investigation phase tracing the hot path |
65
- | 5 | Docs | Add an API reference | Pipeline handles non-code work |
56
+ ## The twenty demo issues
57
+
58
+ Each issue targets a different pipeline strength and difficulty tier.
59
+ Issues 1–5 are the original quick-start set; issues 6–20 expand the corpus
60
+ for statistically meaningful one-shot benchmark runs.
61
+
62
+ | # | Difficulty | Type | Title |
63
+ |---|-----------|------|-------|
64
+ | 1 | Easy | Bug / security | DELETE is missing an auth check |
65
+ | 2 | Medium | Feature / security | Safe filtering for `GET /notes` |
66
+ | 3 | Easy | Refactor | Extract the router module |
67
+ | 4 | Medium | Performance | O(1) `findById` |
68
+ | 5 | Easy | Docs | Add an API reference |
69
+ | 6 | Medium | Bug | POST stores malformed tags without validation |
70
+ | 7 | Medium | Bug | Negative `?offset=` returns wrong notes instead of 400 |
71
+ | 8 | Hard | Bug / security | PATCH allows mass-assignment of owner and secret |
72
+ | 9 | Hard | Bug / security | GET /notes/count is vulnerable to expression injection |
73
+ | 10 | Easy | Feature | Add `?sort=` parameter to GET /notes |
74
+ | 11 | Medium | Feature | Allow PATCH to update tags |
75
+ | 12 | Hard | Feature | POST /notes/bulk for all-or-nothing batch creation |
76
+ | 13 | Hard | Feature | Archive/soft-delete with `?archived=` list filter |
77
+ | 14 | Easy | Refactor | Extract title validation into validate.js |
78
+ | 15 | Medium | Refactor | Centralize auth enforcement with a protected route flag |
79
+ | 16 | Hard | Refactor | Extract generic store machinery into store.js |
80
+ | 17 | Easy | Performance | Replace O(n²) tag de-dup in listTags with a Set |
81
+ | 18 | Medium | Performance | Remove unnecessary O(n²) sort before pagination slice |
82
+ | 19 | Easy | Docs | Complete API reference covering all endpoints |
83
+ | 20 | Easy | Docs | Document the intentional-flaw and graded-issue convention |
66
84
 
67
85
  Full specs live in [`issues/`](./issues). When you run `bootstrap.sh`, these
68
- become real GitHub issues #1–#5 in your demo repo, in this order.
86
+ become real GitHub issues #1–#20 in your demo repo, in this order.
69
87
 
70
88
  ---
71
89
 
@@ -87,7 +105,7 @@ credentials is creating the actual repo. One command does it:
87
105
  2. Push this codebase to `main`.
88
106
  3. Create the workflow + priority labels (via `npx forgedock labels setup`,
89
107
  falling back to `labels.json`).
90
- 4. File the five issues from [`issues/`](./issues).
108
+ 4. File all twenty issues from [`issues/`](./issues).
91
109
 
92
110
  Then:
93
111
 
@@ -106,8 +124,9 @@ gate, open a PR, review it, and merge — all on a repo you can safely throw awa
106
124
 
107
125
  ## After your first run
108
126
 
109
- Try `/work-on 2` through `/work-on 5` to see the other pipeline phases, or run
110
- `/orchestrate` to let ForgeDock work several issues in parallel.
127
+ Try `/work-on 2` through `/work-on 20` to see the full range of pipeline phases
128
+ and issue types, or run `/orchestrate` to let ForgeDock work several issues in
129
+ parallel.
111
130
 
112
131
  When you're comfortable, point ForgeDock at your real project — you've already
113
132
  seen exactly what it does.
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: "fix(notes): POST /notes stores malformed tags without validation"
3
+ labels: ["bug", "priority:P2"]
4
+ difficulty: "medium"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `POST /notes` accepts a `tags` field from the request body but stores it without
10
+ checking that it is an array. When a caller passes `"tags": 42` (or any non-array
11
+ value), the malformed value is saved to the in-memory store. A subsequent
12
+ `GET /notes?tag=work` then throws an uncaught `TypeError: n.tags.includes is not
13
+ a function` from the tag-filter in `listNotes()`, returning a 500 response for
14
+ all future tag-filtered requests — until the server is restarted.
15
+
16
+ ## Steps to Reproduce
17
+
18
+ ```bash
19
+ # 1. Create a note with malformed tags
20
+ curl -s -X POST http://localhost:3000/notes \
21
+ -H "Authorization: Bearer demo-token" \
22
+ -H "Content-Type: application/json" \
23
+ -d '{"title":"bad","tags":99}' | jq .
24
+
25
+ # 2. Any tag-filtered list now 500s
26
+ curl -s "http://localhost:3000/notes?tag=work" | jq .
27
+ # => {"error":"n.tags.includes is not a function"}
28
+ ```
29
+
30
+ ## Affected Files
31
+
32
+ 1. `src/routes/notes.js` — `createNote()` must validate that `tags`, when
33
+ supplied, is an array. Return 400 if it is not.
34
+
35
+ ## Acceptance Criteria
36
+
37
+ - [ ] `POST /notes` with `"tags": 42` returns `400 Bad Request`.
38
+ - [ ] `POST /notes` with `"tags": ["work"]` still succeeds (201).
39
+ - [ ] `GET /notes?tag=work` works correctly after a valid note with tags is created.
40
+ - [ ] `npm run smoke` still passes.
41
+
42
+ > This issue exercises the **validation gap** pattern — the pipeline should
43
+ > add a type check at the write boundary so the read path stays safe.
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: "fix(notes): negative ?offset= returns wrong notes instead of 400"
3
+ labels: ["bug", "priority:P2"]
4
+ difficulty: "medium"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `GET /notes?offset=-1` silently applies JavaScript's negative-index slice
10
+ semantics (`Array.prototype.slice(-1)` returns the last element) instead of
11
+ returning a client error. A caller expecting an empty or 400 response receives
12
+ the final note in the list, which is confusing and incorrect.
13
+
14
+ ## Steps to Reproduce
15
+
16
+ ```bash
17
+ # List all notes first
18
+ curl -s http://localhost:3000/notes | jq '.notes | length'
19
+ # => 3
20
+
21
+ # Negative offset returns the last note — unexpected
22
+ curl -s "http://localhost:3000/notes?offset=-1" | jq '.notes'
23
+ # => [{...note 3...}]
24
+
25
+ # Expected: 400 Bad Request or normalized to 0
26
+ ```
27
+
28
+ ## Affected Files
29
+
30
+ 1. `src/routes/notes.js` — `listNotes()` must clamp `offset` to a non-negative
31
+ integer (reject with 400, or normalize to 0) before passing it to
32
+ `Array.prototype.slice`.
33
+
34
+ ## Acceptance Criteria
35
+
36
+ - [ ] `GET /notes?offset=-1` returns `400 Bad Request` with a clear error message.
37
+ - [ ] `GET /notes?offset=0` still returns all notes.
38
+ - [ ] `GET /notes?offset=1&limit=2` returns notes starting from index 1.
39
+ - [ ] `npm run smoke` still passes.
40
+
41
+ > This issue demonstrates that **numeric input validation** is a separate concern
42
+ > from type validation — the value is a number but falls outside the accepted range.
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: "fix(notes): PATCH /notes/:id allows mass-assignment of owner and secret"
3
+ labels: ["bug", "security", "priority:P1"]
4
+ difficulty: "hard"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `PATCH /notes/:id` lets any authenticated caller overwrite the `owner` and
10
+ `secret` fields alongside `title` and `body`. This is a mass-assignment
11
+ vulnerability: a caller can flip `"secret": true` notes to `false`, making them
12
+ visible in unauthenticated `GET /notes` responses, or reassign `owner` to
13
+ impersonate another user.
14
+
15
+ Unlike issue #1 (which has no auth check at all), this endpoint does require a
16
+ token — the flaw is that it accepts a too-wide field set.
17
+
18
+ ## Steps to Reproduce
19
+
20
+ ```bash
21
+ # Flip a secret note to non-secret without knowing its content
22
+ curl -s -X PATCH http://localhost:3000/notes/2 \
23
+ -H "Authorization: Bearer demo-token" \
24
+ -H "Content-Type: application/json" \
25
+ -d '{"secret":false}' | jq .
26
+ # => note is now visible to unauthenticated callers
27
+
28
+ # Confirm it is now publicly listed
29
+ curl -s http://localhost:3000/notes | jq '[.notes[] | select(.id==2)]'
30
+ # => [{...secret note now visible...}]
31
+ ```
32
+
33
+ ## Affected Files
34
+
35
+ 1. `src/routes/notes.js` — `updateNote()` must restrict the allowed fields to
36
+ `title` and `body` only. Attempts to set `owner` or `secret` should return
37
+ `400 Bad Request` with a message listing the disallowed fields.
38
+
39
+ ## Acceptance Criteria
40
+
41
+ - [ ] `PATCH /notes/:id` with `{"secret": false}` returns `400 Bad Request`.
42
+ - [ ] `PATCH /notes/:id` with `{"owner": "hacker"}` returns `400 Bad Request`.
43
+ - [ ] `PATCH /notes/:id` with `{"title": "new"}` still succeeds and updates the title.
44
+ - [ ] `PATCH /notes/:id` with both `{"title": "new", "secret": false}` returns `400`.
45
+ - [ ] `npm run smoke` still passes.
46
+
47
+ > This issue covers the **mass-assignment / over-permissive write** pattern — a
48
+ > common class of security bugs where an update endpoint accepts more fields than
49
+ > it should.
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: "fix(notes): GET /notes/count is vulnerable to expression injection"
3
+ labels: ["bug", "security", "priority:P1"]
4
+ difficulty: "hard"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `GET /notes/count` builds its `db.query()` where-clause by concatenating the raw
10
+ `?owner=` query parameter into a string: `'owner === "' + owner + '"'`. This is
11
+ the same injection class as the `?where=` flaw in issue #2, but in a new
12
+ endpoint that issue #2's fix does not cover.
13
+
14
+ A caller can inject arbitrary JavaScript: `?owner=" || true; //` would evaluate
15
+ to `owner === "" || true; //` — matching every note and leaking the real count
16
+ regardless of owner.
17
+
18
+ ## Steps to Reproduce
19
+
20
+ ```bash
21
+ # Normal count
22
+ curl -s "http://localhost:3000/notes/count?owner=alice" | jq .
23
+ # => {"count":2}
24
+
25
+ # Injection — count all notes regardless of owner
26
+ curl -s 'http://localhost:3000/notes/count?owner=" || true; //' | jq .
27
+ # => {"count":3} (all notes leaked)
28
+ ```
29
+
30
+ ## Affected Files
31
+
32
+ 1. `src/routes/notes.js` — `countNotes()` must not build a `db.query()` clause
33
+ via string concatenation. Replace the `db.query()` call with a direct
34
+ `db.all().filter()` using strict equality checks on typed values.
35
+
36
+ ## Acceptance Criteria
37
+
38
+ - [ ] `GET /notes/count?owner=alice` returns `{"count": 2}`.
39
+ - [ ] `GET /notes/count?owner=" || true; //` returns `{"count": 0}` (no match,
40
+ not a leak).
41
+ - [ ] `GET /notes/count?secret=true` returns the correct count of secret notes.
42
+ - [ ] `GET /notes/count` (no filters) returns the total note count.
43
+ - [ ] `npm run smoke` still passes.
44
+
45
+ > This issue reinforces the pattern from issue #2 — injection through dynamic
46
+ > `db.query()` clause building — but in a different endpoint, demonstrating that
47
+ > fixing the first occurrence doesn't automatically close the class.
@@ -0,0 +1,39 @@
1
+ ---
2
+ title: "feat(notes): add ?sort= parameter to GET /notes"
3
+ labels: ["feature", "priority:P3"]
4
+ difficulty: "easy"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `GET /notes` always returns notes in insertion order. There is no way for a
10
+ caller to request notes sorted by `title` or `createdAt` without fetching all
11
+ notes and sorting client-side.
12
+
13
+ ## Proposed Solution
14
+
15
+ Add a `?sort=` query parameter to `GET /notes` that accepts:
16
+
17
+ - `sort=id` — sort by numeric id ascending (current default behavior)
18
+ - `sort=title` — sort alphabetically by `title`
19
+ - `sort=createdAt` — sort by `createdAt` string (ISO 8601, lexicographic sort
20
+ is equivalent to chronological for the demo's fixed-format timestamps)
21
+
22
+ An unknown `sort` value should return `400 Bad Request`.
23
+
24
+ ## Affected Files
25
+
26
+ 1. `src/routes/notes.js` — extend `listNotes()` to read a `sort` query param
27
+ and apply the appropriate comparison before returning the results array.
28
+
29
+ ## Acceptance Criteria
30
+
31
+ - [ ] `GET /notes?sort=title` returns notes sorted alphabetically by title.
32
+ - [ ] `GET /notes?sort=createdAt` returns notes sorted oldest-first.
33
+ - [ ] `GET /notes?sort=id` returns notes in id order (same as current default).
34
+ - [ ] `GET /notes?sort=unknown` returns `400 Bad Request`.
35
+ - [ ] `GET /notes` (no sort param) still returns notes in insertion order.
36
+ - [ ] `npm run smoke` still passes.
37
+
38
+ > A focused, additive feature — the pipeline should confirm no existing
39
+ > behavior changes and that the 400 guard is present.
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "feat(notes): allow PATCH /notes/:id to update tags"
3
+ labels: ["feature", "priority:P3"]
4
+ difficulty: "medium"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `PATCH /notes/:id` currently only updates `title` and `body` (after the fix from
10
+ issue #8). There is no way to add or replace tags on an existing note without
11
+ deleting and recreating it.
12
+
13
+ ## Proposed Solution
14
+
15
+ Extend `PATCH /notes/:id` to accept an optional `tags` field. The value must be
16
+ an array of strings (validate at write time, same fix pattern as issue #6).
17
+ When supplied, it replaces the note's existing `tags` array entirely — a full
18
+ replacement (not a merge/append), keeping the semantics simple.
19
+
20
+ ## Affected Files
21
+
22
+ 1. `src/routes/notes.js` — `updateNote()` should accept `tags` in addition to
23
+ `title` and `body`. Validate that `tags` is an array when present; return
24
+ `400` if it is not.
25
+
26
+ ## Acceptance Criteria
27
+
28
+ - [ ] `PATCH /notes/:id` with `{"tags": ["work", "urgent"]}` replaces the note's
29
+ tags and returns the updated note.
30
+ - [ ] `PATCH /notes/:id` with `{"tags": 99}` returns `400 Bad Request`.
31
+ - [ ] Omitting `tags` from the patch body leaves existing tags unchanged.
32
+ - [ ] `GET /notes?tag=urgent` returns the patched note after its tags are updated.
33
+ - [ ] `npm run smoke` still passes.
34
+
35
+ > This feature has a deliberate dependency on issue #8 being fixed first — tags
36
+ > must not be writable via mass-assignment before they are intentionally writable
37
+ > via a validated field. Run issue #8 before this one.
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: "feat(notes): add POST /notes/bulk for all-or-nothing batch creation"
3
+ labels: ["feature", "priority:P3"]
4
+ difficulty: "hard"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ Creating multiple notes requires a separate `POST /notes` call per note.
10
+ For loading test fixtures or seeding data, callers want to create several notes
11
+ in one request, with the guarantee that either all are created or none are
12
+ (all-or-nothing semantics).
13
+
14
+ ## Proposed Solution
15
+
16
+ Add `POST /notes/bulk` (protected) that accepts `{ "notes": [...] }` where each
17
+ item has the same shape as the single-note `POST /notes` body. The endpoint
18
+ must:
19
+
20
+ 1. Validate that the top-level `notes` field is a non-empty array.
21
+ 2. Validate that every item has a non-empty `title`.
22
+ 3. Only insert any notes if all items are valid (all-or-nothing).
23
+ 4. Return `{ "notes": [...created notes] }` with status 201 on success.
24
+
25
+ ## Affected Files
26
+
27
+ 1. `src/routes/notes.js` — add a new `bulkCreateNotes()` handler.
28
+ 2. `src/server.js` — register `POST /notes/bulk` in the route table **before**
29
+ the existing `POST /notes` entry so the literal path match takes priority.
30
+
31
+ ## Acceptance Criteria
32
+
33
+ - [ ] `POST /notes/bulk` with a valid array creates all notes and returns 201.
34
+ - [ ] If any note is missing `title`, returns `400` and no notes are created.
35
+ - [ ] `POST /notes/bulk` without a token returns `401`.
36
+ - [ ] `POST /notes/bulk` with `{"notes": []}` returns `400`.
37
+ - [ ] `GET /notes` after a successful bulk create shows the new notes.
38
+ - [ ] `npm run smoke` still passes.
39
+
40
+ > This feature requires careful thought about route ordering (the `/bulk`
41
+ > literal path must not be shadowed by `POST /notes$`) and about transaction
42
+ > semantics in the in-memory store.
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: "feat(notes): implement archive/soft-delete with ?archived= list filter"
3
+ labels: ["feature", "priority:P3"]
4
+ difficulty: "hard"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ `DELETE /notes/:id` permanently removes a note. There is no way to soft-delete
10
+ (archive) a note and later restore it or filter archived notes out of the default
11
+ listing. The `archived` field already exists in the data model but is not
12
+ writable or filterable.
13
+
14
+ ## Proposed Solution
15
+
16
+ 1. Add `POST /notes/:id/archive` (protected) — sets `archived: true` on the
17
+ note. Returns the updated note.
18
+ 2. Add `POST /notes/:id/restore` (protected) — sets `archived: false`.
19
+ 3. Extend `GET /notes` to support `?archived=true` (list only archived notes),
20
+ `?archived=false` (list only non-archived notes — new default behavior), and
21
+ no `archived` param (list all, preserving today's behavior).
22
+
23
+ By default (no `?archived=` param) `GET /notes` should still return all notes,
24
+ including archived ones, to avoid a breaking change for the existing smoke test.
25
+
26
+ ## Affected Files
27
+
28
+ 1. `src/routes/notes.js` — add `archiveNote()` and `restoreNote()` handlers;
29
+ extend `listNotes()` to apply an `archived` filter when the param is present.
30
+ 2. `src/server.js` — register the two new routes:
31
+ - `POST /notes/:id/archive`
32
+ - `POST /notes/:id/restore`
33
+
34
+ ## Acceptance Criteria
35
+
36
+ - [ ] `POST /notes/1/archive` (with token) returns `{"note": {..., "archived": true}}`.
37
+ - [ ] `POST /notes/1/restore` (with token) returns `{"note": {..., "archived": false}}`.
38
+ - [ ] `GET /notes?archived=false` omits archived notes.
39
+ - [ ] `GET /notes?archived=true` returns only archived notes.
40
+ - [ ] `GET /notes` (no param) returns all notes regardless of archived status.
41
+ - [ ] Both new routes return `401` without a token and `404` for unknown ids.
42
+ - [ ] `npm run smoke` still passes.
43
+
44
+ > A multi-route feature that touches both handler and server wiring. Good for
45
+ > testing whether the pipeline correctly sequences route registration and
46
+ > confirms no regression on the existing list/create/delete flows.