mindforge-cc 11.0.0 → 11.2.0
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.
- package/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/config.json +13 -4
- package/CHANGELOG.md +101 -0
- package/MINDFORGE.md +3 -3
- package/RELEASENOTES.md +1 -1
- package/bin/autonomous/audit-writer.js +108 -86
- package/bin/autonomous/auto-runner.js +304 -19
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/wave-executor.js +20 -1
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/server.js +1 -1
- package/bin/dashboard/sse-bridge.js +9 -12
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/verification-runner.js +131 -0
- package/bin/engine/verify-cli.js +34 -0
- package/bin/eval/eval-harness.js +82 -0
- package/bin/eval/golden-set-retrieval.json +46 -0
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/quantum-crypto.js +63 -9
- package/bin/governance/ztai-manager.js +30 -2
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-store.js +30 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +111 -6
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +13 -4
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +6 -2
- package/bin/models/model-router.js +31 -18
- package/bin/models/openai-provider.js +6 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/security/trust-boundaries.js +102 -0
- package/bin/security/trust-gate-hook.js +39 -0
- package/bin/skill-registry.js +3 -2
- package/bin/skills-builder/marketplace-cli.js +5 -3
- package/bin/skills-builder/skill-registrar.js +4 -6
- package/bin/sre/sentinel.js +7 -5
- package/bin/utils/append-queue.js +55 -0
- package/bin/utils/file-io.js +27 -37
- package/bin/utils/version-check.js +59 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/package.json +1 -1
- 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
|
package/.mindforge/config.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "11.2.0",
|
|
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:
|
|
8
|
+
"active_did": "did:mindforge:0f5f4777-ffd4-4aef-aa46-e8eb34c0e8c0"
|
|
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":
|
|
65
|
+
"pqas_enabled": false,
|
|
60
66
|
"enclave_tier": 2,
|
|
61
|
-
"provider": "simulated
|
|
67
|
+
"provider": "Dilithium-5 (simulated — inactive)"
|
|
68
|
+
},
|
|
69
|
+
"experimental": {
|
|
70
|
+
"pqc_demo": false
|
|
62
71
|
},
|
|
63
72
|
"mesh": {
|
|
64
73
|
"node_id": "beta-node",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [11.2.0] - 2026-05-31 — "Verification & Trust"
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **UC-08 — Unified Verification Runner**
|
|
8
|
+
- `bin/engine/verification-runner.js`: orchestrates test/lint/audit/typecheck stages into structured results with pass/fail/skip per stage
|
|
9
|
+
- `bin/engine/verify-cli.js`: CLI entrypoint writing `.planning/VERIFICATION.md` reports
|
|
10
|
+
- `mindforge verify` command registered in CLI
|
|
11
|
+
|
|
12
|
+
- **UC-25 — Eval Harness**
|
|
13
|
+
- `bin/eval/eval-harness.js`: recall@k, nDCG (graded relevance), and `runEval()` orchestrator for measuring retrieval quality
|
|
14
|
+
- `bin/eval/golden-set-retrieval.json`: 10-query seed golden set covering orchestration, security, memory, cost, verification, hooks, and architecture domains
|
|
15
|
+
|
|
16
|
+
- **UC-22 — Tool/MCP Trust Boundaries**
|
|
17
|
+
- `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
|
|
18
|
+
- `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
|
|
19
|
+
- Trust-gate registered in `.claude/settings.json` PreToolUse
|
|
20
|
+
|
|
21
|
+
- **Council CLI**
|
|
22
|
+
- `bin/council-cli.js`: thin wrapper wiring `runCouncil` to the `/mindforge:council` command with structured JSON output and formatted display
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `auto-runner.js`: removed erroneous `new` on singleton ZTAIManager instance (was line 692)
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- Dead `quantum-verify` CLI command entry (no handler existed)
|
|
31
|
+
- Dead `AuditRotator` references (broke hash chain on rotation boundaries)
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Config `pqas_signing` provider clarified as "Dilithium-5 (simulated — inactive)"
|
|
36
|
+
- Banner version strings updated to v11.1.0 in self-corrective-synthesizer and remediation-engine
|
|
37
|
+
- SDK README heading updated to "New in v11.1.0"
|
|
38
|
+
- Version bumped to 11.2.0 across package.json and config.json
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## [11.1.0] - 2026-05-31 — "Beast Mode"
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Pillar I — Integrity & Trust**
|
|
47
|
+
- `bin/utils/append-queue.js`: single-writer fsync'd append queue for concurrent-safe durable writes (UC-09)
|
|
48
|
+
- `bin/governance/audit-hash.js`: single canonical SHA-256 hasher shared by writer + verifier (UC-04)
|
|
49
|
+
- `bin/governance/audit-verifier.js` + `bin/verify-audit.js` CLI: fail-closed hash-chain verifier (`exit 1` on any break) (UC-04)
|
|
50
|
+
- All 7 audit-write paths unified through `appendAuditEntrySync` — the audit log is now genuinely tamper-evident with a verifiable prev-hash chain (UC-04b)
|
|
51
|
+
- Simulated PQC demoted: `pqas_enabled=false` by default, gated behind `experimental.pqc_demo`; Tier-3 uses real Ed25519 (UC-24)
|
|
52
|
+
|
|
53
|
+
- **Pillar II — Orchestration Correctness**
|
|
54
|
+
- `bin/autonomous/dependency-dag.js`: real Kahn topological sort + cycle detection, ported from test-only to the engine (UC-03)
|
|
55
|
+
- `planWaves(handoffs, {useDag:true})` opt-in DAG wave planning — explicit `.wave` always wins, legacy default unchanged (UC-03)
|
|
56
|
+
- Pre-flight cycle detection halts loud before any wave executes (UC-03)
|
|
57
|
+
- Wave-boundary timeout enforcement: `isTimedOut` + status `'timeout'` + resumable state (UC-14)
|
|
58
|
+
- Opt-in rollback hook records intent on terminal ESCALATE (no auto git-reset by default) (UC-14)
|
|
59
|
+
- `bin/engine/council-runtime.js`: minimal 4-voice council runtime (ADS loop) with injectable model, consensus scoring, dissent capture (UC-10)
|
|
60
|
+
|
|
61
|
+
- **Pillar III — Cost-Aware Routing**
|
|
62
|
+
- `bin/models/pricing-registry.js`: single source of truth for model pricing, loaded from `config.json` market_registry (UC-05)
|
|
63
|
+
- All 3 providers routed through the registry — hardcoded pricing eliminated (UC-05)
|
|
64
|
+
- Prompt-cache accounting: `cache_control:{type:'ephemeral'}` on system blocks, `cache_read`/`cache_creation` parsed + priced at ~10% input rate (UC-21)
|
|
65
|
+
- `bin/models/difficulty-scorer.js`: heuristic 1-10 scorer with Tier-3 floor (UC-06)
|
|
66
|
+
- Shadow-mode difficulty routing: logs intended model without changing actual selection (UC-06)
|
|
67
|
+
|
|
68
|
+
- **Pillar IV — Native Alignment + Observability**
|
|
69
|
+
- `.claude/settings.json` with hooks under real native events (`PostToolUse`/`PreToolUse`/`SessionStart`) — the security guard and context monitor now actually fire (UC-19a)
|
|
70
|
+
- `bin/hooks/instinct-capture-hook.js`: auto-captures behavioral patterns via PostToolUse, respects session limit (UC-11)
|
|
71
|
+
- `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)
|
|
72
|
+
- `bin/memory/retrieval-fusion.js`: Reciprocal Rank Fusion (RRF) over knowledge-graph + BM25 retrieval paths, replacing incomparable linear blends (UC-20)
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- Audit-writer `close()` data-loss bug: threshold-triggered flushes were un-awaited, losing up to 10 entries on close (UC-09)
|
|
77
|
+
- Audit-writer flush failure no longer crashes the process via unhandled rejection — logs to stderr (UC-09)
|
|
78
|
+
- Vector-hub exit guard: `_dirty` boolean → pending-saves counter, closing a window where scheduled-but-unwritten saves were lost on hard exit (UC-09)
|
|
79
|
+
- `isTimedOut` fails CLOSED on malformed `timeout_at` (garbage deadline = halt, not run unbounded) (UC-14)
|
|
80
|
+
- Council position validation rejects NaN confidence / invalid recommendations (UC-10)
|
|
81
|
+
- Dissent is now captured under NO_CONSENSUS verdict (the deadlock case where it matters most) (UC-10)
|
|
82
|
+
- Pre-flight DAG check only cycle-checks stable-id tasks (id-less tasks can't be dependency targets) (UC-03)
|
|
83
|
+
- `groupIntoWaves` self-defends: dangling dep throws "Unknown dependency" (not misleading "Circular") (UC-03)
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- 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)
|
|
88
|
+
- `knowledge-store.js` appends now use `appendDurableSync` (openSync+writeSync+fsyncSync+closeSync) for guaranteed durability (UC-09)
|
|
89
|
+
- `auto-shadow.js` `generateShadowContext()` now fuses retrieval paths via RRF instead of a single-path linear blend (UC-20)
|
|
90
|
+
|
|
91
|
+
### Removed
|
|
92
|
+
|
|
93
|
+
- Orphaned `createAuditWriter` buffered path (zero production callers after UC-04b unification)
|
|
94
|
+
- Hardcoded per-token pricing from `anthropic-provider.js`, `openai-provider.js`, `gemini-provider.js` (replaced by PricingRegistry)
|
|
95
|
+
|
|
96
|
+
## [11.0.1] - 2026-05-30 — "Stability Patch"
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **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)
|
|
101
|
+
- **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)
|
|
102
|
+
- **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)
|
|
103
|
+
|
|
3
104
|
## [11.0.0] - 2026-05-28 — "Sovereign Stability"
|
|
4
105
|
|
|
5
106
|
### Breaking Changes
|
package/MINDFORGE.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# MINDFORGE.md — Parameter Registry (v11.
|
|
1
|
+
# MINDFORGE.md — Parameter Registry (v11.2.0)
|
|
2
2
|
|
|
3
3
|
## 1. IDENTITY & VERSIONING
|
|
4
4
|
|
|
5
5
|
[NAME] = MindForge
|
|
6
|
-
[VERSION] = 11.
|
|
6
|
+
[VERSION] = 11.2.0
|
|
7
7
|
[STABLE] = true
|
|
8
8
|
[MODE] = "Platform Sovereign"
|
|
9
|
-
[REQUIRED_CORE_VERSION] = 11.
|
|
9
|
+
[REQUIRED_CORE_VERSION] = 11.2.0
|
|
10
10
|
[SOVEREIGN_IDENTITY] = true
|
|
11
11
|
[SRE_LAYER_ENABLED] = true
|
|
12
12
|
|
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
|
|
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 (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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 = {
|
|
125
|
+
module.exports = { appendAuditEntrySync };
|