forgedock 1.1.1 → 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 (171) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +20 -0
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +68 -19
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +80 -8
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/design-bench.md +5 -1
  27. package/commands/design-render-critique-loop.md +1 -1
  28. package/commands/diagnose.md +2 -1
  29. package/commands/explain.md +2 -1
  30. package/commands/forge-stats.md +121 -2
  31. package/commands/forgedock-init.md +1 -1
  32. package/commands/geo-audit.md +3 -0
  33. package/commands/incident-response.md +2 -1
  34. package/commands/issue.md +23 -2
  35. package/commands/milestone.md +77 -12
  36. package/commands/optimize.md +2 -1
  37. package/commands/orchestrate/config.md +67 -0
  38. package/commands/orchestrate/phase-1-resolve.md +159 -0
  39. package/commands/orchestrate/phase-2-triage.md +126 -0
  40. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  41. package/commands/orchestrate/phase-3-dependency.md +591 -0
  42. package/commands/orchestrate/phase-4-execution.md +962 -0
  43. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  44. package/commands/orchestrate/phase-6-report.md +198 -0
  45. package/commands/orchestrate/safety.md +62 -0
  46. package/commands/orchestrate.md +25 -2045
  47. package/commands/pipeline-health/config.md +34 -0
  48. package/commands/pipeline-health/phase-1-context.md +180 -0
  49. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  50. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  51. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  52. package/commands/pipeline-health/phase-5-report.md +284 -0
  53. package/commands/pipeline-health/phase-6-summary.md +52 -0
  54. package/commands/pipeline-health.md +25 -2401
  55. package/commands/pipeline-resume.md +24 -1
  56. package/commands/pipeline-status.md +65 -0
  57. package/commands/qa-sweep.md +4 -1
  58. package/commands/quality-gate.md +248 -10
  59. package/commands/replay.md +2 -1
  60. package/commands/review-pr-agents/api.md +135 -0
  61. package/commands/review-pr-agents/auth.md +137 -0
  62. package/commands/review-pr-agents/billing.md +101 -0
  63. package/commands/review-pr-agents/concurrency.md +145 -0
  64. package/commands/review-pr-agents/database.md +103 -0
  65. package/commands/review-pr-agents/frontend.md +89 -0
  66. package/commands/review-pr-agents/infra.md +385 -0
  67. package/commands/review-pr-agents/protocols.md +185 -0
  68. package/commands/review-pr-agents/scraper.md +185 -0
  69. package/commands/review-pr-agents/security.md +406 -0
  70. package/commands/review-pr-agents.md +45 -1722
  71. package/commands/review-pr-staging.md +196 -40
  72. package/commands/review-pr.md +591 -52
  73. package/commands/rollback.md +2 -1
  74. package/commands/scope.md +2 -1
  75. package/commands/security-audit.md +2 -1
  76. package/commands/signal-planner.md +623 -0
  77. package/commands/sync-ecosystem.md +1 -0
  78. package/commands/test-gate.md +17 -1
  79. package/commands/validate.md +2 -1
  80. package/commands/work-on/build/architect.md +28 -0
  81. package/commands/work-on/build/context.md +2 -0
  82. package/commands/work-on/build/implement.md +3 -1
  83. package/commands/work-on/build/validate.md +26 -7
  84. package/commands/work-on/build.md +163 -24
  85. package/commands/work-on/close.md +130 -3
  86. package/commands/work-on/decompose.md +19 -1
  87. package/commands/work-on/investigate.md +117 -2
  88. package/commands/work-on/review.md +24 -4
  89. package/commands/work-on-monolithic.md +7 -2
  90. package/commands/work-on.md +202 -4
  91. package/docs/BRAND.md +14 -7
  92. package/docs/CI.md +2 -2
  93. package/docs/CODEX.md +36 -1
  94. package/docs/CONFIG.md +5 -8
  95. package/docs/FORGE-PROTOCOL.md +15 -1064
  96. package/docs/anthropic-devrel-outreach.md +2 -2
  97. package/docs/articles/command-decomposition-token-savings.md +214 -0
  98. package/docs/articles/model-release-playbook.md +131 -0
  99. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  100. package/docs/claude-breakpoints.json +75 -5
  101. package/docs/design/abc-benchmark.md +1 -1
  102. package/docs/design/design-spec-schema.md +1 -1
  103. package/docs/design/divergent-generation.md +1 -1
  104. package/docs/design/eval-gate-runbook.md +178 -0
  105. package/docs/design/show-hn-draft.md +26 -1
  106. package/docs/design/token-cost-baseline-1298.md +59 -0
  107. package/docs/eval/README.md +117 -0
  108. package/docs/eval/corpus/seed-v1.json +6 -0
  109. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  110. package/docs/site/annotations-explained.md +24 -0
  111. package/docs/site/command-learning-path.md +51 -14
  112. package/docs/site/command-reference.md +339 -113
  113. package/docs/site/for-companies.md +95 -0
  114. package/docs/site/forge-annotation-protocol.md +12 -256
  115. package/docs/site/getting-started.md +43 -4
  116. package/docs/site/github-is-the-memory.md +113 -0
  117. package/docs/site/how-it-works.md +3 -13
  118. package/docs/site/index.md +9 -1
  119. package/docs/spec/eval-run-result.md +147 -0
  120. package/docs/spec/forge-protocol-v1.md +441 -18
  121. package/docs/spec/label-state-machine.md +55 -0
  122. package/docs/spec/review-protocol.md +158 -0
  123. package/docs/spec-graph-schema.md +28 -0
  124. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  125. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  126. package/examples/forgedock-demo/README.md +41 -22
  127. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  128. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  129. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  130. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  131. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  132. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  133. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  134. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  135. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  136. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  137. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  138. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  139. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  140. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  141. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  142. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  143. package/examples/forgedock-demo/src/db.js +36 -11
  144. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  145. package/examples/forgedock-demo/src/server.js +4 -1
  146. package/package.json +3 -1
  147. package/packages/protocol/LICENSE +21 -0
  148. package/packages/protocol/src/cli.js +135 -0
  149. package/packages/protocol/src/emit.js +146 -0
  150. package/packages/protocol/src/index.js +43 -0
  151. package/packages/protocol/src/parse.js +229 -0
  152. package/packages/protocol/src/types.js +171 -0
  153. package/packages/protocol/src/validate.js +122 -0
  154. package/scripts/bench-topology-cost.mjs +372 -0
  155. package/scripts/check-command-docs-drift.sh +198 -0
  156. package/scripts/check-protocol-restatements.sh +176 -0
  157. package/scripts/check-registry/README.md +72 -0
  158. package/scripts/check-registry/manifest.json +6 -0
  159. package/scripts/code-index.sh +454 -0
  160. package/scripts/conformance-check.mjs +168 -0
  161. package/scripts/doctor-pipeline-state.sh +484 -0
  162. package/scripts/eval-gate-baseline.json +17 -0
  163. package/scripts/eval-gate-scorecard.mjs +242 -0
  164. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  165. package/scripts/flaky-quarantine.sh +241 -0
  166. package/scripts/forge-annotation.sh +567 -0
  167. package/scripts/issue-dedup.sh +236 -0
  168. package/scripts/validate-annotation-node.mjs +233 -0
  169. package/scripts/worktree-lifecycle.sh +3 -3
  170. package/templates/devdocs/agent/using-forgedock.md +14 -0
  171. package/docs/site/.vitepress/config.mjs +0 -85
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: "docs(api): add complete API reference covering all endpoints"
3
+ labels: ["documentation", "priority:P3"]
4
+ difficulty: "easy"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ The demo API now has 9 endpoints (health, list, count, tags, get, create, patch,
10
+ delete, and the bulk/archive/restore endpoints added by later issues) but only
11
+ the original 5 are described in issue #5's `docs/API.md` — and issue #5 may not
12
+ have been run yet. Either way, the current baseline has no `docs/API.md`, or an
13
+ incomplete one.
14
+
15
+ ## Proposed Solution
16
+
17
+ Create (or rewrite) `docs/API.md` documenting all endpoints in the **current
18
+ baseline** (i.e. after the source-code changes in this PR land):
19
+
20
+ | Method | Path | Auth | Description |
21
+ |--------|------|------|-------------|
22
+ | GET | `/health` | no | Liveness check |
23
+ | GET | `/notes` | no | List notes (supports `?where=`, `?tag=`, `?limit=`, `?offset=`) |
24
+ | GET | `/notes/count` | no | Count notes (supports `?owner=`, `?secret=`) |
25
+ | GET | `/notes/tags` | no | List all unique tags |
26
+ | GET | `/notes/:id` | no | Fetch one note |
27
+ | POST | `/notes` | yes | Create a note |
28
+ | PATCH | `/notes/:id` | yes | Update title/body |
29
+ | DELETE | `/notes/:id` | no* | Delete a note |
30
+
31
+ Include a `curl` example for each endpoint, note the intentional flaws inline
32
+ (e.g. `?where=` is injection-vulnerable — see issue #2), and describe the
33
+ `Authorization: Bearer demo-token` header.
34
+
35
+ `*` DELETE is intentionally unprotected — document this as a known flaw (issue #1).
36
+
37
+ ## Affected Files
38
+
39
+ 1. `docs/API.md` (NEW or REWRITE) — full endpoint reference.
40
+ 2. `README.md` — add a "API reference: docs/API.md" link in the "What's inside"
41
+ section if not already present.
42
+
43
+ ## Acceptance Criteria
44
+
45
+ - [ ] `docs/API.md` documents all 8 baseline endpoints with method, path, auth
46
+ requirement, description, and a `curl` example.
47
+ - [ ] The intentional flaws (injection, missing auth on DELETE) are called out
48
+ inline with references to the corresponding issue numbers.
49
+ - [ ] `README.md` links to `docs/API.md`.
50
+ - [ ] No code changes required.
51
+
52
+ > Non-code work that still goes through the full pipeline — investigate the
53
+ > current route surface, write accurate docs, review for completeness.
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: "docs(demo): document the intentional-flaw and graded-issue authoring convention"
3
+ labels: ["documentation", "priority:P3"]
4
+ difficulty: "easy"
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ The demo corpus relies on a deliberate convention — intentional flaws in source
10
+ code, graded issue difficulty, DEMO NOTE comments — but this convention is
11
+ undocumented outside of the source code comments themselves. Someone contributing
12
+ new issues, or using this corpus as a template for their own eval harness, has no
13
+ single reference explaining how the pieces fit together.
14
+
15
+ ## Proposed Solution
16
+
17
+ Create `docs/CORPUS-AUTHORING.md` that explains:
18
+
19
+ 1. **The intentional-flaw pattern** — why flaws are introduced in baseline
20
+ source code and how to mark them with `>>> DEMO NOTE:` comments.
21
+ 2. **The graded-difficulty taxonomy** — the five categories (bug, feature,
22
+ refactor, perf, docs) and the three difficulty tiers (easy, medium, hard),
23
+ with one example per category from this corpus.
24
+ 3. **Issue spec format** — the required YAML frontmatter fields (`title`,
25
+ `labels`, `difficulty`), the required markdown sections (`## Problem`,
26
+ `## Affected Files`, `## Acceptance Criteria`), and the closing `> ...`
27
+ callout paragraph.
28
+ 4. **Independence constraint** — how to verify that a new issue is resolvable
29
+ against the baseline commit without depending on other unmerged issues.
30
+
31
+ ## Affected Files
32
+
33
+ 1. `docs/CORPUS-AUTHORING.md` (NEW).
34
+ 2. `README.md` — add a link to `docs/CORPUS-AUTHORING.md` in a "Contributing
35
+ issues" sub-section.
36
+
37
+ ## Acceptance Criteria
38
+
39
+ - [ ] `docs/CORPUS-AUTHORING.md` covers all four sections listed above.
40
+ - [ ] Each section references at least one existing issue from the corpus as a
41
+ concrete example.
42
+ - [ ] `README.md` links to the new doc.
43
+ - [ ] No code changes required.
44
+
45
+ > A meta-doc that makes the demo self-explaining. Useful for anyone wanting to
46
+ > extend the corpus or use it as a template for their own benchmark.
@@ -48,8 +48,25 @@ async function main() {
48
48
  const unauth = await request(port, 'POST', '/notes', { json: { title: 'nope' } });
49
49
  assert.strictEqual(unauth.status, 401, 'POST without token returns 401');
50
50
 
51
+ const count = await request(port, 'GET', '/notes/count');
52
+ assert.strictEqual(typeof count.body.count, 'number', 'GET /notes/count returns a number');
53
+
54
+ const tags = await request(port, 'GET', '/notes/tags');
55
+ assert.ok(Array.isArray(tags.body.tags), 'GET /notes/tags returns an array');
56
+
57
+ const page = await request(port, 'GET', '/notes?limit=1');
58
+ assert.ok(page.body.notes.length <= 1, 'GET /notes?limit=1 returns at most one note');
59
+
60
+ const noteId = created.body.note.id;
61
+ const patched = await request(port, 'PATCH', `/notes/${noteId}`, {
62
+ token: 'demo-token',
63
+ json: { title: 'smoke (patched)' },
64
+ });
65
+ assert.strictEqual(patched.status, 200, 'PATCH with token returns 200');
66
+ assert.strictEqual(patched.body.note.title, 'smoke (patched)', 'PATCH updates the title');
67
+
51
68
  // eslint-disable-next-line no-console
52
- console.log('SMOKE OK — health, list, create(auth), create(unauth) all pass');
69
+ console.log('SMOKE OK — health, list, create(auth), create(unauth), count, tags, pagination, patch all pass');
53
70
  server.close();
54
71
  }
55
72
 
@@ -10,12 +10,14 @@
10
10
  // >>> It exists only inside examples/forgedock-demo so the review agents have
11
11
  // >>> something to catch. Do not copy this pattern into real code.
12
12
 
13
- let notes = [
14
- { id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false },
15
- { id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true },
16
- { id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false },
13
+ const SEED = [
14
+ { id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false, tags: ['errand'], archived: false, createdAt: '2024-01-01T00:00:00.000Z' },
15
+ { id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true, tags: ['work'], archived: false, createdAt: '2024-01-02T00:00:00.000Z' },
16
+ { id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false, tags: ['fitness', 'errand'], archived: false, createdAt: '2024-01-03T00:00:00.000Z' },
17
17
  ];
18
18
 
19
+ let notes = SEED.map((n) => ({ ...n }));
20
+
19
21
  let nextId = 4;
20
22
 
21
23
  function all() {
@@ -43,7 +45,13 @@ function query(whereClause) {
43
45
  }
44
46
 
45
47
  function insert(note) {
46
- const record = { id: nextId++, secret: false, ...note };
48
+ const record = { id: nextId++, secret: false, tags: [], archived: false, createdAt: new Date().toISOString() };
49
+ // Only override a default when the caller actually supplied the key — a
50
+ // spread of an object containing an explicit `undefined` would otherwise
51
+ // silently clobber the default (e.g. `tags: undefined`).
52
+ for (const [key, value] of Object.entries(note)) {
53
+ if (value !== undefined) record[key] = value;
54
+ }
47
55
  notes.push(record);
48
56
  return record;
49
57
  }
@@ -55,12 +63,29 @@ function remove(id) {
55
63
  }
56
64
 
57
65
  function reset() {
58
- notes = [
59
- { id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false },
60
- { id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true },
61
- { id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false },
62
- ];
66
+ notes = SEED.map((n) => ({ ...n }));
63
67
  nextId = 4;
64
68
  }
65
69
 
66
- module.exports = { all, findById, query, insert, remove, reset };
70
+ // Returns a NEW array sorted by `id` ascending — does not mutate `arr`.
71
+ //
72
+ // >>> DEMO NOTE: bubble sort is an INTENTIONAL, isolated inefficiency (O(n^2))
73
+ // >>> kept here so the review/perf pipeline has a real hot path to optimize.
74
+ // >>> `notes` is already maintained in insertion (id) order, so callers that
75
+ // >>> only need "sorted by id" rarely need this at all — see the perf issue
76
+ // >>> that targets its one caller in routes/notes.js.
77
+ function bubbleSortById(arr) {
78
+ const copy = arr.slice();
79
+ for (let i = 0; i < copy.length; i++) {
80
+ for (let j = 0; j < copy.length - i - 1; j++) {
81
+ if (copy[j].id > copy[j + 1].id) {
82
+ const tmp = copy[j];
83
+ copy[j] = copy[j + 1];
84
+ copy[j + 1] = tmp;
85
+ }
86
+ }
87
+ }
88
+ return copy;
89
+ }
90
+
91
+ module.exports = { all, findById, query, insert, remove, reset, bubbleSortById };
@@ -8,17 +8,80 @@ const db = require('../db');
8
8
  const { requireToken } = require('../auth');
9
9
 
10
10
  // GET /notes
11
- // Lists notes. Supports an optional ?where= filter that is passed straight
12
- // into db.query().
11
+ // Lists notes. Supports:
12
+ // ?where= — raw expression passed straight into db.query() (see db.js)
13
+ // ?tag= — exact match against a note's `tags` array
14
+ // ?limit=, ?offset= — pagination
13
15
  //
14
16
  // >>> DEMO NOTE: forwarding the raw ?where= string into db.query() is the
15
17
  // >>> injection surface (see db.js). Issue #2 asks to make this safe.
16
18
  function listNotes(req, res, params) {
17
- const where = params.query.where;
18
- if (where) {
19
- return { notes: db.query(where) };
19
+ const { where, tag, limit, offset } = params.query;
20
+
21
+ let results = where ? db.query(where) : db.all();
22
+
23
+ if (tag) {
24
+ // >>> DEMO NOTE: `tags` is accepted but never validated as an array at
25
+ // >>> write time (see createNote below). A note created with a
26
+ // >>> malformed `tags` value makes this `.includes()` call throw instead
27
+ // >>> of being skipped safely. INTENTIONAL, isolated flaw.
28
+ results = results.filter((n) => n.tags.includes(tag));
29
+ }
30
+
31
+ if (limit !== undefined || offset !== undefined) {
32
+ // >>> DEMO NOTE: `offset` is not clamped to a non-negative value, so a
33
+ // >>> negative offset falls through to Array.prototype.slice's
34
+ // >>> negative-index semantics (counts from the end) instead of being
35
+ // >>> rejected or normalized to 0. INTENTIONAL, isolated flaw.
36
+ const off = Number(offset) || 0;
37
+ const lim = Number(limit);
38
+ // Sorting by id here is redundant — `results` is already in id order —
39
+ // but the demo intentionally uses the O(n^2) helper anyway (see db.js).
40
+ const sorted = db.bubbleSortById(results);
41
+ results = sorted.slice(off, Number.isFinite(lim) ? off + lim : undefined);
42
+ }
43
+
44
+ return { notes: results };
45
+ }
46
+
47
+ // GET /notes/count
48
+ // Returns { count } for notes matching optional ?owner= / ?secret= filters.
49
+ //
50
+ // >>> DEMO NOTE: builds its where-clause with naive string concatenation and
51
+ // >>> reuses the vulnerable db.query() eval path — the same injection class
52
+ // >>> issue #2 asks you to remove from listNotes, but reintroduced here in a
53
+ // >>> sibling endpoint issue #2's scope doesn't touch. INTENTIONAL, isolated
54
+ // >>> flaw kept for the benchmark corpus.
55
+ function countNotes(req, res, params) {
56
+ const { owner, secret } = params.query;
57
+ const clauses = [];
58
+ if (owner) clauses.push('owner === "' + owner + '"');
59
+ if (secret !== undefined) clauses.push('secret === ' + (secret === 'true'));
60
+ const whereClause = clauses.length ? clauses.join(' && ') : 'true';
61
+ return { count: db.query(whereClause).length };
62
+ }
63
+
64
+ // GET /notes/tags
65
+ // Returns the de-duplicated list of tags across all notes.
66
+ //
67
+ // >>> DEMO NOTE: de-dupes with a nested loop (checks every tag against every
68
+ // >>> unique tag seen so far) instead of a Set — O(n^2) on the total tag
69
+ // >>> count. INTENTIONAL, isolated inefficiency kept for the perf corpus.
70
+ function listTags(req, res, params) {
71
+ const unique = [];
72
+ for (const note of db.all()) {
73
+ for (const t of note.tags || []) {
74
+ let seen = false;
75
+ for (const u of unique) {
76
+ if (u === t) {
77
+ seen = true;
78
+ break;
79
+ }
80
+ }
81
+ if (!seen) unique.push(t);
82
+ }
20
83
  }
21
- return { notes: db.all() };
84
+ return { tags: unique };
22
85
  }
23
86
 
24
87
  // GET /notes/:id
@@ -33,19 +96,53 @@ function getNote(req, res, params) {
33
96
  }
34
97
 
35
98
  // POST /notes (protected)
99
+ //
100
+ // >>> DEMO NOTE: accepts a `tags` field but never checks that it's an array
101
+ // >>> before storing it. A malformed value (e.g. a number) is stored as-is
102
+ // >>> and later crashes GET /notes?tag= (see listNotes). INTENTIONAL,
103
+ // >>> isolated flaw kept for the benchmark corpus.
36
104
  function createNote(req, res, params) {
37
105
  requireToken(req);
38
- const { title, body, owner } = params.body || {};
106
+ const { title, body, owner, tags } = params.body || {};
39
107
  if (!title) {
40
108
  const err = new Error('title is required');
41
109
  err.statusCode = 400;
42
110
  throw err;
43
111
  }
44
- const note = db.insert({ title, body: body || '', owner: owner || 'anonymous' });
112
+ const payload = { title, body: body || '', owner: owner || 'anonymous' };
113
+ if (tags !== undefined) payload.tags = tags;
114
+ const note = db.insert(payload);
45
115
  res.statusCode = 201;
46
116
  return { note };
47
117
  }
48
118
 
119
+ // PATCH /notes/:id (protected)
120
+ //
121
+ // >>> DEMO NOTE: applies `owner` and `secret` from the request body
122
+ // >>> unconditionally alongside `title`/`body`. Any caller holding the
123
+ // >>> shared demo token can reassign a note's ownership or flip its
124
+ // >>> `secret` flag to false, exposing it via the unauthenticated
125
+ // >>> GET /notes listing. This is a mass-assignment / over-permissive-write
126
+ // >>> flaw, distinct from issue #1's missing-auth-check pattern.
127
+ // >>> INTENTIONAL, isolated flaw kept for the benchmark corpus. Note that
128
+ // >>> `tags` is NOT among the fields this handler updates yet — that gap is
129
+ // >>> a separate, not-yet-built feature.
130
+ function updateNote(req, res, params) {
131
+ requireToken(req);
132
+ const note = db.findById(params.id);
133
+ if (!note) {
134
+ const err = new Error('Note not found');
135
+ err.statusCode = 404;
136
+ throw err;
137
+ }
138
+ const { title, body, owner, secret } = params.body || {};
139
+ if (title !== undefined) note.title = title;
140
+ if (body !== undefined) note.body = body;
141
+ if (owner !== undefined) note.owner = owner;
142
+ if (secret !== undefined) note.secret = secret;
143
+ return { note };
144
+ }
145
+
49
146
  // DELETE /notes/:id
50
147
  //
51
148
  // >>> DEMO NOTE: this mutating route is MISSING a requireToken(req) call, so
@@ -61,4 +158,4 @@ function deleteNote(req, res, params) {
61
158
  return { deleted: Number(params.id) };
62
159
  }
63
160
 
64
- module.exports = { listNotes, getNote, createNote, deleteNote };
161
+ module.exports = { listNotes, countNotes, listTags, getNote, createNote, updateNote, deleteNote };
@@ -14,7 +14,10 @@ const PORT = process.env.PORT || 3000;
14
14
  const TABLE = [
15
15
  ['GET', /^\/notes$/, routes.listNotes],
16
16
  ['POST', /^\/notes$/, routes.createNote],
17
+ ['GET', /^\/notes\/count$/, routes.countNotes],
18
+ ['GET', /^\/notes\/tags$/, routes.listTags],
17
19
  ['GET', /^\/notes\/(?<id>\d+)$/, routes.getNote],
20
+ ['PATCH', /^\/notes\/(?<id>\d+)$/, routes.updateNote],
18
21
  ['DELETE', /^\/notes\/(?<id>\d+)$/, routes.deleteNote],
19
22
  ];
20
23
 
@@ -58,7 +61,7 @@ const server = http.createServer(async (req, res) => {
58
61
 
59
62
  const [, pattern, handler] = match;
60
63
  const groups = url.pathname.match(pattern).groups || {};
61
- const body = req.method === 'POST' ? await readBody(req) : {};
64
+ const body = req.method === 'POST' || req.method === 'PATCH' ? await readBody(req) : {};
62
65
 
63
66
  try {
64
67
  const result = handler(req, res, { ...groups, query, body });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -36,7 +36,9 @@
36
36
  "!bin/tests/",
37
37
  "commands/",
38
38
  "docs/",
39
+ "!docs/site/.vitepress/",
39
40
  "examples/",
41
+ "packages/protocol/src/",
40
42
  "scripts/",
41
43
  "templates/",
42
44
  "LICENSE",
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RapierCraft Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge-protocol-conformance — FORGE Annotation Protocol conformance test runner.
4
+ *
5
+ * Usage:
6
+ * forge-protocol-conformance [fixtures-dir]
7
+ * node src/cli.js [fixtures-dir]
8
+ *
9
+ * Reads fixtures from the given directory (default: ./fixtures/).
10
+ * Each fixture is a JSON file with:
11
+ * {
12
+ * "description": "human-readable test name",
13
+ * "input": "comment body string to parse",
14
+ * "expect": {
15
+ * "type": "INVESTIGATOR",
16
+ * "valid": true,
17
+ * "sentinelState": "complete",
18
+ * "inlineValue": null,
19
+ * "fieldChecks": { "Verdict": "CONFIRMED" } // optional
20
+ * }
21
+ * }
22
+ *
23
+ * Exit code: 0 if all fixtures pass, 1 if any fail.
24
+ *
25
+ * @license MIT
26
+ */
27
+
28
+ import { readFileSync, readdirSync } from 'node:fs';
29
+ import { resolve, extname } from 'node:path';
30
+ import { parse } from './parse.js';
31
+ import { validate } from './validate.js';
32
+
33
+ const fixturesDir = resolve(process.argv[2] ?? 'fixtures');
34
+
35
+ let passed = 0;
36
+ let failed = 0;
37
+ const failures = [];
38
+
39
+ let files;
40
+ try {
41
+ files = readdirSync(fixturesDir).filter(f => extname(f) === '.json').sort();
42
+ } catch (err) {
43
+ console.error(`Cannot read fixtures directory: ${fixturesDir}`);
44
+ console.error(err.message);
45
+ process.exit(1);
46
+ }
47
+
48
+ if (files.length === 0) {
49
+ console.error(`No .json fixtures found in ${fixturesDir}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ for (const file of files) {
54
+ const fixturePath = resolve(fixturesDir, file);
55
+ let fixture;
56
+ try {
57
+ fixture = JSON.parse(readFileSync(fixturePath, 'utf8'));
58
+ } catch (err) {
59
+ console.error(`SKIP ${file} — cannot parse JSON: ${err.message}`);
60
+ continue;
61
+ }
62
+
63
+ const { description, input, expect: expected } = fixture;
64
+ const label = `${file}: ${description ?? '(no description)'}`;
65
+
66
+ if (typeof input !== 'string') {
67
+ console.error(`SKIP ${label} — "input" must be a string`);
68
+ continue;
69
+ }
70
+
71
+ const annotations = parse(input);
72
+
73
+ // Find the annotation matching the expected type
74
+ const ann = annotations.find(a => a.type === expected.type);
75
+
76
+ const fixtureErrors = [];
77
+
78
+ if (!ann) {
79
+ fixtureErrors.push(`Expected annotation type "${expected.type}" not found in parsed output`);
80
+ } else {
81
+ // Sentinel state check
82
+ if (expected.sentinelState !== undefined && ann.sentinelState !== expected.sentinelState) {
83
+ fixtureErrors.push(
84
+ `sentinelState: expected "${expected.sentinelState}", got "${ann.sentinelState}"`,
85
+ );
86
+ }
87
+
88
+ // Inline value check
89
+ if ('inlineValue' in expected) {
90
+ if (ann.inlineValue !== expected.inlineValue) {
91
+ fixtureErrors.push(
92
+ `inlineValue: expected ${JSON.stringify(expected.inlineValue)}, got ${JSON.stringify(ann.inlineValue)}`,
93
+ );
94
+ }
95
+ }
96
+
97
+ // Field checks
98
+ if (expected.fieldChecks) {
99
+ for (const [key, val] of Object.entries(expected.fieldChecks)) {
100
+ if (ann.fields[key] !== val) {
101
+ fixtureErrors.push(
102
+ `field "${key}": expected "${val}", got "${ann.fields[key] ?? '(missing)'}"`,
103
+ );
104
+ }
105
+ }
106
+ }
107
+
108
+ // validate() result check
109
+ const { valid, errors } = validate(ann);
110
+ if (expected.valid === true && !valid) {
111
+ fixtureErrors.push(`Expected valid=true but got errors: ${errors.join('; ')}`);
112
+ } else if (expected.valid === false && valid) {
113
+ fixtureErrors.push(`Expected valid=false but annotation passed validation`);
114
+ }
115
+ }
116
+
117
+ if (fixtureErrors.length === 0) {
118
+ console.log(`ok ${label}`);
119
+ passed++;
120
+ } else {
121
+ console.log(`FAIL ${label}`);
122
+ for (const e of fixtureErrors) {
123
+ console.log(` ${e}`);
124
+ }
125
+ failures.push(label);
126
+ failed++;
127
+ }
128
+ }
129
+
130
+ console.log('');
131
+ console.log(`Results: ${passed} passed, ${failed} failed (${files.length} fixtures total)`);
132
+
133
+ if (failed > 0) {
134
+ process.exit(1);
135
+ }