mindforge-cc 11.0.0 → 11.2.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 (70) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/config.json +14 -4
  3. package/CHANGELOG.md +137 -0
  4. package/MINDFORGE.md +5 -5
  5. package/RELEASENOTES.md +1 -1
  6. package/bin/autonomous/audit-writer.js +108 -86
  7. package/bin/autonomous/auto-runner.js +304 -19
  8. package/bin/autonomous/dependency-dag.js +59 -0
  9. package/bin/autonomous/mesh-self-healer.js +101 -28
  10. package/bin/autonomous/wave-executor.js +20 -1
  11. package/bin/browser/regression-writer.js +45 -3
  12. package/bin/browser/session-manager.js +21 -17
  13. package/bin/council-cli.js +161 -0
  14. package/bin/dashboard/approval-handler.js +3 -1
  15. package/bin/dashboard/server.js +1 -1
  16. package/bin/dashboard/sse-bridge.js +9 -12
  17. package/bin/engine/council-runtime.js +124 -0
  18. package/bin/engine/logic-drift-detector.js +14 -6
  19. package/bin/engine/logic-validator.js +155 -25
  20. package/bin/engine/orbital-guardian.js +56 -10
  21. package/bin/engine/otel-exporter.js +123 -0
  22. package/bin/engine/reason-source-aligner.js +19 -6
  23. package/bin/engine/remediation-engine.js +1 -1
  24. package/bin/engine/self-corrective-synthesizer.js +1 -1
  25. package/bin/engine/sre-manager.js +33 -6
  26. package/bin/engine/temporal-cli.js +4 -2
  27. package/bin/engine/verification-runner.js +131 -0
  28. package/bin/engine/verify-cli.js +34 -0
  29. package/bin/eval/eval-harness.js +82 -0
  30. package/bin/eval/golden-set-retrieval.json +46 -0
  31. package/bin/governance/audit-hash.js +12 -0
  32. package/bin/governance/audit-verifier.js +60 -0
  33. package/bin/governance/policy-engine.js +17 -4
  34. package/bin/governance/quantum-crypto.js +63 -9
  35. package/bin/governance/ztai-archiver.js +74 -9
  36. package/bin/governance/ztai-manager.js +33 -5
  37. package/bin/hindsight-injector.js +5 -6
  38. package/bin/hooks/instinct-capture-hook.js +186 -0
  39. package/bin/installer-core.js +31 -2
  40. package/bin/memory/auto-shadow.js +32 -3
  41. package/bin/memory/eis-client.js +45 -4
  42. package/bin/memory/identity-synthesizer.js +2 -2
  43. package/bin/memory/knowledge-store.js +30 -6
  44. package/bin/memory/retrieval-fusion.js +58 -0
  45. package/bin/memory/semantic-hub.js +2 -2
  46. package/bin/memory/vector-hub.js +143 -6
  47. package/bin/mindforge-cli.js +4 -5
  48. package/bin/models/anthropic-provider.js +13 -4
  49. package/bin/models/cost-tracker.js +3 -1
  50. package/bin/models/difficulty-scorer.js +54 -0
  51. package/bin/models/gemini-provider.js +6 -2
  52. package/bin/models/model-router.js +31 -18
  53. package/bin/models/openai-provider.js +6 -3
  54. package/bin/models/pricing-registry.js +128 -0
  55. package/bin/review/ads-engine.js +1 -1
  56. package/bin/review/finding-synthesizer.js +35 -6
  57. package/bin/security/trust-boundaries.js +194 -0
  58. package/bin/security/trust-gate-hook.js +49 -0
  59. package/bin/skill-registry.js +34 -22
  60. package/bin/skills-builder/marketplace-cli.js +5 -3
  61. package/bin/skills-builder/skill-registrar.js +4 -6
  62. package/bin/sre/sentinel.js +7 -5
  63. package/bin/sre/shadow-mirror.js +90 -40
  64. package/bin/utils/append-queue.js +67 -0
  65. package/bin/utils/file-io.js +29 -80
  66. package/bin/utils/version-check.js +75 -0
  67. package/bin/verify-audit.js +12 -0
  68. package/bin/wizard/theme.js +1 -2
  69. package/package.json +1 -1
  70. package/bin/dashboard/team-tracker.js +0 -0
@@ -84,7 +84,7 @@ process.stdin.on('end', () => {
84
84
  const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8'));
85
85
  const inProgress = todos.find(t => t.status === 'in_progress');
86
86
  if (inProgress) task = inProgress.activeForm || '';
87
- } catch (e) {}
87
+ } catch (e) { /* intentionally empty */ }
88
88
  }
89
89
  } catch (e) {
90
90
  // Silently fail on file system errors - don't break statusline
@@ -103,7 +103,7 @@ process.stdin.on('end', () => {
103
103
  if (cache.stale_hooks && cache.stale_hooks.length > 0) {
104
104
  mindforgeUpdate += '\x1b[31m⚠ stale hooks — run /mindforge:update\x1b[0m │ ';
105
105
  }
106
- } catch (e) {}
106
+ } catch (e) { /* intentionally empty */ }
107
107
  }
108
108
 
109
109
  // Output
@@ -1,11 +1,11 @@
1
1
  {
2
- "version": "10.7.0",
2
+ "version": "11.2.1",
3
3
  "environment": "development",
4
4
  "governance": {
5
5
  "drift_threshold": 0.75,
6
6
  "critical_drift_threshold": 0.5,
7
7
  "res_threshold": 0.8,
8
- "active_did": "did:mindforge:da5daf83-c478-490f-b528-bd907ad4eee3"
8
+ "active_did": "did:mindforge:cbca5a94-a796-4919-a1a3-d0488f228611"
9
9
  },
10
10
  "revops": {
11
11
  "market_registry": {
@@ -18,6 +18,8 @@
18
18
  "claude-sonnet-4-6": {
19
19
  "cost_input": 0.003,
20
20
  "cost_output": 0.015,
21
+ "cost_cache_read": 0.0003,
22
+ "cost_cache_creation": 0.00375,
21
23
  "benchmark": 99,
22
24
  "provider": "Anthropic"
23
25
  },
@@ -42,12 +44,16 @@
42
44
  "claude-haiku-4-5": {
43
45
  "cost_input": 0.0002,
44
46
  "cost_output": 0.0004,
47
+ "cost_cache_read": 0.00002,
48
+ "cost_cache_creation": 0.00025,
45
49
  "benchmark": 82,
46
50
  "provider": "Anthropic"
47
51
  },
48
52
  "claude-opus-4-7": {
49
53
  "cost_input": 0.015,
50
54
  "cost_output": 0.075,
55
+ "cost_cache_read": 0.0015,
56
+ "cost_cache_creation": 0.01875,
51
57
  "benchmark": 100,
52
58
  "provider": "Anthropic"
53
59
  }
@@ -56,9 +62,12 @@
56
62
  "premium_fallback_model": "claude-opus-4-7"
57
63
  },
58
64
  "security": {
59
- "pqas_enabled": true,
65
+ "pqas_enabled": false,
60
66
  "enclave_tier": 2,
61
- "provider": "simulated-lattice"
67
+ "provider": "Dilithium-5 (simulated — inactive)"
68
+ },
69
+ "experimental": {
70
+ "pqc_demo": false
62
71
  },
63
72
  "mesh": {
64
73
  "node_id": "beta-node",
@@ -98,6 +107,7 @@
98
107
  },
99
108
  "cost_routing": {
100
109
  "enabled": true,
110
+ "shadow_mode": true,
101
111
  "simple_threshold": 3,
102
112
  "standard_threshold": 6,
103
113
  "complex_threshold": 8,
package/CHANGELOG.md CHANGED
@@ -1,5 +1,142 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.2.1] - 2026-05-31 — "Hardening" (security & integrity audit remediation)
4
+
5
+ Post-v11.2.0 audit remediation. Closes every exploitable security defect and
6
+ false-assurance stub surfaced by the end-to-end audit. No new features, no
7
+ breaking changes — fixes and honest-labeling only.
8
+
9
+ ### Security (fixed)
10
+
11
+ - **trust-gate-hook**: scans the whole command + every line (a benign first line could previously cloak a destructive later line)
12
+ - **orbital-guardian**: `verify()` re-checks the Ed25519 attestation signature (added `did`/`signed_message` columns + migration); rejects forged APPROVED rows
13
+ - **policy-engine**: `reasoning_proof` alone no longer bypasses the blast-radius limit (`isProofValid` inits false; cryptographic `pq_proof` path unchanged)
14
+ - **shadow-mirror**: git calls use `execFileSync` (argv) + fail-closed `sanitizeRemediationId()` — closes command injection via `remediation_id`
15
+ - **trust-boundaries `isHighImpact`**: added chmod/chown/dd/mv/kill/shutdown/eval/command-substitution/redirect detection + shell-obfuscation normalization; narrowed interpreter-script pattern to stop false-positives on `node <projectfile>`
16
+ - **eis-client**: `verifyRemoteProvenance` delegates to real ZTAI signature verification, fail-closed (was returning true for any non-empty signature)
17
+
18
+ ### Integrity / honest labeling (fixed)
19
+
20
+ - **ztai-archiver**: `verifyIntegrity()` recomputes the Merkle root from the live log (was a no-op `return true`); fail-closed on tamper/delete/reorder
21
+ - **mesh-self-healer**: emits an honest degraded advisory instead of fabricated 94%/100% consensus
22
+ - **logic-validator**: probes Ollama when reachable, honest heuristic fallback; stopped advertising the dead model path
23
+ - **reason-source-aligner**: consistent return shape (uninitialized no longer silently disables the mission-fidelity gate); real Jaccard similarity
24
+ - **sre-manager**: HMAC artifact relabeled as integrity tag, not "ZK-Proof"
25
+ - **installer-core**: PQAS-enabled message gated on the real `experimental.pqc_demo` flag
26
+ - **finding-synthesizer**: detects real severity-gap contradictions (was hardcoded `[]`)
27
+ - **logic-drift-detector**: relabeled heuristic, not "Neural"
28
+ - **session-manager / shadow-mirror(docker) / regression-writer / skill-registry**: honest disclosure instead of silent empties / fake isolation / tautological tests / mock placeholders
29
+ - **MINDFORGE.md**: `[PQAS_ENFORCED]` reconciled to reflect simulated/inactive default; **ztai-manager** logs relabeled `[ZTAI-HSM-SIM]`
30
+
31
+ ### CI / hygiene
32
+
33
+ - **release workflow**: asserts the git tag matches `package.json` version (fail-closed) + skips publish if the version is already on npm
34
+ - **version-check**: runtime drift check widened to all 4 sources (was 2); SDK README guarded by test
35
+ - removed dead `AuditRotator` class (CHANGELOG had wrongly claimed it removed); deprecated orphaned `createAppendQueue`
36
+ - refreshed stale `v11.1.0` banners → current; added `cost_routing.shadow_mode` latch
37
+ - version bumped to 11.2.1 across all sources
38
+
39
+ ## [11.2.0] - 2026-05-31 — "Verification & Trust"
40
+
41
+ ### Added
42
+
43
+ - **UC-08 — Unified Verification Runner**
44
+ - `bin/engine/verification-runner.js`: orchestrates test/lint/audit/typecheck stages into structured results with pass/fail/skip per stage
45
+ - `bin/engine/verify-cli.js`: CLI entrypoint writing `.planning/VERIFICATION.md` reports
46
+ - `mindforge verify` command registered in CLI
47
+
48
+ - **UC-25 — Eval Harness**
49
+ - `bin/eval/eval-harness.js`: recall@k, nDCG (graded relevance), and `runEval()` orchestrator for measuring retrieval quality
50
+ - `bin/eval/golden-set-retrieval.json`: 10-query seed golden set covering orchestration, security, memory, cost, verification, hooks, and architecture domains
51
+
52
+ - **UC-22 — Tool/MCP Trust Boundaries**
53
+ - `bin/security/trust-boundaries.js`: manifest pinning (deterministic SHA-256 with recursive key sort), tamper detection, untrusted output tagging with provenance, and high-impact command detection
54
+ - `bin/security/trust-gate-hook.js`: PreToolUse hook that blocks destructive Bash commands (rm -rf, force-push, DROP TABLE, hard reset) via native Claude Code hooks
55
+ - Trust-gate registered in `.claude/settings.json` PreToolUse
56
+
57
+ - **Council CLI**
58
+ - `bin/council-cli.js`: thin wrapper wiring `runCouncil` to the `/mindforge:council` command with structured JSON output and formatted display
59
+
60
+ ### Fixed
61
+
62
+ - `auto-runner.js`: removed erroneous `new` on singleton ZTAIManager instance (was line 692)
63
+
64
+ ### Removed
65
+
66
+ - Dead `quantum-verify` CLI command entry (no handler existed)
67
+ - Dead `AuditRotator` class and its export from `bin/utils/file-io.js` (zero callers; rotation broke the hash chain at rotation boundaries — see `bin/autonomous/audit-writer.js`)
68
+
69
+ ### Changed
70
+
71
+ - Config `pqas_signing` provider clarified as "Dilithium-5 (simulated — inactive)"
72
+ - Banner version strings updated to v11.1.0 in self-corrective-synthesizer and remediation-engine
73
+ - SDK README heading updated to "New in v11.1.0"
74
+ - Version bumped to 11.2.0 across package.json and config.json
75
+
76
+ ---
77
+
78
+ ## [11.1.0] - 2026-05-31 — "Beast Mode"
79
+
80
+ ### Added
81
+
82
+ - **Pillar I — Integrity & Trust**
83
+ - `bin/utils/append-queue.js`: single-writer fsync'd append queue for concurrent-safe durable writes (UC-09)
84
+ - `bin/governance/audit-hash.js`: single canonical SHA-256 hasher shared by writer + verifier (UC-04)
85
+ - `bin/governance/audit-verifier.js` + `bin/verify-audit.js` CLI: fail-closed hash-chain verifier (`exit 1` on any break) (UC-04)
86
+ - All 7 audit-write paths unified through `appendAuditEntrySync` — the audit log is now genuinely tamper-evident with a verifiable prev-hash chain (UC-04b)
87
+ - Simulated PQC demoted: `pqas_enabled=false` by default, gated behind `experimental.pqc_demo`; Tier-3 uses real Ed25519 (UC-24)
88
+
89
+ - **Pillar II — Orchestration Correctness**
90
+ - `bin/autonomous/dependency-dag.js`: real Kahn topological sort + cycle detection, ported from test-only to the engine (UC-03)
91
+ - `planWaves(handoffs, {useDag:true})` opt-in DAG wave planning — explicit `.wave` always wins, legacy default unchanged (UC-03)
92
+ - Pre-flight cycle detection halts loud before any wave executes (UC-03)
93
+ - Wave-boundary timeout enforcement: `isTimedOut` + status `'timeout'` + resumable state (UC-14)
94
+ - Opt-in rollback hook records intent on terminal ESCALATE (no auto git-reset by default) (UC-14)
95
+ - `bin/engine/council-runtime.js`: minimal 4-voice council runtime (ADS loop) with injectable model, consensus scoring, dissent capture (UC-10)
96
+
97
+ - **Pillar III — Cost-Aware Routing**
98
+ - `bin/models/pricing-registry.js`: single source of truth for model pricing, loaded from `config.json` market_registry (UC-05)
99
+ - All 3 providers routed through the registry — hardcoded pricing eliminated (UC-05)
100
+ - Prompt-cache accounting: `cache_control:{type:'ephemeral'}` on system blocks, `cache_read`/`cache_creation` parsed + priced at ~10% input rate (UC-21)
101
+ - `bin/models/difficulty-scorer.js`: heuristic 1-10 scorer with Tier-3 floor (UC-06)
102
+ - Shadow-mode difficulty routing: logs intended model without changing actual selection (UC-06)
103
+
104
+ - **Pillar IV — Native Alignment + Observability**
105
+ - `.claude/settings.json` with hooks under real native events (`PostToolUse`/`PreToolUse`/`SessionStart`) — the security guard and context monitor now actually fire (UC-19a)
106
+ - `bin/hooks/instinct-capture-hook.js`: auto-captures behavioral patterns via PostToolUse, respects session limit (UC-11)
107
+ - `bin/engine/otel-exporter.js`: optional OTel GenAI exporter mapping NexusTracer spans to `gen_ai.*` semantic conventions, gated behind `OTEL_EXPORTER_OTLP_ENDPOINT` (UC-18)
108
+ - `bin/memory/retrieval-fusion.js`: Reciprocal Rank Fusion (RRF) over knowledge-graph + BM25 retrieval paths, replacing incomparable linear blends (UC-20)
109
+
110
+ ### Fixed
111
+
112
+ - Audit-writer `close()` data-loss bug: threshold-triggered flushes were un-awaited, losing up to 10 entries on close (UC-09)
113
+ - Audit-writer flush failure no longer crashes the process via unhandled rejection — logs to stderr (UC-09)
114
+ - Vector-hub exit guard: `_dirty` boolean → pending-saves counter, closing a window where scheduled-but-unwritten saves were lost on hard exit (UC-09)
115
+ - `isTimedOut` fails CLOSED on malformed `timeout_at` (garbage deadline = halt, not run unbounded) (UC-14)
116
+ - Council position validation rejects NaN confidence / invalid recommendations (UC-10)
117
+ - Dissent is now captured under NO_CONSENSUS verdict (the deadlock case where it matters most) (UC-10)
118
+ - Pre-flight DAG check only cycle-checks stable-id tasks (id-less tasks can't be dependency targets) (UC-03)
119
+ - `groupIntoWaves` self-defends: dangling dep throws "Unknown dependency" (not misleading "Circular") (UC-03)
120
+
121
+ ### Changed
122
+
123
+ - Audit rotation retired (it broke the hash chain at every 5000-line boundary); AUDIT.jsonl grows unbounded for now — chain-aware compaction is a deferred future feature (UC-04b)
124
+ - `knowledge-store.js` appends now use `appendDurableSync` (openSync+writeSync+fsyncSync+closeSync) for guaranteed durability (UC-09)
125
+ - `auto-shadow.js` `generateShadowContext()` now fuses retrieval paths via RRF instead of a single-path linear blend (UC-20)
126
+
127
+ ### Removed
128
+
129
+ - Orphaned `createAuditWriter` buffered path (zero production callers after UC-04b unification)
130
+ - Hardcoded per-token pricing from `anthropic-provider.js`, `openai-provider.js`, `gemini-provider.js` (replaced by PricingRegistry)
131
+
132
+ ## [11.0.1] - 2026-05-30 — "Stability Patch"
133
+
134
+ ### Fixed
135
+
136
+ - **Version drift**: reconciled `.mindforge/config.json` (was 10.7.0) with the 11.x line; added a fail-closed pre-flight version-consistency assertion (`bin/utils/version-check.js`) and a regression test that runs the migration and asserts its post-state, so the drift cannot silently return. (UC-01)
137
+ - **Lint & dead code**: resolved all 109 ESLint errors, removed orphaned `bin/dashboard/team-tracker.js`, and made CI fail on any lint error. Fixed a latent `no-const-assign` runtime crash in `bin/review/ads-engine.js`. (UC-02)
138
+ - **SDK `executeCommand` no-op**: replaced the published no-op stub (which made `batchExecute` report every task "fulfilled" while executing nothing) with a real `child_process.spawn` executor with stdout/stderr capture, timeout (SIGTERM→SIGKILL), and exit-code propagation; added a regression test against the compiled dist. (UC-07a)
139
+
3
140
  ## [11.0.0] - 2026-05-28 — "Sovereign Stability"
4
141
 
5
142
  ### Breaking Changes
package/MINDFORGE.md CHANGED
@@ -1,12 +1,12 @@
1
- # MINDFORGE.md — Parameter Registry (v11.0.0)
1
+ # MINDFORGE.md — Parameter Registry (v11.2.1)
2
2
 
3
3
  ## 1. IDENTITY & VERSIONING
4
4
 
5
5
  [NAME] = MindForge
6
- [VERSION] = 11.0.0
6
+ [VERSION] = 11.2.1
7
7
  [STABLE] = true
8
8
  [MODE] = "Platform Sovereign"
9
- [REQUIRED_CORE_VERSION] = 11.0.0
9
+ [REQUIRED_CORE_VERSION] = 11.2.1
10
10
  [SOVEREIGN_IDENTITY] = true
11
11
  [SRE_LAYER_ENABLED] = true
12
12
 
@@ -26,7 +26,7 @@
26
26
  [ZTAI_KEY_TYPE] = "Dilithium-5"
27
27
  [NEXUS_TRACE_RETENTION_DAYS] = 30
28
28
  [CADIA_CORE] = true
29
- [PQAS_ENFORCED] = true
29
+ [PQAS_ENFORCED] = false # PQAS is SIMULATED/inactive by default (config: pqas_enabled=false, gated behind experimental.pqc_demo). Tier-3 trust uses real Ed25519. See .mindforge/config.json + bin/governance/quantum-crypto.js.
30
30
  [PROACTIVE_HOMING] = true
31
31
 
32
32
  ---
@@ -100,6 +100,6 @@ The following parameters cannot be overridden by plugins, agents, or session-lev
100
100
  - [MIN_SOUL_SCORE] — Minimum SOUL score required for architectural changes
101
101
  - [BLOCK_ON_SECURITY] — Security gate enforcement cannot be disabled
102
102
  - [COST_HARD_LIMIT_USD] — Hard cost limit cannot be raised without human approval
103
- - [PQAS_ENFORCED] Post-quantum security cannot be disabled
103
+ - [BLOCK_ON_SECURITY] is non-overridable; PQAS itself is simulated/experimental (inactive by default) and is NOT a non-overridable guarantee — do not rely on it as an enforced control
104
104
  - [SOVEREIGN_IDENTITY] — Identity verification is always required
105
105
  - [ENABLE_ZTAI] — Zero-trust identity cannot be bypassed
package/RELEASENOTES.md CHANGED
@@ -266,7 +266,7 @@ If your scripts call `mindforge sync-jira` or `mindforge sync-confluence`, remov
266
266
  ### Step 6: Verify
267
267
 
268
268
  ```bash
269
- npx mindforge-cc --version # Should print 10.0.1
269
+ npx mindforge-cc --version # Should print 11.0.0
270
270
  npm test # All 41 tests should pass
271
271
  ```
272
272
 
@@ -1,103 +1,125 @@
1
1
  /**
2
- * MindForge — Audit Writer (Async Buffered)
3
- * Extracted from auto-runner.js — handles all JSONL audit append operations.
4
- * Buffers entries and flushes every 100ms or when buffer reaches 10 entries.
2
+ * MindForge — Audit Writer (Synchronous Durable, Hash-Chained)
3
+ *
4
+ * Provides the ONE unified audit-append primitive {@link appendAuditEntrySync}
5
+ * that every audit write site funnels through, producing a single verifiable
6
+ * hash chain (UC-04 / UC-04b).
7
+ *
8
+ * Retired in UC-04b: the old buffered async writer (`createAuditWriter`) and its
9
+ * AuditRotator-based 5000-line rotation. Rotation BROKE the hash chain — archiving
10
+ * + truncating AUDIT.jsonl orphaned the carried head's `previous_hash` from an entry
11
+ * no longer on disk, so the verifier failed closed on a rotated-but-untampered file.
12
+ * As a result AUDIT.jsonl now grows UNBOUNDED. That is an accepted short-term tradeoff
13
+ * at single-operator/dev scale. The proper fix — chain-aware compaction (archive old
14
+ * entries AND re-anchor the first carried entry to previous_hash=null so the live tail
15
+ * verifies standalone) — is a DEFERRED future feature, intentionally NOT in scope here.
5
16
  */
6
17
  'use strict';
7
18
 
8
19
  const fs = require('fs');
9
20
  const path = require('path');
10
21
  const crypto = require('crypto');
11
- const { AuditRotator } = require('../utils/file-io');
12
-
13
- const FLUSH_INTERVAL_MS = 100;
14
- const FLUSH_THRESHOLD = 10;
15
-
16
- const rotator = new AuditRotator({ maxLines: 5000 });
22
+ const { hashAuditEntry } = require('../governance/audit-hash');
17
23
 
18
24
  /**
19
- * Creates a buffered async audit writer.
20
- * @param {string} auditPath Path to the AUDIT.jsonl file
21
- * @returns {{ write: (entry: object) => void, flush: () => Promise<void>, close: () => Promise<void> }}
25
+ * Computes the SHA-256 hash of an entry chained to its predecessor (UC-04).
26
+ * Delegates to the canonical {@link hashAuditEntry} (bin/governance/audit-hash.js)
27
+ * so the writer and verifier share ONE hasher material drift is impossible.
28
+ * The material is {...entry, previous_hash} where `entry` does NOT contain `_hash`.
29
+ * @param {object} entry — entry WITHOUT a `_hash` field
30
+ * @param {string|null} previousHash — prior entry's `_hash` (null for the first link)
31
+ * @returns {string} hex-encoded SHA-256 digest
22
32
  */
23
- function createAuditWriter(auditPath) {
24
- let buffer = [];
25
- let flushTimer = null;
26
- let isClosed = false;
27
-
28
- function scheduleFlush() {
29
- if (flushTimer !== null) return;
30
- flushTimer = setTimeout(async () => {
31
- flushTimer = null;
32
- await flush();
33
- }, FLUSH_INTERVAL_MS);
34
- }
35
-
36
- /**
37
- * Adds an entry to the buffer. Triggers flush if threshold reached.
38
- * Stamps entry with id and timestamp if missing (immutable — creates new object).
39
- */
40
- function write(entry) {
41
- if (isClosed) {
42
- throw new Error('AuditWriter is closed — cannot write after close()');
43
- }
44
-
45
- const stamped = Object.assign(Object.create(null), entry, {
46
- id: entry.id || crypto.randomBytes(8).toString('hex'),
47
- timestamp: entry.timestamp || new Date().toISOString(),
48
- });
49
-
50
- buffer = [...buffer, stamped];
51
-
52
- if (buffer.length >= FLUSH_THRESHOLD) {
53
- // Immediate flush — don't wait for timer
54
- if (flushTimer !== null) {
55
- clearTimeout(flushTimer);
56
- flushTimer = null;
57
- }
58
- flush();
59
- } else {
60
- scheduleFlush();
61
- }
62
-
63
- return stamped;
64
- }
65
-
66
- /**
67
- * Flushes the buffer to disk using async appendFile.
68
- */
69
- async function flush() {
70
- if (buffer.length === 0) return;
71
-
72
- const toWrite = buffer;
73
- buffer = [];
33
+ function hashEntry(entry, previousHash) {
34
+ return hashAuditEntry(entry, previousHash);
35
+ }
74
36
 
75
- const payload = toWrite.map(e => JSON.stringify(e)).join('\n') + '\n';
76
- await fs.promises.appendFile(auditPath, payload);
37
+ /**
38
+ * Reads the `_hash` of the last entry already on disk so a re-opened writer
39
+ * continues the existing chain instead of starting a fresh, disconnected one.
40
+ * @param {string} auditPath — path to the AUDIT.jsonl file
41
+ * @returns {string|null} last `_hash`, or null if absent/unreadable/empty
42
+ */
43
+ function readLastHash(auditPath) {
44
+ try {
45
+ const lines = fs.readFileSync(auditPath, 'utf8').split('\n').filter(Boolean);
46
+ if (lines.length === 0) return null;
47
+ const last = JSON.parse(lines[lines.length - 1]);
48
+ return last._hash || null;
49
+ } catch { return null; }
50
+ }
77
51
 
78
- try {
79
- if (rotator.shouldRotate(auditPath)) {
80
- const archiveDir = path.join(path.dirname(auditPath), '..', '.planning', 'audit-archive');
81
- rotator.rotate(auditPath, archiveDir);
82
- }
83
- } catch (err) {
84
- process.stderr.write(`[audit-writer] rotation warning: ${err.message}\n`);
85
- }
86
- }
52
+ // ── Unified synchronous-durable chained append (UC-04b) ───────────────────────
53
+ // ONE primitive that ALL audit write sites funnel through, producing a single
54
+ // verifiable chain via the canonical hashAuditEntry. Synchronous + fsync'd so an
55
+ // acknowledged write is on disk before the call returns (UC-09 durability) — this
56
+ // is what lets us delete the old raw `appendFileSync` shadow-writes: the durable
57
+ // sync write gives in-process consumers (e.g. StuckMonitor, which is fed the event
58
+ // object directly but may also re-read the file) immediate, durable data.
59
+ //
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.
66
+ //
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
87
73
 
88
- /**
89
- * Flushes remaining entries and stops the timer. After close(), write() will throw.
90
- */
91
- async function close() {
92
- isClosed = true;
93
- if (flushTimer !== null) {
94
- clearTimeout(flushTimer);
95
- flushTimer = null;
96
- }
97
- await flush();
74
+ /**
75
+ * Synchronously appends ONE hash-chained, durable entry to an audit JSONL file.
76
+ * This is the single unified append used by every audit write site (UC-04b).
77
+ *
78
+ * NOTE: performs an fsync (openSync('a') + writeSync + fsyncSync + closeSync) on
79
+ * EVERY call. This is deliberate for audit integrity/durability — but it makes the
80
+ * call relatively expensive, so it is intended for audit-grade events, NOT for
81
+ * high-frequency telemetry. Hot-path callers (e.g. nexus-tracer span/reasoning
82
+ * events) pay one fsync per event; keep that cost in mind before adding new hot
83
+ * write sites.
84
+ *
85
+ * @param {string} auditPath — path to the AUDIT.jsonl file
86
+ * @param {object} event — the event payload (id/timestamp stamped if missing)
87
+ * @returns {object} the stamped + chained entry actually written
88
+ */
89
+ function appendAuditEntrySync(auditPath, event) {
90
+ const resolved = path.resolve(auditPath);
91
+
92
+ // 1. Stamp id + timestamp if missing — immutable (new object, never mutate input).
93
+ const stamped = {
94
+ ...event,
95
+ id: event.id || crypto.randomBytes(8).toString('hex'),
96
+ timestamp: event.timestamp || new Date().toISOString(),
97
+ };
98
+
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);
104
+
105
+ // 3. Compute _hash over {...stamped, previous_hash} WITHOUT _hash in the material.
106
+ const _hash = hashEntry(stamped, previous_hash);
107
+
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);
98
118
  }
99
119
 
100
- return Object.freeze({ write, flush, close });
120
+ // 5. Advance the in-process chain head and return the written entry.
121
+ _lastHashCache.set(resolved, _hash);
122
+ return chained;
101
123
  }
102
124
 
103
- module.exports = { createAuditWriter };
125
+ module.exports = { appendAuditEntrySync };