mindforge-cc 11.9.1 → 11.9.3

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 (121) hide show
  1. package/.agent/CLAUDE.md +37 -13
  2. package/.agent/hooks/mindforge-block-no-verify.js +61 -13
  3. package/.agent/hooks/mindforge-config-protection.js +82 -3
  4. package/.agent/hooks/mindforge-context-monitor.js +1 -1
  5. package/.agent/hooks/mindforge-workflow-guard.js +2 -2
  6. package/.agent/hooks/run-with-flags.js +190 -20
  7. package/.agent/mindforge/browse.md +2 -2
  8. package/.agent/mindforge/checkpoint.md +1 -1
  9. package/.agent/mindforge/consult.md +1 -1
  10. package/.agent/mindforge/cost-report.md +1 -1
  11. package/.agent/mindforge/harness-audit.md +1 -1
  12. package/.agent/mindforge/orch-add-feature.md +1 -1
  13. package/.agent/mindforge/orch-build-mvp.md +1 -1
  14. package/.agent/mindforge/orch-change-feature.md +1 -1
  15. package/.agent/mindforge/orch-fix-defect.md +1 -1
  16. package/.agent/mindforge/orch-refine-code.md +1 -1
  17. package/.agent/mindforge/qa.md +2 -2
  18. package/.claude/CLAUDE.md +37 -13
  19. package/.claude/commands/mindforge/browse.md +2 -2
  20. package/.claude/commands/mindforge/checkpoint.md +1 -1
  21. package/.claude/commands/mindforge/consult.md +1 -1
  22. package/.claude/commands/mindforge/cost-report.md +1 -1
  23. package/.claude/commands/mindforge/harness-audit.md +1 -1
  24. package/.claude/commands/mindforge/orch-add-feature.md +1 -1
  25. package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
  26. package/.claude/commands/mindforge/orch-change-feature.md +1 -1
  27. package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
  28. package/.claude/commands/mindforge/orch-refine-code.md +1 -1
  29. package/.claude/commands/mindforge/qa.md +2 -2
  30. package/.mindforge/MINDFORGE-SCHEMA.json +126 -13
  31. package/.mindforge/config.json +4 -4
  32. package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
  33. package/.mindforge/engine/cost-tracking/router.md +1 -1
  34. package/.mindforge/engine/cost-tracking/token-ledger.md +21 -24
  35. package/.mindforge/engine/temporal-protocol.md +2 -2
  36. package/.mindforge/governance/change-classifier.md +20 -4
  37. package/.mindforge/memory/sync-manifest.json +1 -1
  38. package/.mindforge/metrics/METRICS-SCHEMA.md +13 -4
  39. package/.mindforge/personas/cost-optimizer.md +2 -2
  40. package/.mindforge/personas/multi-model-bridge.md +1 -1
  41. package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
  42. package/.mindforge/skills/cost-aware-routing/SKILL.md +3 -3
  43. package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
  44. package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
  45. package/CHANGELOG.md +402 -0
  46. package/MINDFORGE.md +13 -6
  47. package/README.md +51 -2
  48. package/RELEASENOTES.md +55 -2
  49. package/SECURITY.md +22 -3
  50. package/bin/autonomous/audit-writer.js +48 -33
  51. package/bin/autonomous/auto-runner.js +65 -2
  52. package/bin/change-classifier.js +151 -16
  53. package/bin/dashboard/api-router.js +28 -47
  54. package/bin/dashboard/error-response.js +44 -0
  55. package/bin/dashboard/frontend/app.js +429 -0
  56. package/bin/dashboard/frontend/index.html +14 -390
  57. package/bin/dashboard/metrics-aggregator.js +75 -30
  58. package/bin/dashboard/revops-api.js +12 -2
  59. package/bin/dashboard/server.js +245 -6
  60. package/bin/dashboard/sse-bridge.js +11 -8
  61. package/bin/dashboard/temporal-api.js +11 -5
  62. package/bin/engine/remediation-engine.js +12 -1
  63. package/bin/engine/sre-manager.js +1 -1
  64. package/bin/engine/temporal-cli.js +56 -6
  65. package/bin/engine/temporal-hub.js +41 -9
  66. package/bin/engine/verification-runner.js +134 -17
  67. package/bin/engine/verify-cli.js +25 -7
  68. package/bin/eval/eval-harness.js +212 -1
  69. package/bin/eval/golden-set-retrieval.json +9 -0
  70. package/bin/governance/approval-record.js +147 -0
  71. package/bin/governance/approve.js +12 -7
  72. package/bin/governance/policy-engine.js +41 -3
  73. package/bin/governance/policy-gate-hardened.js +36 -1
  74. package/bin/governance/verify-approvals.js +163 -0
  75. package/bin/harness-audit.js +224 -10
  76. package/bin/hindsight-injector.js +8 -2
  77. package/bin/hooks/instinct-capture-hook.js +19 -5
  78. package/bin/install.js +63 -3
  79. package/bin/installer/harness-adapter-compliance.js +339 -28
  80. package/bin/installer/hook-registration.js +504 -0
  81. package/bin/installer-core.js +451 -63
  82. package/bin/learning/instinct-cli.js +14 -24
  83. package/bin/memory/knowledge-capture.js +23 -3
  84. package/bin/memory/knowledge-graph.js +70 -31
  85. package/bin/memory/vector-hub.js +500 -44
  86. package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
  87. package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
  88. package/bin/mindforge-cli.js +110 -17
  89. package/bin/models/cost-tracker.js +126 -29
  90. package/bin/models/model-client.js +6 -1
  91. package/bin/models/model-router.js +28 -7
  92. package/bin/models/usage-record.js +71 -0
  93. package/bin/revops/debt-monitor.js +57 -13
  94. package/bin/security/trust-gate-hook.js +50 -6
  95. package/bin/skill-validator.js +6 -1
  96. package/bin/skills-builder/skill-scorer.js +46 -6
  97. package/bin/updater/self-update.js +6 -1
  98. package/bin/updater/version-comparator.js +21 -1
  99. package/bin/utils/file-lock.js +106 -0
  100. package/bin/utils/mindforge-params.js +124 -0
  101. package/bin/utils/mindforge-version.js +99 -0
  102. package/bin/utils/redact-secrets.js +106 -0
  103. package/bin/validate-config.js +75 -17
  104. package/bin/wizard/setup-wizard.js +4 -1
  105. package/bin/wizard/theme.js +9 -1
  106. package/changelogs/index.json +11 -9
  107. package/changelogs/v11.9.2.md +209 -0
  108. package/changelogs/v11.9.3.md +195 -0
  109. package/docs/References/config-reference.md +76 -14
  110. package/docs/References/sdk-api.md +1 -1
  111. package/docs/Templates/Codebase/architecture.md +1 -1
  112. package/docs/commands-reference.md +4 -5
  113. package/docs/faq.md +25 -5
  114. package/docs/getting-started.md +3 -3
  115. package/docs/sdk-reference.md +15 -7
  116. package/docs/troubleshooting.md +10 -6
  117. package/docs/user-guide.md +14 -14
  118. package/examples/sdk-integration/README.md +1 -1
  119. package/package.json +10 -4
  120. package/subagents/.claude-plugin/marketplace.json +1 -1
  121. package/bin/dashboard/approval-handler.js +0 -136
package/README.md CHANGED
@@ -6,13 +6,55 @@
6
6
 
7
7
  ## Latest release
8
8
 
9
- **v11.9.0** (2026-07-27) — Bedrock provider + full dry-run audit. See [CHANGELOG.md](./CHANGELOG.md) for full history, or [RELEASENOTES.md](./RELEASENOTES.md) for human-readable notes.
9
+ **v11.9.2** (2026-08-16) — Correctness: the config gate can now fail, the audit chain no longer
10
+ forks under concurrent writers, trace retrieval works, and the cost ledger has one record shape.
11
+ **Contains a breaking change to the dashboard HTTP surface** (loopback-only) — see the BREAKING
12
+ section in [CHANGELOG.md](./CHANGELOG.md), or [RELEASENOTES.md](./RELEASENOTES.md) for
13
+ human-readable notes.
14
+
15
+ ---
16
+
17
+ ## What is actually enforced
18
+
19
+ Read this before the install instructions. MindForge ships a large corpus of agent
20
+ instructions — commands, skills, personas, protocols — and those are advisory: they work by
21
+ being in the model's context, and a model can decline them. The parts that would *block* an
22
+ action are hooks, and **no install channel currently registers them.**
23
+
24
+ | Capability | Plugin channel | `npx` channel |
25
+ |---|---|---|
26
+ | Slash commands | Yes | Yes |
27
+ | Skills / personas / protocol docs | Yes | Yes |
28
+ | Subagents | Yes | Yes |
29
+ | Audit hash-chain (`bin/verify-audit.js`) | Yes | Yes |
30
+ | **Hooks enforced (can block a tool call)** | **No** | **No** |
31
+
32
+ Why, specifically:
33
+
34
+ - **No hook configuration ships, and nothing generates one.** `package.json` `files[]` has 48
35
+ entries and none contains `settings`, so neither `.claude/settings.json` nor
36
+ `.agent/settings.json` is published. All references to those paths in `bin/` are reads or
37
+ metadata strings — there is no code that writes or merges one. `bin/harness-audit.js:335`
38
+ even offers "wire trust-gate + block-no-verify into both …" as a *fix suggestion*, auditing a
39
+ wiring nothing creates.
40
+ - **The plugin channel's hooks additionally crash when fired.**
41
+ `plugins/mindforge/scripts/run-with-flags.js:24` requires `./lib/hook-flags`, and
42
+ `plugins/mindforge/scripts/lib/` does not exist in the published plugin. Running the
43
+ dispatcher gives `Error: Cannot find module './lib/hook-flags'` and exit 1. The module it
44
+ needs does exist at `.agent/hooks/lib/hook-flags.js`; it was never copied in.
45
+
46
+ So treat MindForge as **governance-by-convention plus a tamper-evident audit log**, not as a
47
+ policy enforcement point. Installing it also expands your repository's trust boundary by a large
48
+ volume of agent instructions — review what you install. Making hook registration real per
49
+ harness is the headline goal of v12; the audit chain is genuinely verifiable today
50
+ (`node bin/verify-audit.js`).
10
51
 
11
52
  ---
12
53
 
13
54
  ## Install
14
55
 
15
- Fastest path — Claude Code plugin marketplace (no project files written):
56
+ Claude Code plugin marketplace (no project files written). **Note:** the plugin's hooks do not
57
+ fire — see *What is actually enforced* above. Slash commands, skills and subagents do work.
16
58
 
17
59
  ```bash
18
60
  /plugin marketplace add sairam0424/MindForge
@@ -27,6 +69,13 @@ npx mindforge-cc@latest --claude --local
27
69
 
28
70
  All install channels (global, local, Antigravity, Cursor, Copilot, Gemini CLI, MCP server, combined runtimes, `--minimal`): see [docs/getting-started.md](docs/getting-started.md).
29
71
 
72
+ **Upgrading from 11.9.x?** The installer does not overwrite an existing
73
+ `.mindforge/MINDFORGE-SCHEMA.json`, so 11.9.2's armed config validator keeps the older
74
+ permissive schema on a plain upgrade. Run with `--force` if you want the stricter gate. The
75
+ daily cost cap declared as `[COST_HARD_LIMIT_USD]` in `MINDFORGE.md` was **not enforced** in
76
+ 11.9.2; 11.9.3 arms it. An upgrade never rewrites an existing `MINDFORGE.md`, so if yours
77
+ predates the key the cap stays off — add `[COST_HARD_LIMIT_USD] = 25.00` to turn it on.
78
+
30
79
  ---
31
80
 
32
81
  ## Verify
package/RELEASENOTES.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Release Notes
2
2
 
3
+ ## v11.9.2 — 2026-08-16 — Correctness: config gate, audit chain, retrieval, cost ledger
4
+
5
+ ### What's New
6
+ Nothing. This is a correctness release: four things that were reported as working were measured
7
+ and found not to be, and a fifth — the plugin channel's hooks — is now documented as broken
8
+ instead of advertised as the fastest path.
9
+
10
+ ### ⚠️ Breaking (under a patch bump)
11
+ The dashboard's HTTP surface changed shape. `detail` is gone from 5 endpoints and raw errno
12
+ strings from 10 more; `correlation_id` is added to 15; malformed bodies now return
13
+ `application/json` instead of an HTML error page. Those fields were leaking absolute filesystem
14
+ paths — and so the operator's username and home directory — into an unauthenticated response.
15
+ The dashboard also now **exits** on an unhandled rejection or uncaught exception where 11.9.1
16
+ logged and continued, so supervised deployments will see restarts instead of silent errors.
17
+ The surface is loopback-only, so this affects you only if you script against it.
18
+
19
+ ### Fixes
20
+ - **`security-scan` could not fail.** It reported `MINDFORGE.md valid — 0 settings configured`
21
+ and exited 0 on every input, because both config parsers used a plain `KEY=value` regex while
22
+ the registry declares bracketed `[KEY] = value`. All 43 parameters were invisible. One shared
23
+ parser now reads both forms, and the schema has real `required` keys. Three CI gates go from
24
+ unfailable to failable — on fresh installs and `--force` reinstalls; a plain upgrade keeps
25
+ your existing schema.
26
+ - **The audit hash chain forked under concurrent writers.** No `.jsonl` append took a lock, and
27
+ the chain head was cached in-process and never invalidated — so a second process's append made
28
+ the first keep chaining from a superseded hash. Both halves are fixed: a fail-closed lock, and
29
+ a cached head that carries the file size witnessing it is still the tail. A lock alone was
30
+ measured insufficient. 8 concurrent appenders went from 199 broken links to 0.
31
+ - **44.4% of trace content was unsearchable and every multi-word query returned nothing.** The
32
+ index was keyed on `trace_id` rather than the row's primary key, so each span evicted the
33
+ previous one; and whole queries were wrapped as a single FTS phrase, so one absent term zeroed
34
+ the result. 2,270 of 5,117 rows are recovered losslessly and queries are now tokenised and
35
+ ranked. The shipped retrieval eval had zero callers and now runs as `npm run eval:retrieval`.
36
+ - **The cost ledger reported two different totals for one concept** ($13.73 vs $0.00) because
37
+ writer and reader used different field names — and the test wrote the reader's name, so the
38
+ mismatch passed. One record shape now, and the configured ledger path no longer points at a
39
+ file that never existed.
40
+ - **The daily cost cap is declared but not enforced**, and is now labelled that way in
41
+ `MINDFORGE.md` and the schema rather than described as non-overridable. Wiring it is COST-02.
42
+
43
+ ### Honesty
44
+ `README.md` now carries a per-channel capability table with an explicit **Hooks enforced: No**
45
+ row for both install channels, because no hook configuration ships and nothing generates one.
46
+ The plugin channel's dispatcher additionally fails with `Cannot find module './lib/hook-flags'`
47
+ on every fire. Treat MindForge as governance-by-convention plus a verifiable audit log, not as a
48
+ policy enforcement point. Making enforcement real per harness is the goal of v12.
49
+
50
+ ### Stats
51
+ 105 test files, 103 pass, 2 environment-dependent skips. Four suites that could not report
52
+ failure now can. Audit chain verifies clean. Tarball 1,973 entries.
53
+
54
+ ---
55
+
3
56
  ## v11.9.0 — 2026-07-27 — Bedrock Provider + Full Dry-Run Audit
4
57
 
5
58
  ### What's New
@@ -569,7 +622,7 @@ You must address these before upgrading from v9.x:
569
622
  |--------|--------|-----------|
570
623
  | `better-sqlite3` removed, replaced by `sql.js` | If you imported or extended VectorHub internals, the constructor API has changed | Use `createVectorHub()` factory function instead of `new VectorHub()` |
571
624
  | VectorHub uses FTS4 (not FTS5) | FTS5-specific syntax (e.g., `BM25`) no longer available | Use standard FTS4 `MATCH` queries; ranking is handled internally |
572
- | SDK `memory.ts` rewritten | If you imported memory utilities from `../../bin/` paths, those imports no longer resolve | Import from `@mindforge/sdk` directly — all public APIs are re-exported |
625
+ | SDK `memory.ts` rewritten | If you imported memory utilities from `../../bin/` paths, those imports no longer resolve | Import from `mindforge-sdk` directly — all public APIs are re-exported |
573
626
  | Dashboard endpoints require auth | Unauthenticated requests to `/api/steering`, `/api/approve`, and SSE mutators now return 401 | Set `MINDFORGE_DASHBOARD_TOKEN` env var; pass as `Authorization: Bearer <token>` |
574
627
  | `sync-jira` / `sync-confluence` CLI routes removed | These were stubs with no implementation | Remove any scripts that invoke these commands |
575
628
  | Prompt injection blocklist removed | The regex-based `BLOCKED_PATTERNS` array no longer exists | Use the structured action allowlist in `bin/governance/action-allowlist.js` |
@@ -661,7 +714,7 @@ The database file format is compatible. No data migration is required.
661
714
  import { MemoryStore } from '../../bin/memory/store';
662
715
 
663
716
  // After (v10) — public SDK exports
664
- import { MemoryStore } from '@mindforge/sdk';
717
+ import { MemoryStore } from 'mindforge-sdk';
665
718
  ```
666
719
 
667
720
  ### Step 5: Remove references to deleted commands
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Security Policy
2
2
 
3
- > **Current version:** 11.9.0 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
3
+ > **Current version:** 11.9.3 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
4
4
 
5
5
  ## Supported Versions
6
6
 
@@ -66,7 +66,9 @@ We follow responsible disclosure practices. We will credit reporters in the rele
66
66
 
67
67
  ### Audit & Integrity
68
68
 
69
- - **Merkle-chain audit log** — Every entry in `AUDIT.jsonl` includes a SHA-256 hash of the previous entry. Tampering with any historical entry breaks the chain, making modifications detectable.
69
+ - **Hash-chained audit log (SHA-256 back-links)** — Every entry in `AUDIT.jsonl` carries the SHA-256
70
+ hash of the previous entry. Not a Merkle tree: there is no hash tree and no inclusion proof, so
71
+ "Merkle" was the wrong word for it. What it detects, and does not, is measured below.
70
72
  - **AuditWriter with buffered writes** — Atomic append operations prevent partial writes from corrupting the log.
71
73
  - **Log rotation with archival** — AUDIT.jsonl auto-archives beyond 5000 lines with gzip compression, preventing unbounded disk growth.
72
74
  - **npm provenance** — Published packages include SLSA Build Level 2 attestation via `--provenance`, proving the package was built from the stated source commit in CI.
@@ -143,7 +145,24 @@ For security questions that are not vulnerability reports, open a GitHub Discuss
143
145
  Tier-3 trust in v11.x uses **in-process key simulation** (`bin/governance/ztai-manager.js` `SecureEnclaveProvider`). Key material resides in the Node.js heap — it is NOT hardware-isolated. A real TPM/HSM provider is planned for v12.x. **Do not use Tier-3 trust for production credential workflows in v11.x.**
144
146
 
145
147
  ## Audit Log Tamper Evidence
146
- The Merkle chain (`bin/governance/audit-hash.js`) provides tamper-evidence for content and ordering but does not prevent replay of identical entries. Restrict OS-level write access to `.planning/AUDIT.jsonl` to prevent replay attacks.
148
+ The hash chain (`bin/governance/audit-hash.js`) is real and independently verifiable `node
149
+ bin/verify-audit.js` recomputes it and exits 1 on a break. Measured on a 200-entry chain taken from
150
+ the head of a live log:
151
+
152
+ | Attack | Detected? | Evidence |
153
+ |---|---|---|
154
+ | Mutate a middle entry | **YES** | `BROKEN at entry 79: hash mismatch (entry mutated)`, exit 1 |
155
+ | Delete a middle entry | **YES** | `BROKEN at entry 79: previous_hash mismatch`, exit 1 |
156
+ | **Truncate the tail** | **NO** | 40 entries removed from the end → `audit chain valid: 160 entries`, **exit 0** |
157
+ | Replay identical entries | **NO** | the chain constrains content and ordering, not multiplicity |
158
+
159
+ The truncation gap is inherent to a bare hash chain: each entry commits to its predecessor, so a
160
+ prefix of a valid chain is itself a valid chain. Closing it needs a commitment to LENGTH — an external
161
+ anchor, a signed length attestation, or a witnessed head — none of which ships today.
162
+
163
+ So the accurate claim is **"detects mutation and mid-file deletion"**, not "immutable" or
164
+ "non-repudiable". Restrict OS-level write access to `.planning/AUDIT.jsonl`; the chain does not
165
+ substitute for filesystem permissions.
147
166
 
148
167
  ## Agent Dispatch (spawn mode)
149
168
  `bin/spawn-agent.js` spawn mode exits with an error in v11.x — real agent dispatch is not yet implemented at the shell level. Use Claude Code slash commands (`/mindforge:auto`, `/mindforge:next`) to dispatch agents.
@@ -20,6 +20,7 @@ const fs = require('fs');
20
20
  const path = require('path');
21
21
  const crypto = require('crypto');
22
22
  const { hashAuditEntry } = require('../governance/audit-hash');
23
+ const { withFileLock } = require('../utils/file-lock');
23
24
 
24
25
  /**
25
26
  * Computes the SHA-256 hash of an entry chained to its predecessor (UC-04).
@@ -57,19 +58,20 @@ function readLastHash(auditPath) {
57
58
  // sync write gives in-process consumers (e.g. StuckMonitor, which is fed the event
58
59
  // object directly but may also re-read the file) immediate, durable data.
59
60
  //
60
- // Chain head caching: re-reading the file's tail on every append is O(file) — bad
61
- // on hot paths. Instead we keep a per-path in-memory lastHash (Map keyed by the
62
- // RESOLVED absolute path), seeded ONCE from the file's last entry on the first
63
- // append, then advanced in-process for O(1) appends. If the cache is cold (new
64
- // process, or a path never written in this process) we seed from disk so a
65
- // second process correctly continues the on-disk chain from its tail.
61
+ // Chain head caching: re-reading the file's tail on every append is O(file) — bad on
62
+ // hot paths. We keep a per-path in-memory head (Map keyed by the RESOLVED absolute
63
+ // path) as { hash, size }, where `size` is the file size immediately after our own
64
+ // write. The size is the WITNESS that the cache is still the true tail: if the file is
65
+ // still exactly that long, nobody else appended and the cached hash is the tail (O(1));
66
+ // if it differs, another process appended and we MUST re-seed from disk.
66
67
  //
67
- // Concurrency: within a process this is fully synchronous, so calls cannot
68
- // interleave and the cached lastHash is always current. ACROSS processes, each
69
- // process seeds from the file tail on its first append; this is correct only under
70
- // the single-operator model (no two processes appending CONCURRENTLY to the same
71
- // audit file). MindForge runs one autonomous operator at a time, so this holds.
72
- const _lastHashCache = new Map(); // resolvedPath -> last `_hash` written/seen
68
+ // Concurrency (LOCK-01): the seed-through-write sequence below runs inside
69
+ // withFileLock, so no other process can read the head, or append, while we hold it.
70
+ // The size check is what makes the cache safe across processes a lock alone is NOT
71
+ // enough, because a warm cache would still hand back a hash that a lock-respecting
72
+ // second writer has since superseded, forking the chain. Verified: lock-only leaves
73
+ // 4-6 link breaks per 8-process run; lock + size-validated reseed leaves 0.
74
+ const _lastHashCache = new Map(); // resolvedPath -> { hash, size }
73
75
 
74
76
  /**
75
77
  * Synchronously appends ONE hash-chained, durable entry to an audit JSONL file.
@@ -96,30 +98,43 @@ function appendAuditEntrySync(auditPath, event) {
96
98
  timestamp: event.timestamp || new Date().toISOString(),
97
99
  };
98
100
 
99
- // 2. Seed previous_hash: prefer the warm in-process cache; fall back to the
100
- // file's last entry when cold (first append in this process for this path).
101
- let previous_hash = _lastHashCache.has(resolved)
102
- ? _lastHashCache.get(resolved)
103
- : readLastHash(resolved);
101
+ // 2-5. LOCK-01: seed-through-write is ONE critical section. The lock opens BEFORE
102
+ // the head is read and closes AFTER the fsync, so no other process can read the
103
+ // same head, or interleave an append, while we hold it. maxTries is raised above
104
+ // the module default because failing closed here DROPS an audit entry: at 16
105
+ // concurrent writers the default ~1s ceiling loses ~35% of entries, 150 tries
106
+ // loses none.
107
+ return withFileLock(resolved, () => {
108
+ // 2. Seed previous_hash from the warm cache ONLY if the file is still exactly as
109
+ // long as we left it (proof nobody else appended); otherwise re-seed from disk.
110
+ const cached = _lastHashCache.get(resolved);
111
+ let onDiskSize = -1;
112
+ try { onDiskSize = fs.statSync(resolved).size; } catch { onDiskSize = -1; }
113
+ const previous_hash = (cached && cached.size === onDiskSize)
114
+ ? cached.hash
115
+ : readLastHash(resolved);
104
116
 
105
- // 3. Compute _hash over {...stamped, previous_hash} WITHOUT _hash in the material.
106
- const _hash = hashEntry(stamped, previous_hash);
117
+ // 3. Compute _hash over {...stamped, previous_hash} WITHOUT _hash in the material.
118
+ const _hash = hashEntry(stamped, previous_hash);
107
119
 
108
- // 4. Write {...stamped, previous_hash, _hash} as one JSON line, durably+synchronously
109
- // (openSync('a') + writeSync + fsyncSync + closeSync — mirrors appendDurableSync).
110
- const chained = { ...stamped, previous_hash, _hash };
111
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
112
- const fd = fs.openSync(resolved, 'a');
113
- try {
114
- fs.writeSync(fd, JSON.stringify(chained) + '\n');
115
- fs.fsyncSync(fd);
116
- } finally {
117
- fs.closeSync(fd);
118
- }
120
+ // 4. Write {...stamped, previous_hash, _hash} as one JSON line, durably+synchronously
121
+ // (openSync('a') + writeSync + fsyncSync + closeSync — mirrors appendDurableSync).
122
+ const chained = { ...stamped, previous_hash, _hash };
123
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
124
+ const fd = fs.openSync(resolved, 'a');
125
+ let sizeAfter = -1;
126
+ try {
127
+ fs.writeSync(fd, JSON.stringify(chained) + '\n');
128
+ fs.fsyncSync(fd);
129
+ sizeAfter = fs.fstatSync(fd).size; // exact new length, one syscall, fd already open
130
+ } finally {
131
+ fs.closeSync(fd);
132
+ }
119
133
 
120
- // 5. Advance the in-process chain head and return the written entry.
121
- _lastHashCache.set(resolved, _hash);
122
- return chained;
134
+ // 5. Advance the in-process chain head (hash + its witness) and return the entry.
135
+ _lastHashCache.set(resolved, { hash: _hash, size: sizeAfter });
136
+ return chained;
137
+ }, { maxTries: 150, label: 'audit' });
123
138
  }
124
139
 
125
140
  module.exports = { appendAuditEntrySync };
@@ -117,6 +117,12 @@ class AutoRunner {
117
117
  this.phase = String(options.phase);
118
118
  this.isHeadless = options.headless || false;
119
119
  this.isPaused = false;
120
+ // Optional async function(task) that performs the actual work for one task. When absent,
121
+ // executeWave() aborts the wave rather than writing task_completed entries for work that would
122
+ // not run — see the comment there. Deliberately NOT defaulted to a no-op: bin/autonomous/
123
+ // wave-executor.js:133 defaults its own `executor` to `async () => {}`, and a silent no-op
124
+ // default is exactly how a wave came to report every task fulfilled while dispatching nothing.
125
+ this.taskExecutor = typeof options.taskExecutor === 'function' ? options.taskExecutor : null;
120
126
 
121
127
  // Paths
122
128
  const planningDir = path.join(process.cwd(), '.planning');
@@ -306,10 +312,30 @@ class AutoRunner {
306
312
  runPreFlight() {
307
313
  console.log('🔍 Running pre-flight checks...');
308
314
 
309
- // UC-01: fail closed on version drift before any wave executes
315
+ // UC-01: fail closed on version drift before any wave executes.
316
+ //
317
+ // The root MUST be MindForge's own install root, NOT process.cwd(). This check compares
318
+ // package.json against .mindforge/config.json, sdk/package.json and MINDFORGE.md — all
319
+ // MindForge's own manifests. Pointed at a consumer's cwd it takes THEIR application
320
+ // version as canonical and reports MindForge's own config as drift. Measured, with an app
321
+ // at 1.0.0 and MindForge at 11.9.2:
322
+ // [".mindforge/config.json declares 11.9.2 but canonical (package.json) is 1.0.0"]
323
+ // and a consumer with no package.json at all fails closed on a missing canonical. Only a
324
+ // project whose app version coincidentally equalled MindForge's would have passed.
325
+ //
326
+ // This has never fired in a shipped build, because nothing constructs AutoRunner today
327
+ // (verified: no `new AutoRunner` anywhere in bin/; the only requirer is
328
+ // tests/wave-timeout-rollback.test.js, for the pure helpers). It is a landmine that arms
329
+ // the moment the runner is wired — which DEL-02 proposes doing — so it is disarmed here
330
+ // rather than left for whoever wires it to find in the field.
331
+ //
332
+ // __dirname/../.. is MindForge's root in both layouts: <repo> from a checkout, and
333
+ // <project>/node_modules/mindforge-cc when installed. In the installed shape
334
+ // .mindforge/config.json is absent (the installer writes that into the CONSUMER project),
335
+ // which reads as null and is SKIPPED rather than counted as drift — verified.
310
336
  try {
311
337
  const { assertVersionConsistency } = require('../utils/version-check');
312
- assertVersionConsistency(process.cwd());
338
+ assertVersionConsistency(path.resolve(__dirname, '..', '..'));
313
339
  } catch (e) {
314
340
  throw new Error(`[pre-flight] ${e.message}`);
315
341
  }
@@ -357,6 +383,40 @@ class AutoRunner {
357
383
 
358
384
  console.log(`\n⚡ Wave ${waveNum}/${this.waves.length}: ${pending.length} tasks (concurrency: ${maxConcurrency})`);
359
385
  if (idcStatus.action === 'UPGRADE_MIR') console.log(` [IDC-ACTIVE] MIR Override: ${idcStatus.new_mir}`);
386
+
387
+ // REFUSE TO FABRICATE. The task body below used to be, in full:
388
+ //
389
+ // try {
390
+ // this.writeAudit({ event: 'task_started', ... });
391
+ // this.writeAudit({ event: 'task_completed', ... duration_ms: Date.now() - taskStart });
392
+ // this.completedTasks.add(task.id);
393
+ //
394
+ // Nothing between the two writes. No dispatch. So every task was recorded as completed with a
395
+ // duration of ~0ms, the catch block was unreachable because nothing could throw, and the
396
+ // hash-chained audit log — the one component of this project that is genuinely production-grade
397
+ // and independently verifiable — became a tamper-evident record of statements that were false.
398
+ // A verifiable false record is worse than no record: it survives verify-audit and carries the
399
+ // authority of the chain.
400
+ //
401
+ // Measured before this change: task_completed = 0 in the live 3056-entry chain, and no module
402
+ // under bin/ requires auto-runner (every repo-wide match is a comment, a test, or an
403
+ // "extracted from" note). So the defect was LATENT — the code would have lied the first time
404
+ // anything invoked it, and nothing had. That is why this is a refusal rather than a cleanup:
405
+ // there is no contamination to repair, only a trap to disarm.
406
+ //
407
+ // A wave with no executor now writes ONE honest entry and throws, instead of N false ones.
408
+ if (typeof this.taskExecutor !== 'function') {
409
+ this.writeAudit({
410
+ event: 'wave_aborted', phase: this.phase, wave: waveNum, task_count: pending.length,
411
+ reason: 'no task executor is wired, so no task can run. Refusing to write task_completed '
412
+ + 'entries for work that would not be performed — see auto-runner.executeWave.',
413
+ });
414
+ throw new Error(
415
+ `Wave ${waveNum} cannot execute: no task executor wired. Pass { taskExecutor } to the `
416
+ + 'AutoRunner constructor, or drive waves through bin/autonomous/wave-executor.js. '
417
+ + 'Refusing to record completions for work that would not run.');
418
+ }
419
+
360
420
  this.writeAudit({ event: 'wave_started', phase: this.phase, wave: waveNum, task_count: pending.length });
361
421
 
362
422
  const semaphore = new Semaphore(maxConcurrency);
@@ -368,6 +428,9 @@ class AutoRunner {
368
428
  console.log(` → Task: ${task.name || task.id}`);
369
429
  try {
370
430
  this.writeAudit({ event: 'task_started', phase: this.phase, wave: waveNum, task_id: task.id, task_name: task.name || task.id });
431
+ // The dispatch that was missing. Its absence is what made the catch below unreachable and
432
+ // every duration_ms ~0; task_completed is now written only after real work returns.
433
+ await this.taskExecutor(task);
371
434
  this.writeAudit({ event: 'task_completed', phase: this.phase, wave: waveNum, task_id: task.id, task_name: task.name || task.id, duration_ms: Date.now() - taskStart });
372
435
  this.completedTasks.add(task.id);
373
436
  return { taskId: task.id, status: 'fulfilled' };
@@ -9,16 +9,55 @@
9
9
  const { execSync, execFileSync } = require('child_process');
10
10
  const fs = require('fs');
11
11
 
12
+ // Matched with String.prototype.startsWith against repo-relative diff paths.
13
+ //
14
+ // Two groups, because this file SHIPS (package.json files[] includes bin/) and therefore runs
15
+ // against two different trees:
16
+ //
17
+ // (a) CONSUMER paths — a project that installed MindForge. Measured against MindForge's own
18
+ // tree these match ZERO tracked files, which is correct: they describe the consumer's
19
+ // layout, not the framework's. Do not remove them.
20
+ // (b) FRAMEWORK paths — MindForge's OWN trust surface, which control-plane.yml classifies on
21
+ // every push and PR to this repo. These were absent, so the detector protected the five
22
+ // markdown files under .mindforge/governance/ while leaving the 15 executable modules
23
+ // under bin/governance/ — the audit hasher, the verifier, RBAC, the policy engine and
24
+ // approve.js itself — unclassified. Editing the prose ABOUT governance tripped Tier 3;
25
+ // editing the code that enforces it did not.
12
26
  const SENSITIVE_PATHS = [
27
+ // (a) consumer-project layout
13
28
  'auth/',
14
29
  'payment/',
15
30
  'security/',
31
+ // (b) MindForge's own trust surface
32
+ 'bin/governance/', // audit-hash, audit-verifier, rbac, policy-engine, approve
33
+ 'bin/security/', // trust-gate-hook and friends — NOT matched by 'security/'
34
+ 'bin/hooks/', // instinct-capture-hook, context-monitor
35
+ 'bin/models/',
36
+ '.agent/hooks/', // the hook dispatcher the installer copies
37
+ '.claude/settings.json', // hook registration
38
+ '.agent/settings.json',
39
+ '.planning/approvals/', // the approval records themselves
16
40
  '.github/workflows/',
17
- '.mindforge/governance/',
18
- 'bin/models/'
41
+ '.mindforge/governance/'
19
42
  ];
20
43
 
44
+ // Scanned against the DIFF CONTENT, so security-critical code in an innocuously-named file still
45
+ // reaches Tier 3 — that is the stated purpose of "Signal B" in
46
+ // .mindforge/governance/change-classifier.md.
47
+ //
48
+ // The doc lists 19 patterns; measured, only 6 of them were detected. The second group below was
49
+ // specified and absent: argon2 (a password hasher), the jose signing pair, paypal.,
50
+ // createCipheriv/createDecipheriv and crypto.subtle (cipher construction), encrypt(/decrypt(,
51
+ // role.*permission and hasPermission (authorization), and SET ROLE / GRANT (SQL privilege
52
+ // changes). tests/governance.test.js pins every documented pattern against the live set, so the
53
+ // doc and this array cannot drift apart again.
54
+ //
55
+ // Adding them cost nothing measurable: over the last 40 commits they caught ZERO additional
56
+ // changes, because anything they would flag was already Tier 3 by path or by one of the broad
57
+ // substring patterns above. So this closes a documented gap without moving the Tier-3 rate, which
58
+ // sat at 50% (path OR pattern) before and after.
21
59
  const SENSITIVE_PATTERNS = [
60
+ // broad substring signals
22
61
  /jwt/i,
23
62
  /bcrypt/i,
24
63
  /stripe/i,
@@ -26,18 +65,89 @@ const SENSITIVE_PATTERNS = [
26
65
  /password/i,
27
66
  /secret/i,
28
67
  /token/i,
29
- /PII/
68
+ /PII/,
69
+ // the specified-but-missing set
70
+ /argon2/i,
71
+ /jose\.(sign|verify)/i,
72
+ /paypal\./i,
73
+ /createCipheriv/,
74
+ /createDecipheriv/,
75
+ /crypto\.subtle/,
76
+ /\bencrypt\(/,
77
+ /\bdecrypt\(/,
78
+ /role.*permission/i,
79
+ /hasPermission/,
80
+ /\bSET ROLE\b/i,
81
+ /\bGRANT\b/
30
82
  ];
31
83
 
84
+ /** True if `rev` resolves to an object in this clone (false on a shallow/partial fetch). */
85
+ function revExists(rev) {
86
+ try {
87
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', `${rev}^{commit}`], { stdio: 'pipe' });
88
+ return true;
89
+ } catch { return false; }
90
+ }
91
+
92
+ /** The pre-push tip GitHub reports for a push event, or '' if unavailable. */
93
+ function pushBefore() {
94
+ if (process.env.MINDFORGE_PUSH_BEFORE) return process.env.MINDFORGE_PUSH_BEFORE;
95
+ // Read the event payload rather than requiring the workflow to thread a variable through.
96
+ // A fix that depends on someone remembering `env: BEFORE: ${{ github.event.before }}` is a
97
+ // fix that silently reverts the first time a workflow is copied.
98
+ const p = process.env.GITHUB_EVENT_PATH;
99
+ if (!p || !fs.existsSync(p)) return '';
100
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')).before || ''; } catch { return ''; }
101
+ }
102
+
103
+ /**
104
+ * Resolve the commit range to classify.
105
+ *
106
+ * A push is a RANGE, not a commit. The old code used HEAD~1..HEAD whenever GITHUB_BASE_REF was
107
+ * unset, so on a push it inspected only the tip. Measured: two commits where the first adds
108
+ * auth/login.js with a hardcoded password and the second is docs-only classify as TIER=1 on a
109
+ * push and TIER=3 as a PR — the sensitive file rides in completely unclassified. control-plane.yml
110
+ * triggers on `push: [main, develop]`, and this repo has allow_rebase_merge enabled, so the
111
+ * multi-commit push is the normal case, not an edge case.
112
+ *
113
+ * @returns {{range:string, how:string}}
114
+ * @throws {Error} with .failClosed when a CI push range cannot be resolved.
115
+ */
116
+ function resolveRange() {
117
+ // Pull request: three-dot diffs against the MERGE-BASE, so a branch that is merely behind
118
+ // its base does not pick up base-only changes. (Two-dot here caused Tier-3 false positives.)
119
+ if (process.env.GITHUB_BASE_REF) {
120
+ return { range: `origin/${process.env.GITHUB_BASE_REF}...HEAD`, how: 'pull_request' };
121
+ }
122
+
123
+ const inCiPush = process.env.GITHUB_EVENT_NAME === 'push';
124
+ if (inCiPush) {
125
+ const before = pushBefore();
126
+ const isBranchCreation = /^0{40}$/.test(before);
127
+ if (before && !isBranchCreation && revExists(before)) {
128
+ return { range: `${before}..HEAD`, how: 'push' };
129
+ }
130
+ // Fail CLOSED. Every remaining case genuinely cannot be scoped: a new branch has no prior
131
+ // tip, and a shallow clone cannot reach `before`. Falling back to HEAD~1 here is what let
132
+ // a sensitive commit ride in behind a benign tip.
133
+ const why = isBranchCreation ? 'branch creation (before is all-zeros)'
134
+ : !before ? 'no `before` in the push event payload'
135
+ : `\`before\` (${before.slice(0, 12)}) is not present in this clone — shallow fetch?`;
136
+ const err = new Error(`cannot scope the push range: ${why}`);
137
+ err.failClosed = true;
138
+ throw err;
139
+ }
140
+
141
+ // Local invocation (`mindforge classify`). HEAD~1..HEAD is a developer convenience, not a
142
+ // gate, and must never be reached in CI — the branch above owns every CI push.
143
+ return { range: 'HEAD~1..HEAD', how: 'local' };
144
+ }
145
+
32
146
  function classify() {
33
147
  try {
34
- // Get list of changed files compared to origin/<base> or HEAD~1.
35
- // Three-dot (...) diffs against the MERGE-BASE, so on a PR branch that is behind its base
36
- // we see ONLY this branch's own changes — not unrelated commits already on the base.
37
- // (Two-dot here caused Tier-3 false positives by pulling in base-only changes.)
38
- const base = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : 'HEAD~1';
39
- const range = process.env.GITHUB_BASE_REF ? `${base}...HEAD` : `${base}..HEAD`;
148
+ const { range, how } = resolveRange();
40
149
  const diffFiles = execFileSync('git', ['diff', '--name-only', range], { encoding: 'utf8' }).split('\n').filter(Boolean);
150
+ if (process.env.MINDFORGE_CLASSIFY_DEBUG) console.error(`[classify] ${how}: ${range} (${diffFiles.length} file(s))`);
41
151
 
42
152
  // Test and documentation files are excluded from the sensitive-PATTERN scan below: a test
43
153
  // asserting on "password"/key patterns, or a doc mentioning secrets, is not a sensitive
@@ -50,10 +160,16 @@ function classify() {
50
160
  let reasons = [];
51
161
 
52
162
  // 1. Path-based detection (Tier 3)
53
- const matchedPath = diffFiles.find(file => SENSITIVE_PATHS.some(p => file.startsWith(p)));
54
- if (matchedPath) {
163
+ // Reports EVERY matched path, not just the first. `find` short-circuited, so a change
164
+ // touching both .github/workflows/ and bin/governance/ was attributed only to the workflow —
165
+ // which made the CI log unable to show that the newly-added framework paths were doing any
166
+ // work, and hid the more interesting of the two matches from the reviewer.
167
+ const matchedPaths = diffFiles.filter(file => SENSITIVE_PATHS.some(p => file.startsWith(p)));
168
+ if (matchedPaths.length) {
55
169
  tier = 3;
56
- reasons.push(`Sensitive path modified: ${matchedPath}`);
170
+ const shown = matchedPaths.slice(0, 5).join(', ');
171
+ const more = matchedPaths.length > 5 ? ` (+${matchedPaths.length - 5} more)` : '';
172
+ reasons.push(`Sensitive path${matchedPaths.length > 1 ? 's' : ''} modified: ${shown}${more}`);
57
173
  }
58
174
 
59
175
  // 2. Pattern-based detection in diff (Tier 3) — non-test/doc files only
@@ -94,13 +210,32 @@ function classify() {
94
210
 
95
211
  return tier;
96
212
  } catch (err) {
97
- console.error(`❌ Classification failed: ${err.message}`);
98
- // Default to Tier 3 for safety if classification fails
213
+ // Fail closed: an unscopeable change is treated as maximum risk. Emit the SAME
214
+ // TIER=/REASONS= lines as the success path so every consumer — the workflow's stdout
215
+ // parsing, GITHUB_OUTPUT, and a human reading the log — sees tier 3 rather than nothing.
216
+ // Previously only GITHUB_OUTPUT was written, so anything reading stdout saw no tier at all.
217
+ const reason = err.failClosed
218
+ ? `Fail-closed: ${err.message}`
219
+ : `Fail-closed: classification error — ${err.message}`;
220
+ console.log('TIER=3');
221
+ console.log(`REASONS=${reason}`);
222
+ console.error(`❌ Classification could not be scoped — defaulting to Tier 3. ${err.message}`);
223
+ if (process.env.GITHUB_ACTIONS) {
224
+ console.error(`::error title=Change classification failed closed::${reason}`);
225
+ }
99
226
  if (process.env.GITHUB_OUTPUT) {
100
227
  fs.appendFileSync(process.env.GITHUB_OUTPUT, 'tier=3\n');
101
228
  }
102
- process.exit(0); // Don't fail the pipeline yet, let the gate handle it
229
+ // Exit 0 deliberately: this step's contract is to REPORT a tier, and the downstream gate
230
+ // is what blocks. Exiting non-zero here would fail the job before the gate can annotate
231
+ // why. That contract is only honest while a gate actually consumes tier 3 — see
232
+ // tests/change-classifier.test.js, which pins that the tier-3 consumer exists.
233
+ return 3;
103
234
  }
104
235
  }
105
236
 
106
- classify();
237
+ module.exports = { classify, resolveRange, revExists, pushBefore, SENSITIVE_PATHS, SENSITIVE_PATTERNS };
238
+
239
+ // Behind a require.main guard so tests can import the pure pieces without triggering a run.
240
+ // bin/mindforge-cli.js:51 spawns this file as a script, which still executes normally.
241
+ if (require.main === module) classify();