forgedock 1.0.21 → 1.1.1

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 (76) hide show
  1. package/README.md +227 -174
  2. package/bin/cinema.mjs +320 -0
  3. package/bin/demo.mjs +386 -0
  4. package/bin/engine/phases.mjs +148 -0
  5. package/bin/engine/projector.mjs +32 -0
  6. package/bin/engine/reconcile.mjs +11 -0
  7. package/bin/engine/runlog.mjs +79 -0
  8. package/bin/engine/state.mjs +27 -0
  9. package/bin/engine-cli.mjs +50 -0
  10. package/bin/engine.mjs +138 -0
  11. package/bin/forgedock.mjs +693 -1393
  12. package/bin/init-detect.mjs +95 -0
  13. package/bin/journey.mjs +783 -0
  14. package/bin/runner.mjs +847 -0
  15. package/bin/settings-hook.mjs +112 -0
  16. package/bin/tui.mjs +6 -3
  17. package/commands/autopilot.md +4 -4
  18. package/commands/cleanup.md +85 -39
  19. package/commands/deploy-info.md +11 -5
  20. package/commands/design-bench.md +1 -0
  21. package/commands/design-render-critique-loop.md +1 -0
  22. package/commands/design.md +1 -0
  23. package/commands/forge-stats.md +2 -1
  24. package/commands/issue.md +1 -1
  25. package/commands/optimize.md +1 -1
  26. package/commands/orchestrate.md +346 -34
  27. package/commands/quality-gate.md +168 -80
  28. package/commands/review-pr-agents.md +69 -45
  29. package/commands/review-pr.md +54 -6
  30. package/commands/scope.md +11 -3
  31. package/commands/security-audit.md +24 -16
  32. package/commands/sync-ecosystem.md +2 -2
  33. package/commands/work-on/build/architect.md +47 -3
  34. package/commands/work-on/build/context.md +36 -5
  35. package/commands/work-on/build/implement.md +16 -4
  36. package/commands/work-on/build/validate.md +54 -8
  37. package/commands/work-on/build.md +27 -9
  38. package/commands/work-on/close.md +131 -3
  39. package/commands/work-on/investigate.md +24 -1
  40. package/commands/work-on/review.md +24 -0
  41. package/commands/work-on-monolithic.md +1 -0
  42. package/commands/work-on.md +271 -11
  43. package/docs/BRAND.md +6 -2
  44. package/docs/CONFIG.md +39 -34
  45. package/docs/site/.vitepress/config.mjs +85 -0
  46. package/docs/site/annotations-explained.md +55 -0
  47. package/docs/site/command-learning-path.md +104 -0
  48. package/docs/site/command-reference.md +3 -0
  49. package/docs/site/getting-started.md +42 -13
  50. package/docs/site/index.md +1 -1
  51. package/docs/site/troubleshooting.md +208 -0
  52. package/docs/spec/forge-protocol-v1.md +768 -0
  53. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  54. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  55. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  56. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  57. package/examples/forgedock-demo/README.md +113 -0
  58. package/examples/forgedock-demo/bootstrap.sh +125 -0
  59. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  60. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  61. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  62. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  63. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  64. package/examples/forgedock-demo/labels.json +21 -0
  65. package/examples/forgedock-demo/package.json +16 -0
  66. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  67. package/examples/forgedock-demo/src/auth.js +28 -0
  68. package/examples/forgedock-demo/src/db.js +66 -0
  69. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  70. package/examples/forgedock-demo/src/server.js +79 -0
  71. package/package.json +13 -3
  72. package/scripts/verify-env-vars.sh +18 -2
  73. package/scripts/verify-host-headers.sh +4 -4
  74. package/scripts/verify-route-registration.sh +2 -2
  75. package/scripts/worktree-lifecycle.sh +202 -0
  76. package/templates/forge.yaml.minimal +44 -0
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "feat(notes): add safe filtering for GET /notes"
3
+ labels: ["feature", "security", "priority:P2"]
4
+ ---
5
+
6
+ ## Problem
7
+
8
+ `GET /notes?where=...` forwards the raw `where` string straight into
9
+ `db.query()`, which evaluates it as a JavaScript expression. That is an
10
+ injection hole (a caller can pass `?where=true||secret` to leak secret notes).
11
+ We want a **safe** way to filter notes instead.
12
+
13
+ ## Proposed Solution
14
+
15
+ Replace the free-form `?where=` evaluation with explicit, allow-listed query
16
+ params:
17
+
18
+ - `?owner=alice` — exact match on `owner`
19
+ - `?secret=false` — match on the `secret` boolean
20
+
21
+ Build the filter from these typed params in code — never by evaluating a string.
22
+
23
+ ## Affected Files
24
+
25
+ 1. `src/routes/notes.js` — `listNotes()` should read `owner` / `secret` params.
26
+ 2. `src/db.js` — replace `query(whereClause)` with a structured filter helper
27
+ (e.g. `filter({ owner, secret })`) that does not evaluate strings.
28
+
29
+ ## Acceptance Criteria
30
+
31
+ - [ ] `GET /notes?owner=alice` returns only alice's notes.
32
+ - [ ] `GET /notes?secret=false` returns only non-secret notes.
33
+ - [ ] The string-evaluating `db.query()` path is removed.
34
+ - [ ] `npm run smoke` still passes.
35
+
36
+ > This issue exercises the **investigation + architecture** phases — the fix
37
+ > touches two files and removes an unsafe code path.
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: "refactor(server): extract the router into its own module"
3
+ labels: ["refactor", "priority:P3"]
4
+ ---
5
+
6
+ ## Problem
7
+
8
+ `src/server.js` mixes three concerns in one file: the route table, request
9
+ body parsing, and the HTTP server wiring. As more routes are added this file
10
+ will become hard to read and test.
11
+
12
+ ## Proposed Solution
13
+
14
+ Extract the routing logic into a dedicated `src/router.js` module that exports
15
+ a `route(req, res)` function (or a small `Router` object). `server.js` should
16
+ shrink to: create the HTTP server, delegate to the router, and listen.
17
+
18
+ This is a pure refactor — no behavior changes.
19
+
20
+ ## Affected Files
21
+
22
+ 1. `src/server.js` — remove the inline `TABLE` + matching logic.
23
+ 2. `src/router.js` (NEW) — owns the route table and dispatch.
24
+
25
+ ## Acceptance Criteria
26
+
27
+ - [ ] Route table and dispatch live in `src/router.js`.
28
+ - [ ] `server.js` only creates the server and listens.
29
+ - [ ] All endpoints behave exactly as before.
30
+ - [ ] `npm run smoke` still passes.
31
+
32
+ > A clean refactor with zero behavior change — a good way to see how the review
33
+ > agents confirm "no regression".
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: "perf(db): make findById O(1) instead of a linear scan"
3
+ labels: ["performance", "priority:P3"]
4
+ ---
5
+
6
+ ## Problem
7
+
8
+ `db.findById(id)` and `db.remove(id)` both do a linear `Array.find` /
9
+ `Array.filter` over every note on each call. With the seed data that is fine,
10
+ but it is O(n) per lookup and will not scale as the note count grows.
11
+
12
+ ## Investigation Hint
13
+
14
+ Before optimizing, confirm where the linear scans actually are and whether an
15
+ index can stay consistent across `insert`, `remove`, and `reset`. The point of
16
+ this issue is to show ForgeDock's **investigation phase** tracing the real hot
17
+ path rather than guessing.
18
+
19
+ ## Proposed Solution
20
+
21
+ Maintain an `id -> note` index (a `Map`) alongside the array so `findById` is
22
+ O(1). Keep the index in sync in `insert`, `remove`, and `reset`.
23
+
24
+ ## Affected Files
25
+
26
+ 1. `src/db.js` — add and maintain a `Map` index; use it in `findById`.
27
+
28
+ ## Acceptance Criteria
29
+
30
+ - [ ] `findById` uses an O(1) Map lookup.
31
+ - [ ] The index stays consistent after `insert`, `remove`, and `reset`.
32
+ - [ ] `npm run smoke` still passes.
33
+
34
+ > This issue showcases the **investigation phase** — the agent verifies the
35
+ > claim against the code before writing the optimization.
@@ -0,0 +1,39 @@
1
+ ---
2
+ title: "docs(api): add an API reference for the Notes endpoints"
3
+ labels: ["documentation", "priority:P3"]
4
+ ---
5
+
6
+ ## Problem
7
+
8
+ The demo API has no endpoint documentation. A newcomer has to read `server.js`
9
+ and `routes/notes.js` to learn what routes exist, which ones need a token, and
10
+ what they return.
11
+
12
+ ## Proposed Solution
13
+
14
+ Add `docs/API.md` documenting every endpoint:
15
+
16
+ | Method | Path | Auth | Description |
17
+ |--------|------|------|-------------|
18
+ | GET | `/health` | no | Liveness check |
19
+ | GET | `/notes` | no | List notes (supports filtering) |
20
+ | GET | `/notes/:id` | no | Fetch one note |
21
+ | POST | `/notes` | yes | Create a note |
22
+ | DELETE | `/notes/:id` | yes | Delete a note |
23
+
24
+ Include a `curl` example for each and note the `Authorization: Bearer demo-token`
25
+ header for protected routes.
26
+
27
+ ## Affected Files
28
+
29
+ 1. `docs/API.md` (NEW) — endpoint reference.
30
+ 2. `README.md` — link to the new API reference.
31
+
32
+ ## Acceptance Criteria
33
+
34
+ - [ ] `docs/API.md` documents all 5 endpoints with auth + examples.
35
+ - [ ] README links to it.
36
+ - [ ] No code changes required.
37
+
38
+ > This issue proves the pipeline handles **non-code work** — docs-only changes
39
+ > still go through investigate → build → review → merge.
@@ -0,0 +1,21 @@
1
+ [
2
+ { "name": "priority:P0", "color": "B60205", "description": "Critical priority — production broken, data loss, security breach." },
3
+ { "name": "priority:P1", "color": "D93F0B", "description": "High priority — major feature broken, no workaround." },
4
+ { "name": "priority:P2", "color": "FBCA04", "description": "Medium priority — impaired functionality, workaround exists." },
5
+ { "name": "priority:P3", "color": "C2E0C6", "description": "Low priority — minor issue or polish." },
6
+ { "name": "workflow:investigating", "color": "1D76DB", "description": "Pipeline: investigation phase in progress." },
7
+ { "name": "workflow:ready-to-build", "color": "0075CA", "description": "Pipeline: investigation complete, ready for build." },
8
+ { "name": "workflow:building", "color": "0052CC", "description": "Pipeline: implementation in progress." },
9
+ { "name": "workflow:in-review", "color": "5319E7", "description": "Pipeline: PR created, under review." },
10
+ { "name": "workflow:merged", "color": "0E8A16", "description": "Pipeline: PR merged, issue closed." },
11
+ { "name": "workflow:decomposed", "color": "BFD4F2", "description": "Pipeline: decomposed into sub-issues." },
12
+ { "name": "workflow:invalid", "color": "CCCCCC", "description": "Pipeline: issue closed as invalid after investigation." },
13
+ { "name": "needs-human", "color": "E4E669", "description": "Pipeline stalled — requires human intervention." },
14
+ { "name": "review-finding", "color": "D93F0B", "description": "Defect or improvement found during automated PR review." },
15
+ { "name": "bug", "color": "D73A4A", "description": "Something isn't working." },
16
+ { "name": "feature", "color": "0075CA", "description": "New feature implementation." },
17
+ { "name": "refactor", "color": "FEF2C0", "description": "Code restructuring without behavior change." },
18
+ { "name": "performance", "color": "F9D0C4", "description": "Performance optimization or regression." },
19
+ { "name": "documentation", "color": "0075CA", "description": "Documentation addition or update." },
20
+ { "name": "security", "color": "B60205", "description": "Security vulnerability or hardening." }
21
+ ]
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "forgedock-demo",
3
+ "version": "1.0.0",
4
+ "description": "A tiny, dependency-free Notes API used as a risk-free playground for trying ForgeDock.",
5
+ "type": "commonjs",
6
+ "main": "src/server.js",
7
+ "scripts": {
8
+ "start": "node src/server.js",
9
+ "smoke": "node scripts/smoke.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "license": "MIT",
15
+ "private": true
16
+ }
@@ -0,0 +1,61 @@
1
+ // Tiny smoke test for the demo Notes API. No test framework, no deps.
2
+ // Starts the server on a random port, hits a few endpoints, and asserts.
3
+ // Run with: npm run smoke
4
+
5
+ const http = require('http');
6
+ const assert = require('assert');
7
+ const { server } = require('../src/server');
8
+
9
+ function request(port, method, path, { token, json } = {}) {
10
+ return new Promise((resolve, reject) => {
11
+ const headers = {};
12
+ if (token) headers['Authorization'] = `Bearer ${token}`;
13
+ let payload;
14
+ if (json) {
15
+ payload = JSON.stringify(json);
16
+ headers['Content-Type'] = 'application/json';
17
+ headers['Content-Length'] = Buffer.byteLength(payload);
18
+ }
19
+ const req = http.request({ host: '127.0.0.1', port, method, path, headers }, (res) => {
20
+ let raw = '';
21
+ res.on('data', (c) => (raw += c));
22
+ res.on('end', () => resolve({ status: res.statusCode, body: raw ? JSON.parse(raw) : {} }));
23
+ });
24
+ req.on('error', reject);
25
+ if (payload) req.write(payload);
26
+ req.end();
27
+ });
28
+ }
29
+
30
+ async function main() {
31
+ await new Promise((r) => server.listen(0, r));
32
+ const { port } = server.address();
33
+
34
+ const health = await request(port, 'GET', '/health');
35
+ assert.strictEqual(health.status, 200, 'health should be 200');
36
+ assert.strictEqual(health.body.status, 'ok', 'health status should be ok');
37
+
38
+ const list = await request(port, 'GET', '/notes');
39
+ assert.ok(Array.isArray(list.body.notes), 'GET /notes returns an array');
40
+ assert.ok(list.body.notes.length >= 3, 'seed notes present');
41
+
42
+ const created = await request(port, 'POST', '/notes', {
43
+ token: 'demo-token',
44
+ json: { title: 'smoke', owner: 'tester' },
45
+ });
46
+ assert.strictEqual(created.status, 201, 'POST with token returns 201');
47
+
48
+ const unauth = await request(port, 'POST', '/notes', { json: { title: 'nope' } });
49
+ assert.strictEqual(unauth.status, 401, 'POST without token returns 401');
50
+
51
+ // eslint-disable-next-line no-console
52
+ console.log('SMOKE OK — health, list, create(auth), create(unauth) all pass');
53
+ server.close();
54
+ }
55
+
56
+ main().catch((err) => {
57
+ // eslint-disable-next-line no-console
58
+ console.error('SMOKE FAILED:', err.message);
59
+ server.close();
60
+ process.exit(1);
61
+ });
@@ -0,0 +1,28 @@
1
+ // Minimal token-based auth for the demo Notes API.
2
+ //
3
+ // The "token" is just a static demo secret — there are no real users or
4
+ // sessions. Routes that should be protected call `requireToken(req)`.
5
+ //
6
+ // >>> DEMO NOTE: In routes/notes.js, one mutating route deliberately does NOT
7
+ // >>> call requireToken(). That missing-auth check is an INTENTIONAL flaw so
8
+ // >>> ForgeDock's review agents have an authorization gap to flag.
9
+
10
+ const DEMO_TOKEN = 'demo-token';
11
+
12
+ // Returns true when the request carries the demo bearer token.
13
+ function isAuthenticated(req) {
14
+ const header = req.headers['authorization'] || '';
15
+ const [scheme, value] = header.split(' ');
16
+ return scheme === 'Bearer' && value === DEMO_TOKEN;
17
+ }
18
+
19
+ // Throws a 401-style error when the request is not authenticated.
20
+ function requireToken(req) {
21
+ if (!isAuthenticated(req)) {
22
+ const err = new Error('Unauthorized: missing or invalid bearer token');
23
+ err.statusCode = 401;
24
+ throw err;
25
+ }
26
+ }
27
+
28
+ module.exports = { isAuthenticated, requireToken, DEMO_TOKEN };
@@ -0,0 +1,66 @@
1
+ // In-memory data store for the demo Notes API.
2
+ //
3
+ // This is intentionally a plain array plus a deliberately naive query helper.
4
+ // There is no real database — `query()` simulates a SQL-style WHERE clause by
5
+ // building a predicate from a raw string. This mirrors the classic SQL-injection
6
+ // pattern (untrusted input concatenated into a query) so that ForgeDock's
7
+ // /review-pr security agent has a realistic finding to surface.
8
+ //
9
+ // >>> DEMO NOTE: The string-built filter below is an INTENTIONAL, isolated flaw.
10
+ // >>> It exists only inside examples/forgedock-demo so the review agents have
11
+ // >>> something to catch. Do not copy this pattern into real code.
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 },
17
+ ];
18
+
19
+ let nextId = 4;
20
+
21
+ function all() {
22
+ return notes;
23
+ }
24
+
25
+ function findById(id) {
26
+ return notes.find((n) => n.id === Number(id));
27
+ }
28
+
29
+ // Simulated SQL query. `whereClause` is a raw expression like "owner == 'alice'".
30
+ // INTENTIONAL FLAW: the clause is evaluated directly, so a caller can inject
31
+ // arbitrary expressions (e.g. "true || secret"). A real fix would use a
32
+ // parameterized filter instead of evaluating untrusted strings.
33
+ function query(whereClause) {
34
+ // eslint-disable-next-line no-new-func
35
+ const predicate = new Function('row', `with (row) { return (${whereClause}); }`);
36
+ return notes.filter((row) => {
37
+ try {
38
+ return predicate(row);
39
+ } catch {
40
+ return false;
41
+ }
42
+ });
43
+ }
44
+
45
+ function insert(note) {
46
+ const record = { id: nextId++, secret: false, ...note };
47
+ notes.push(record);
48
+ return record;
49
+ }
50
+
51
+ function remove(id) {
52
+ const before = notes.length;
53
+ notes = notes.filter((n) => n.id !== Number(id));
54
+ return notes.length < before;
55
+ }
56
+
57
+ 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
+ ];
63
+ nextId = 4;
64
+ }
65
+
66
+ module.exports = { all, findById, query, insert, remove, reset };
@@ -0,0 +1,64 @@
1
+ // Route handlers for the demo Notes API.
2
+ //
3
+ // Each handler takes (req, res, params) and returns a plain object that the
4
+ // server serializes to JSON. Handlers throw errors with a `statusCode` to
5
+ // signal HTTP error responses.
6
+
7
+ const db = require('../db');
8
+ const { requireToken } = require('../auth');
9
+
10
+ // GET /notes
11
+ // Lists notes. Supports an optional ?where= filter that is passed straight
12
+ // into db.query().
13
+ //
14
+ // >>> DEMO NOTE: forwarding the raw ?where= string into db.query() is the
15
+ // >>> injection surface (see db.js). Issue #2 asks to make this safe.
16
+ function listNotes(req, res, params) {
17
+ const where = params.query.where;
18
+ if (where) {
19
+ return { notes: db.query(where) };
20
+ }
21
+ return { notes: db.all() };
22
+ }
23
+
24
+ // GET /notes/:id
25
+ function getNote(req, res, params) {
26
+ const note = db.findById(params.id);
27
+ if (!note) {
28
+ const err = new Error('Note not found');
29
+ err.statusCode = 404;
30
+ throw err;
31
+ }
32
+ return { note };
33
+ }
34
+
35
+ // POST /notes (protected)
36
+ function createNote(req, res, params) {
37
+ requireToken(req);
38
+ const { title, body, owner } = params.body || {};
39
+ if (!title) {
40
+ const err = new Error('title is required');
41
+ err.statusCode = 400;
42
+ throw err;
43
+ }
44
+ const note = db.insert({ title, body: body || '', owner: owner || 'anonymous' });
45
+ res.statusCode = 201;
46
+ return { note };
47
+ }
48
+
49
+ // DELETE /notes/:id
50
+ //
51
+ // >>> DEMO NOTE: this mutating route is MISSING a requireToken(req) call, so
52
+ // >>> anyone can delete any note. That missing authorization check is the
53
+ // >>> INTENTIONAL flaw that Issue #1 (bug fix) asks you to close.
54
+ function deleteNote(req, res, params) {
55
+ const ok = db.remove(params.id);
56
+ if (!ok) {
57
+ const err = new Error('Note not found');
58
+ err.statusCode = 404;
59
+ throw err;
60
+ }
61
+ return { deleted: Number(params.id) };
62
+ }
63
+
64
+ module.exports = { listNotes, getNote, createNote, deleteNote };
@@ -0,0 +1,79 @@
1
+ // Entry point for the demo Notes API.
2
+ //
3
+ // Uses only Node's built-in `http` module — no Express, no npm install. Run it
4
+ // with `node src/server.js` (or `npm start`). The router below is intentionally
5
+ // small and hand-rolled so the whole codebase stays readable in a few minutes.
6
+
7
+ const http = require('http');
8
+ const { URL } = require('url');
9
+ const routes = require('./routes/notes');
10
+
11
+ const PORT = process.env.PORT || 3000;
12
+
13
+ // Route table: [method, pathRegex, handler]. A `:id` segment is captured.
14
+ const TABLE = [
15
+ ['GET', /^\/notes$/, routes.listNotes],
16
+ ['POST', /^\/notes$/, routes.createNote],
17
+ ['GET', /^\/notes\/(?<id>\d+)$/, routes.getNote],
18
+ ['DELETE', /^\/notes\/(?<id>\d+)$/, routes.deleteNote],
19
+ ];
20
+
21
+ function readBody(req) {
22
+ return new Promise((resolve) => {
23
+ let raw = '';
24
+ req.on('data', (chunk) => {
25
+ raw += chunk;
26
+ });
27
+ req.on('end', () => {
28
+ if (!raw) return resolve({});
29
+ try {
30
+ resolve(JSON.parse(raw));
31
+ } catch {
32
+ resolve({});
33
+ }
34
+ });
35
+ });
36
+ }
37
+
38
+ const server = http.createServer(async (req, res) => {
39
+ res.setHeader('Content-Type', 'application/json');
40
+
41
+ // Health check.
42
+ if (req.method === 'GET' && req.url === '/health') {
43
+ res.statusCode = 200;
44
+ return res.end(JSON.stringify({ status: 'ok' }));
45
+ }
46
+
47
+ const url = new URL(req.url, `http://localhost:${PORT}`);
48
+ const query = Object.fromEntries(url.searchParams.entries());
49
+
50
+ const match = TABLE.find(
51
+ ([method, pattern]) => method === req.method && pattern.test(url.pathname)
52
+ );
53
+
54
+ if (!match) {
55
+ res.statusCode = 404;
56
+ return res.end(JSON.stringify({ error: 'Not found' }));
57
+ }
58
+
59
+ const [, pattern, handler] = match;
60
+ const groups = url.pathname.match(pattern).groups || {};
61
+ const body = req.method === 'POST' ? await readBody(req) : {};
62
+
63
+ try {
64
+ const result = handler(req, res, { ...groups, query, body });
65
+ return res.end(JSON.stringify(result));
66
+ } catch (err) {
67
+ res.statusCode = err.statusCode || 500;
68
+ return res.end(JSON.stringify({ error: err.message }));
69
+ }
70
+ });
71
+
72
+ if (require.main === module) {
73
+ server.listen(PORT, () => {
74
+ // eslint-disable-next-line no-console
75
+ console.log(`Notes API listening on http://localhost:${PORT}`);
76
+ });
77
+ }
78
+
79
+ module.exports = { server };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.0.21",
3
+ "version": "1.1.1",
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"
@@ -27,7 +27,7 @@
27
27
  "type": "git",
28
28
  "url": "git+https://github.com/RapierCraftStudios/ForgeDock.git"
29
29
  },
30
- "homepage": "https://forgedock.dev",
30
+ "homepage": "https://github.com/RapierCraftStudios/ForgeDock#readme",
31
31
  "bugs": {
32
32
  "url": "https://github.com/RapierCraftStudios/ForgeDock/issues"
33
33
  },
@@ -36,17 +36,27 @@
36
36
  "!bin/tests/",
37
37
  "commands/",
38
38
  "docs/",
39
+ "examples/",
39
40
  "scripts/",
40
41
  "templates/",
41
42
  "LICENSE",
42
43
  "README.md"
43
44
  ],
44
45
  "scripts": {
45
- "test": "node --test \"bin/tests/**/*.test.mjs\""
46
+ "test": "node --test \"bin/tests/**/*.test.mjs\"",
47
+ "docs:dev": "vitepress dev docs/site",
48
+ "docs:build": "vitepress build docs/site",
49
+ "docs:preview": "vitepress preview docs/site"
46
50
  },
47
51
  "engines": {
48
52
  "node": ">=18"
49
53
  },
54
+ "optionalDependencies": {
55
+ "@anthropic-ai/sdk": ">=0.30.0 <1"
56
+ },
57
+ "devDependencies": {
58
+ "vitepress": "^1.6.3"
59
+ },
50
60
  "peerDependencies": {
51
61
  "@anthropic-ai/claude-code": ">=2.0.0"
52
62
  },
@@ -179,8 +179,24 @@ for var in $VAR_NAMES; do
179
179
  fi
180
180
 
181
181
  if [ -z "$FOUND_IN" ]; then
182
- echo "BLOCKING: $var — not found in .env.example, docker-compose.yml, or env_validation.py"
183
- BLOCKING=$((BLOCKING + 1))
182
+ # Determine whether any of the optional surfaces exist in this repo.
183
+ # If NONE of the known surfaces exist (not even .env.example), the repo
184
+ # uses a layout we can't validate — emit SKIP instead of a false BLOCKING.
185
+ # This is the verify-sops-chain.sh pattern: unknown layout → graceful skip,
186
+ # not a spurious failure. <!-- Added: forge#1349 -->
187
+ HAS_ANY_SURFACE=0
188
+ [ -f "$REPO_ROOT/.env.example" ] && HAS_ANY_SURFACE=1
189
+ [ -f "$REPO_ROOT/docker-compose.yml" ] && HAS_ANY_SURFACE=1
190
+ [ -f "$REPO_ROOT/docker-compose.prod.yml" ] && HAS_ANY_SURFACE=1
191
+ [ -f "$REPO_ROOT/services/api/app/core/env_validation.py" ] && HAS_ANY_SURFACE=1
192
+ [ -f "$REPO_ROOT/scripts/decrypt-secrets.sh" ] && HAS_ANY_SURFACE=1
193
+
194
+ if [ "$HAS_ANY_SURFACE" -eq 0 ]; then
195
+ echo "SKIP: $var — no known env-wiring surfaces found in repo (no .env.example, docker-compose, env_validation.py, or SOPS mapping). Configure verification.env_check_surfaces in forge.yaml or add .env.example to enable this check."
196
+ else
197
+ echo "BLOCKING: $var — not found in any detected env-wiring surface (.env.example, docker-compose.yml, env_validation.py, or decrypt-secrets.sh)"
198
+ BLOCKING=$((BLOCKING + 1))
199
+ fi
184
200
  else
185
201
  echo "OK: $var — found in:$FOUND_IN"
186
202
  fi
@@ -67,9 +67,9 @@ while read -r f; do
67
67
  elif echo "$CONTENT" | grep -qE "\-\-header\s+[\"']Host:"; then
68
68
  echo "OK: $f:$LINENO — Host header explicitly set (--header)"
69
69
  else
70
- echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. TrustedHostMiddleware will reject raw IP as Host value."
70
+ echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. Host-validation middleware will reject a raw IP as the Host value."
71
71
  echo " Line: $CONTENT"
72
- echo " Fix: Add -H 'Host: localhost' (or appropriate allowed host from services/api/app/main.py allowed_hosts)"
72
+ echo " Fix: Add -H 'Host: <allowed-hostname>' — check your service's allowed_hosts configuration for the correct value"
73
73
  BLOCKING=$((BLOCKING + 1))
74
74
  fi
75
75
  done < <(echo "$CURL_LINES")
@@ -96,11 +96,11 @@ if [ -n "$FRONTEND_FILES" ]; then
96
96
  fi
97
97
 
98
98
  if [ -n "$DIRECT_CALLS" ]; then
99
- echo "BLOCKING: $f — direct /api/v1/ call bypasses Next.js proxy (auth will fail)"
99
+ echo "BLOCKING: $f — direct /api/v1/ call bypasses frontend proxy (auth middleware will be skipped)"
100
100
  while IFS= read -r match; do
101
101
  echo " $match"
102
102
  done < <(echo "$DIRECT_CALLS")
103
- echo " Fix: Use /api/* proxy routes instead of /api/v1/* direct FastAPI calls"
103
+ echo " Fix: Use the frontend proxy route (e.g. /api/*) instead of calling the backend API directly"
104
104
  BLOCKING=$((BLOCKING + 1))
105
105
  else
106
106
  echo "OK: $f — no direct backend calls"
@@ -5,7 +5,7 @@
5
5
  # changed_files_file: path to a file listing changed files (one per line)
6
6
  # repo_root: path to the repository root
7
7
  #
8
- # Layout path overrides (env vars — override defaults for non-AlterLab layouts):
8
+ # Layout path overrides (env vars — override defaults for project-specific layouts):
9
9
  # FORGE_PAGES_ROOT Root of Next.js App Router pages dir (default: web/src/app)
10
10
  # FORGE_API_ROUTERS_DIR Dir containing FastAPI router .py files (default: services/api/app/routers)
11
11
  # FORGE_API_MAIN Path to API app entrypoint, relative to repo root (default: services/api/app/main.py)
@@ -35,7 +35,7 @@ else
35
35
  FILES=$(cat "$CHANGED_FILES")
36
36
  fi
37
37
 
38
- # Layout path configuration — read from env vars, fall back to AlterLab defaults
38
+ # Layout path configuration — read from env vars, fall back to project defaults
39
39
  FORGE_PAGES_ROOT="${FORGE_PAGES_ROOT:-web/src/app}"
40
40
  FORGE_API_ROUTERS_DIR="${FORGE_API_ROUTERS_DIR:-services/api/app/routers}"
41
41
  FORGE_API_MAIN="${FORGE_API_MAIN:-services/api/app/main.py}"