mindforge-cc 11.2.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.
@@ -70,15 +70,41 @@ function tagUntrusted(content, meta) {
70
70
  // character in a regex literal (eslint no-control-regex).
71
71
  const NUL = String.fromCharCode(0);
72
72
 
73
+ // ${IFS} / $IFS are shell-internal field separators that expand to whitespace.
74
+ // Attackers use them to avoid literal spaces between a destructive token and
75
+ // its flags (rm${IFS}-rf${IFS}/). We normalize them back to a space so the
76
+ // existing rm/-rf patterns catch the de-obfuscated form (audit #8).
77
+ const IFS_TOKEN = /\$\{IFS\}|\$IFS/g;
78
+
79
+ /**
80
+ * De-obfuscates shell metacharacter tricks WITHOUT emulating a real shell.
81
+ * Strips quotes (' ") and backslash escapes, collapses ${IFS}/$IFS to a space,
82
+ * then collapses runs of whitespace. This turns r''m, r"m, r\m and
83
+ * rm${IFS}-rf${IFS}/ back into plain `rm -rf /` so the existing patterns fire.
84
+ * Intentionally conservative: it removes characters rather than interpreting
85
+ * them, which can only make a string MORE likely to match (fail-toward-block).
86
+ */
87
+ function normalizeShell(input) {
88
+ return input
89
+ .split(NUL).join('') // shells ignore NUL; never let it split a token
90
+ .replace(IFS_TOKEN, ' ') // ${IFS}/$IFS -> space
91
+ .replace(/[\\'"]/g, '') // drop backslash escapes and quote chars
92
+ .replace(/\s+/g, ' '); // collapse whitespace runs
93
+ }
94
+
73
95
  /**
74
96
  * Detects high-impact / destructive commands via case-insensitive pattern matching.
75
97
  * Returns true if the command matches known destructive patterns.
98
+ *
99
+ * The detector errs toward blocking by design: this feeds a PreToolUse gate
100
+ * where approval friction is strictly preferable to a destructive bypass.
76
101
  */
77
102
  function isHighImpact(command) {
78
- // Strip null bytes first shells ignore them, so an attacker must not be
79
- // able to use a NUL to split a destructive token and slip past the patterns.
80
- const sanitized = String(command).split(NUL).join('');
103
+ // Normalize first so metacharacter-obfuscated commands (audit #8) are matched
104
+ // by the SAME pattern set as their plain-text equivalents.
105
+ const sanitized = normalizeShell(String(command));
81
106
  const patterns = [
107
+ // ── Original allowlist (unchanged) ──────────────────────────────────────
82
108
  /rm\s+(-\w*r\w*\s+-\w*f|(-\w*f\w*\s+-\w*r)|-\w*rf|-\w*fr)/i,
83
109
  /git\s+push\s+.*--force/i,
84
110
  /git\s+push\s+.*-f/i,
@@ -87,9 +113,75 @@ function isHighImpact(command) {
87
113
  /delete\s+from/i,
88
114
  /truncate\s+table/i,
89
115
  /\bmkfs(\.\w+)?\s+\/dev\//i,
90
- /\bdd\b.*\bof=\/dev\//i,
116
+ // #11: any dd write target, not just /dev/ (dd if=... of=important.db).
117
+ // Original /dev/-only check is a subset of this, so it stays covered.
118
+ /\bdd\b.*\bof=/i,
91
119
  /\b(curl|wget)\b.*\|\s*(bash|sh|zsh)\b/i,
92
120
  /^\s*find\s+.*-delete\b/i,
121
+
122
+ // ── #4 Command/process substitution RCE ─────────────────────────────────
123
+ // `eval` anywhere is high-impact (dynamic code execution).
124
+ /\beval\b/i,
125
+ // Command substitution $(...) or backtick combined with a network fetch.
126
+ /\$\(\s*(curl|wget)\b/i,
127
+ new RegExp(String.fromCharCode(96) + '\\s*(curl|wget)\\b', 'i'),
128
+ // Process substitution feeding an interpreter: bash <(...), sh <(...).
129
+ /\b(bash|sh|zsh)\b.*<\(/i,
130
+ // Curl/wget directly inside a process substitution.
131
+ /<\(\s*(curl|wget)\b/i,
132
+
133
+ // ── #5 Interpreter invocation of a script file ──────────────────────────
134
+ // source <file> and `. <file>` are unambiguous script execution.
135
+ /\bsource\s+\S+/i,
136
+ /(^|[;&|]|\s)\.\s+\S+\.\w+/i,
137
+ // bash/sh/zsh running a .sh script — clearly script execution. Kept broad
138
+ // (any .sh) because shelling out to an arbitrary shell script is itself a
139
+ // strong execution signal; this also covers untrusted paths like
140
+ // `bash /tmp/x.sh`.
141
+ /\b(bash|sh|zsh)\s+\S*\.sh\b/i,
142
+ // node/python/etc. running a script — narrowed (UC-22). Running a project
143
+ // file is THE default safe action in a Node/Python repo, so a blanket
144
+ // `node <file>.js` match is a terrible signal-to-noise ratio and blocked
145
+ // the project's OWN idiom (`node tests/run-all.js`). We now flag ONLY when
146
+ // the script path looks UNTRUSTED — an absolute path (/...), a writable
147
+ // temp dir (/tmp, /var/tmp, /dev/shm), or a home-relative path (~/...) —
148
+ // i.e. the write-then-execute attack chain (drop payload in a writable
149
+ // location, then run it). Project-relative paths (tests/run-all.js,
150
+ // bin/foo.js, scripts/build.py, index.js) are NOT matched. Piped/
151
+ // substituted execution (curl | bash, $(...), <(...), backticks) is
152
+ // already covered by the #4 patterns above.
153
+ /\b(node|python3?|ruby|perl)\s+(~\/|\/)\S*\.(js|mjs|cjs|ts|py|rb|pl)\b/i,
154
+
155
+ // ── #7 Redirect-overwrite of critical files / devices ───────────────────
156
+ // > or >> targeting an absolute sensitive path (/etc/, /dev/, /boot/, /sys/,
157
+ // /proc/, /var/, /usr/, /bin/, /sbin/, /lib/). Project-local redirects
158
+ // (> out.log) are NOT matched.
159
+ />>?\s*\/(etc|dev|boot|sys|proc|var|usr|bin|sbin|lib|root|lib64)\//i,
160
+
161
+ // ── #8 handled by normalizeShell() above (de-obfuscation), no pattern here.
162
+
163
+ // ── #9 chmod dangerous modes ────────────────────────────────────────────
164
+ // Canonical dangerous octal modes only: 000 (full lockout) and the
165
+ // world-writable 666/777 family. Common safe modes (644/755/600/700/640)
166
+ // and symbolic modes (chmod +x build.sh) are intentionally NOT matched.
167
+ // ANY recursive chmod is also treated as high-impact regardless of mode.
168
+ /\bchmod\b.*\b(000|0?666|0?777)\b/i,
169
+ /\bchmod\s+-\w*[rR]/i,
170
+
171
+ // ── #10 chown recursive ─────────────────────────────────────────────────
172
+ /\bchown\s+-\w*[rR]/i,
173
+
174
+ // ── #12 mv of root / into /dev/null ─────────────────────────────────────
175
+ /\bmv\b.*\/dev\/null\b/i,
176
+ /\bmv\s+(-\w+\s+)?\/\s/i,
177
+
178
+ // ── #13 Process killers ─────────────────────────────────────────────────
179
+ /\bkill\s+-9\s+-1\b/i,
180
+ /\bkillall\b/i,
181
+ /\bpkill\b/i,
182
+
183
+ // ── #14 Power-state commands ────────────────────────────────────────────
184
+ /\b(shutdown|reboot|halt|poweroff)\b/i,
93
185
  ];
94
186
  return patterns.some(pattern => pattern.test(sanitized));
95
187
  }
@@ -16,13 +16,23 @@ process.stdin.on('end', () => {
16
16
  }
17
17
 
18
18
  const fullCommand = event.tool_input?.command || '';
19
- const command = fullCommand.split('\n')[0];
20
19
 
21
- if (isHighImpact(command)) {
20
+ // Check the whole command AND every individual line, blocking if ANY
21
+ // segment is high-impact. Per-line scanning means a benign first line
22
+ // cannot cloak a destructive command on a later line; the whole-string
23
+ // check catches patterns a line split might fragment. This is a security
24
+ // gate, so it errs toward blocking: a destructive keyword in (e.g.) a
25
+ // commit message will prompt for approval rather than risk a cloaked
26
+ // command slipping through. Approval friction is preferable to a bypass.
27
+ const lines = fullCommand.split('\n');
28
+ const offending = [fullCommand, ...lines].find((segment) => isHighImpact(segment));
29
+
30
+ if (offending) {
31
+ const display = offending.length > 80 ? offending.slice(0, 80) + '...' : offending;
22
32
  // Output a block reason (Claude Code shows this to the user)
23
33
  process.stdout.write(JSON.stringify({
24
34
  decision: 'block',
25
- reason: `[TrustGate] High-impact command detected: "${command.substring(0, 80)}..." — requires explicit user approval`
35
+ reason: `[TrustGate] High-impact command detected: "${display}" — requires explicit user approval`
26
36
  }));
27
37
  process.exit(2); // block
28
38
  }
@@ -124,31 +124,42 @@ function handleInstall() {
124
124
  if (tier === '1') targetBase = '.mindforge/skills';
125
125
  if (tier === '3') targetBase = '.mindforge/project-skills';
126
126
 
127
+ // Resolve a real local source for the skill. There is no remote registry
128
+ // backend, so the ONLY honest sources are an examples/ entry or a SKILL.md in
129
+ // the current working directory (the agentic authoring flow).
130
+ const exampleSrc = path.join(process.cwd(), 'examples', 'skills', `${skillName}.md`);
131
+ const localSrc = path.join(process.cwd(), 'SKILL.md');
132
+
133
+ let source = null;
134
+ if (fs.existsSync(exampleSrc)) {
135
+ source = exampleSrc;
136
+ } else if (fs.existsSync(localSrc)) {
137
+ source = localSrc;
138
+ }
139
+
140
+ if (!source) {
141
+ // HONEST REFUSAL (UC-22): no local source and no remote registry is
142
+ // configured. Do NOT write a placeholder file that masquerades as an
143
+ // installed skill — that would falsely report success. Refuse clearly and
144
+ // exit non-zero so callers can detect the failure.
145
+ console.error(
146
+ `❌ Skill '${skillName}' not found locally and no remote registry is configured — nothing installed.`
147
+ );
148
+ console.error(
149
+ ' Provide a source first: place a SKILL.md in the current directory ' +
150
+ `or add examples/skills/${skillName}.md, then re-run.`
151
+ );
152
+ process.exit(1);
153
+ }
154
+
155
+ // Real source found → perform the actual install.
127
156
  const targetDir = path.join(process.cwd(), targetBase, skillName);
128
157
  if (!fs.existsSync(targetDir)) {
129
158
  fs.mkdirSync(targetDir, { recursive: true });
130
159
  }
131
-
132
- // Mock behavior for Day 3/4 testing if no real registry
133
- // Expect source to be in examples/ if it exists, otherwise just touch it
134
- const mockSrc = path.join(process.cwd(), 'examples', 'skills', `${skillName}.md`);
135
160
  const targetFile = path.join(targetDir, 'SKILL.md');
136
-
137
- if (fs.existsSync(mockSrc)) {
138
- fs.copyFileSync(mockSrc, targetFile);
139
- console.log(` ✅ Copied from ${mockSrc}`);
140
- } else {
141
- // If no source, we look for it in the current dir for the agentic flow
142
- const localSrc = path.join(process.cwd(), 'SKILL.md');
143
- if (fs.existsSync(localSrc)) {
144
- fs.copyFileSync(localSrc, targetFile);
145
- console.log(' ✅ Copied from local SKILL.md');
146
- } else {
147
- // Just create a placeholder for testing if nothing else
148
- fs.writeFileSync(targetFile, `---\nname: ${skillName}\nversion: 1.0.0\nstatus: alpha\ntriggers: test, mock, placeholder\n---\n# ${skillName}\nPlaceholder for ${skillName} skill.\n`);
149
- console.log(' ⚠️ Created mock SKILL.md (no source found)');
150
- }
151
- }
161
+ fs.copyFileSync(source, targetFile);
162
+ console.log(` ✅ Installed ${skillName} from ${path.relative(process.cwd(), source)}`);
152
163
 
153
164
  process.exit(0);
154
165
  }
@@ -1,13 +1,28 @@
1
1
  /**
2
2
  * MindForge v9.0.0 — Temporal Shadow Mirror (Pillar XXI)
3
- * Hybrid isolation for incident replication.
3
+ *
4
+ * Incident replication via git-worktree isolation.
5
+ *
6
+ * ISOLATION HONESTY (audit finding #24 / UC-22):
7
+ * This module provides GIT WORKTREE isolation only — a separate working tree
8
+ * and branch off the current repo. It does NOT provide container/Docker
9
+ * isolation (no separate kernel, network, filesystem, or process namespaces).
10
+ * A `docker --version` probe gates an "enhanced" path, but that path still runs
11
+ * in a git worktree; real Docker orchestration (build/run/mount) is a
12
+ * NOT-YET-IMPLEMENTED future enhancement. The returned mirror is always a
13
+ * worktree, and `activeMirror.isolation` reports the actual level provided.
4
14
  */
5
15
  'use strict';
6
16
 
7
- const { execSync } = require('node:child_process');
17
+ const { execSync, execFileSync } = require('node:child_process');
8
18
  const fs = require('node:fs');
9
19
  const path = require('node:path');
10
- const crypto = require('node:crypto');
20
+
21
+ // remediation_id flows from the SRE sentinel (a trust boundary) into git
22
+ // branch/worktree names. Only allow a safe identifier charset and reject
23
+ // anything that could be read as a flag or shell metacharacter. Defence in
24
+ // depth on top of execFileSync (argv array, no shell). Fail-closed.
25
+ const SAFE_REMEDIATION_ID = /^[A-Za-z0-9._-]+$/;
11
26
 
12
27
  class ShadowMirror {
13
28
  constructor(options = {}) {
@@ -15,32 +30,58 @@ class ShadowMirror {
15
30
  this.activeMirror = null;
16
31
  }
17
32
 
33
+ /**
34
+ * Validates a remediation_id (trust-boundary input) before it is used to
35
+ * build git branch/worktree names. Fail-closed: throws on missing, empty,
36
+ * overlong, flag-like, or metacharacter-bearing ids. Returns the id on pass.
37
+ * Static so it can be unit-tested in isolation.
38
+ */
39
+ static sanitizeRemediationId(id) {
40
+ if (typeof id !== 'string' || id.length === 0 || id.length > 128) {
41
+ throw new Error('[ShadowMirror] invalid remediation_id: must be a non-empty string of <=128 chars');
42
+ }
43
+ if (id.startsWith('-')) {
44
+ throw new Error('[ShadowMirror] invalid remediation_id: must not start with "-"');
45
+ }
46
+ if (!SAFE_REMEDIATION_ID.test(id)) {
47
+ throw new Error('[ShadowMirror] invalid remediation_id: only [A-Za-z0-9._-] characters allowed');
48
+ }
49
+ return id;
50
+ }
51
+
18
52
  /**
19
53
  * Orchestrates replication based on incident severity and requirements.
20
54
  */
21
55
  async replicate(incident) {
22
56
  console.log(`🌀 Shadow Mirror: Replicating incident [${incident.remediation_id}]...`);
23
-
24
- // Choose isolation level
25
- const level = (incident.details?.severity === 'CRITICAL') ? 2 : 1;
26
-
27
- if (level === 2 && this.isDockerAvailable()) {
28
- return this.replicateLevel2(incident);
29
- } else {
30
- return this.replicateLevel1(incident);
57
+
58
+ // All replication currently runs in a git worktree (see file header).
59
+ // For CRITICAL incidents where a Docker toolchain is present we note that a
60
+ // future container-isolation path could be used, but we DO NOT claim to run
61
+ // it we still replicate in a worktree and label it honestly.
62
+ const wantsContainer = incident.details?.severity === 'CRITICAL' && this.isDockerAvailable();
63
+
64
+ if (wantsContainer) {
65
+ return this.replicateCriticalWorktree(incident);
31
66
  }
67
+ return this.replicateLevel1(incident);
32
68
  }
33
69
 
34
70
  /**
35
71
  * Level 1 Replication: Git Worktree
36
- * High-speed, lightweight logic isolation.
72
+ * High-speed, lightweight logic isolation (NOT container isolation).
73
+ *
74
+ * Provides a separate working tree + branch off the current repo. This
75
+ * isolates source/logic changes; it shares the host kernel, network,
76
+ * filesystem and process namespaces with the running engine.
37
77
  */
38
78
  async replicateLevel1(incident) {
39
- const mirrorId = `mirror-${incident.remediation_id}`;
79
+ const remediationId = ShadowMirror.sanitizeRemediationId(incident.remediation_id);
80
+ const mirrorId = `mirror-${remediationId}`;
40
81
  const mirrorPath = path.join(this.baseDir, mirrorId);
41
- const branchName = `sre-repro-${incident.remediation_id}`;
82
+ const branchName = `sre-repro-${remediationId}`;
42
83
 
43
- console.log(`[Level 1] Creating git worktree at ${mirrorPath}...`);
84
+ console.log(`[worktree] Creating git worktree at ${mirrorPath}...`);
44
85
 
45
86
  try {
46
87
  if (!fs.existsSync(this.baseDir)) {
@@ -49,41 +90,46 @@ class ShadowMirror {
49
90
 
50
91
  // 1. Create a reproduction branch and add worktree in one step (Atomic)
51
92
  // In a real system, we'd base this on the commit hash from the trace_id
52
- execSync(`git worktree add -b ${branchName} ${mirrorPath}`, { stdio: 'ignore' });
53
-
54
- this.activeMirror = { path: mirrorPath, branch: branchName, type: 'worktree' };
93
+ // execFileSync with an argv array no shell, so branchName/mirrorPath
94
+ // cannot inject commands even if they somehow contained metacharacters.
95
+ execFileSync('git', ['worktree', 'add', '-b', branchName, mirrorPath], { stdio: 'ignore' });
96
+
97
+ this.activeMirror = {
98
+ path: mirrorPath,
99
+ branch: branchName,
100
+ type: 'worktree-isolation',
101
+ // Honest disclosure of the isolation actually provided.
102
+ isolation: 'git-worktree',
103
+ };
55
104
 
56
105
  // 3. Inject incident metadata for the agent to use
57
106
  fs.writeFileSync(path.join(mirrorPath, 'REPLICATION.json'), JSON.stringify(incident, null, 2));
58
107
 
59
108
  return mirrorPath;
60
109
  } catch (err) {
61
- console.error(`[ShadowMirror] Level 1 replication failed: ${err.message}`);
110
+ console.error(`[ShadowMirror] worktree replication failed: ${err.message}`);
62
111
  throw err;
63
112
  }
64
113
  }
65
114
 
66
115
  /**
67
- * Level 2 Replication: Docker Sandbox
68
- * Full environment isolation for state-bound incidents.
116
+ * CRITICAL-incident replication when a Docker toolchain is detected.
117
+ *
118
+ * HONESTY: This still replicates in a GIT WORKTREE. It does not build or run
119
+ * a container, does not mount any volume, and does not provide container
120
+ * isolation. Real Docker sandboxing (build/run/mount) is a NOT-YET-IMPLEMENTED
121
+ * enhancement tracked separately; until it lands, this path is identical in
122
+ * isolation guarantees to {@link replicateLevel1} and is labelled as such.
69
123
  */
70
- async replicateLevel2(incident) {
71
- console.log('[Level 2] Initializing Docker sandbox interface...');
72
- // For the hackathon demo, we'll scaffold the Docker-ready worktree which would then be mounted
124
+ async replicateCriticalWorktree(incident) {
125
+ console.log('[worktree] CRITICAL incident: replicating in git worktree (container isolation not implemented).');
73
126
  const mirrorPath = await this.replicateLevel1(incident);
74
-
75
- const dockerfile = `
76
- FROM node:18-slim
77
- WORKDIR /app
78
- COPY . .
79
- RUN npm install --production
80
- CMD ["node", "bin/engine/logic-validator.js"]
81
- `;
82
-
83
- fs.writeFileSync(path.join(mirrorPath, 'Dockerfile.sre'), dockerfile);
84
- console.log(`[Level 2] Dockerfile.sre generated at ${mirrorPath}. Mounting volume for isolation.`);
85
-
86
- this.activeMirror.type = 'docker-hybrid';
127
+
128
+ // Note the unimplemented future path without claiming it ran.
129
+ this.activeMirror.dockerAvailable = true;
130
+ this.activeMirror.note =
131
+ 'docker binary detected; real container isolation is a future enhancement — this mirror used git-worktree isolation only';
132
+
87
133
  return mirrorPath;
88
134
  }
89
135
 
@@ -104,9 +150,13 @@ CMD ["node", "bin/engine/logic-validator.js"]
104
150
 
105
151
  console.log(`🧹 Cleaning up Shadow Mirror: ${this.activeMirror.path}...`);
106
152
  try {
107
- if (this.activeMirror.type === 'worktree' || this.activeMirror.type === 'docker-hybrid') {
108
- execSync(`git worktree remove ${this.activeMirror.path} --force`, { stdio: 'ignore' });
109
- execSync(`git branch -D ${this.activeMirror.branch}`, { stdio: 'ignore' });
153
+ // Every mirror this module produces is a git worktree (see replicate()).
154
+ // Accept the legacy 'worktree'/'docker-hybrid' labels for backward compat
155
+ // in case an older serialized mirror is handed in.
156
+ const worktreeTypes = ['worktree-isolation', 'worktree', 'docker-hybrid'];
157
+ if (worktreeTypes.includes(this.activeMirror.type)) {
158
+ execFileSync('git', ['worktree', 'remove', this.activeMirror.path, '--force'], { stdio: 'ignore' });
159
+ execFileSync('git', ['branch', '-D', this.activeMirror.branch], { stdio: 'ignore' });
110
160
  // Clean up the folder just in case
111
161
  if (fs.existsSync(this.activeMirror.path)) {
112
162
  fs.rmSync(this.activeMirror.path, { recursive: true });
@@ -17,6 +17,18 @@ const fs = require('fs');
17
17
  // unboundedly. Revisit with an LRU/eviction policy if dynamic paths are needed.
18
18
  const queues = new Map();
19
19
 
20
+ /**
21
+ * @deprecated No production caller. The real durable-append path is
22
+ * `appendDurableSync` in `bin/memory/knowledge-store.js` (synchronous
23
+ * openSync+writeSync+fsyncSync+closeSync), which all audit/KB write sites use.
24
+ * This async single-writer queue is retained only as a tested reference
25
+ * implementation for the in-process serialized-append pattern (UC-09) and is
26
+ * exercised solely by `tests/append-queue.test.js`. Do NOT wire it into new
27
+ * code without first reconciling it with the canonical sync path.
28
+ *
29
+ * @param {string} filePath — file to serialize appends for
30
+ * @returns {{append: (line: string) => Promise<void>, drain: () => Promise<void>}}
31
+ */
20
32
  function createAppendQueue(filePath) {
21
33
  if (!queues.has(filePath)) queues.set(filePath, Promise.resolve());
22
34
 
@@ -3,7 +3,6 @@
3
3
  const fs = require('fs');
4
4
  const fsp = require('fs/promises');
5
5
  const path = require('path');
6
- const zlib = require('zlib');
7
6
 
8
7
  /**
9
8
  * Hash-chained audit writer (class API preserved for nexus-tracer + policy-engine).
@@ -28,8 +27,9 @@ class AuditWriter {
28
27
  }
29
28
 
30
29
  async write(entry) {
31
- // Lazy require to avoid a require-cycle: audit-writer.js requires this file
32
- // (AuditRotator) at load time, so we cannot require it at module top level.
30
+ // Lazy require (not module-top-level) to keep this leaf utility free of a
31
+ // load-time dependency on the autonomous layer and avoid any future require
32
+ // cycle: bin/autonomous/audit-writer.js is the canonical durable-append site.
33
33
  const { appendAuditEntrySync } = require('../autonomous/audit-writer');
34
34
  const chained = appendAuditEntrySync(this._path, entry);
35
35
  this._lastHash = chained._hash;
@@ -110,45 +110,4 @@ async function atomicWriteJSONAsync(filePath, data) {
110
110
  await fsp.rename(tmpPath, filePath);
111
111
  }
112
112
 
113
- class AuditRotator {
114
- constructor(options = {}) {
115
- this.maxLines = options.maxLines || 5000;
116
- }
117
-
118
- shouldRotate(filePath) {
119
- if (!fs.existsSync(filePath)) return false;
120
- const content = fs.readFileSync(filePath, 'utf8');
121
- const lineCount = content.split('\n').filter(l => l.trim()).length;
122
- return lineCount >= this.maxLines;
123
- }
124
-
125
- rotate(filePath, archiveDir) {
126
- const dir = archiveDir || path.join(path.dirname(filePath), '..', 'audit-archive');
127
- if (!fs.existsSync(dir)) {
128
- fs.mkdirSync(dir, { recursive: true });
129
- }
130
-
131
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
132
- const baseName = path.basename(filePath, path.extname(filePath));
133
- const archiveName = `${baseName}-${timestamp}.jsonl.gz`;
134
- const archivePath = path.join(dir, archiveName);
135
-
136
- // Crash-safe: write archive FIRST, then truncate source
137
- const content = fs.readFileSync(filePath);
138
- const compressed = zlib.gzipSync(content);
139
- fs.writeFileSync(archivePath, compressed);
140
-
141
- const lines = content.toString('utf8').split('\n').filter(l => l.trim());
142
- const carryover = lines.slice(-100).join('\n') + '\n';
143
- const tmpPath = filePath + '.tmp.' + process.pid;
144
- const fd = fs.openSync(tmpPath, 'w');
145
- fs.writeSync(fd, carryover);
146
- fs.fsyncSync(fd);
147
- fs.closeSync(fd);
148
- fs.renameSync(tmpPath, filePath);
149
-
150
- return archivePath;
151
- }
152
- }
153
-
154
- module.exports = { AuditWriter, AuditRotator, readJSON, writeJSON, readJSONL, readJSONSync, atomicWriteJSON, atomicWriteJSONAsync };
113
+ module.exports = { AuditWriter, readJSON, writeJSON, readJSONL, readJSONSync, atomicWriteJSON, atomicWriteJSONAsync };
@@ -3,12 +3,25 @@
3
3
  * MindForge version single-source-of-truth + drift detector.
4
4
  * package.json is canonical; everything else must agree.
5
5
  */
6
+ const fs = require('fs');
6
7
  const path = require('path');
7
8
  // Use the repo's stricter reader: returns null only on ENOENT and RE-THROWS on
8
9
  // parse errors. Re-throwing on a corrupt JSON source is the fail-closed
9
10
  // behavior we want — a file we cannot parse means we cannot establish truth.
10
11
  const { readJSONSync } = require('./file-io');
11
12
 
13
+ /**
14
+ * Extracts `[VERSION] = X` from MINDFORGE.md. Returns null when the file is
15
+ * absent or the marker is missing (treated as skip, not drift).
16
+ */
17
+ function readMindforgeMdVersion(projectRoot) {
18
+ const mdPath = path.join(projectRoot, 'MINDFORGE.md');
19
+ if (!fs.existsSync(mdPath)) return null;
20
+ const text = fs.readFileSync(mdPath, 'utf8');
21
+ const match = text.match(/\[VERSION\]\s*=\s*([^\s]+)/);
22
+ return match ? match[1].trim() : null;
23
+ }
24
+
12
25
  /**
13
26
  * @param {string} projectRoot
14
27
  * @returns {{ canonical: string|null, sources: Record<string,string|null>, drift: string[] }}
@@ -18,14 +31,17 @@ function checkVersionConsistency(projectRoot) {
18
31
  const canonical = pkg ? pkg.version : null;
19
32
 
20
33
  const configJson = readJSONSync(path.join(projectRoot, '.mindforge', 'config.json'));
21
- // Runtime drift coverage is intentionally limited to package.json (canonical)
22
- // vs .mindforge/config.json — the live config is the operational drift risk
23
- // during `auto`. Wider agreement (sdk/package.json, MINDFORGE.md [VERSION]) is
24
- // enforced by the test suite (tests/version-consistency.test.js), not at
25
- // runtime do not assume this checker provides full version coverage.
34
+ const sdkPkg = readJSONSync(path.join(projectRoot, 'sdk', 'package.json'));
35
+ const mindforgeMdVersion = readMindforgeMdVersion(projectRoot);
36
+ // Runtime coverage now spans every source the test suite knows about, so
37
+ // drift in any of them halts `auto` — not just the live config. Absent
38
+ // optional sources (no sdk/, no MINDFORGE.md) read as null and are SKIPPED
39
+ // (not counted as drift); only a present-but-mismatched source flags drift.
26
40
  const sources = {
27
41
  'package.json': canonical,
28
42
  '.mindforge/config.json': configJson ? configJson.version : null,
43
+ 'sdk/package.json': sdkPkg ? sdkPkg.version : null,
44
+ 'MINDFORGE.md': mindforgeMdVersion,
29
45
  };
30
46
 
31
47
  const drift = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindforge-cc",
3
- "version": "11.2.0",
3
+ "version": "11.2.1",
4
4
  "description": "MindForge \u2014 Sovereign Agentic Intelligence Framework. Sovereign Stability: Production-Hardened Agentic Intelligence (v11)",
5
5
  "bin": {
6
6
  "mindforge-cc": "bin/install.js",