intentdna 1.8.3 → 1.8.5

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.
@@ -3,6 +3,9 @@ import { readFile, realpath, stat } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, relative, resolve } from "node:path";
4
4
  export const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30_000;
5
5
  export const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
6
+ export const VERIFIER_DIAGNOSTIC_PREFIX = "INTENTDNA_DIAGNOSTIC:";
7
+ export const MAX_VERIFIER_DIAGNOSTIC_BYTES = 2048;
8
+ const VERIFIER_DIAGNOSTIC_CODE = /^INTENTDNA_DIAGNOSTIC:[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
6
9
  const SAFE_COMMAND_VARIABLE_RE = /^[A-Za-z0-9_-]+$/;
7
10
  const SAFE_PATH_VARIABLE_RE = /^[-A-Za-z0-9_./]+$/;
8
11
  export function resolveVerifierTemplate(template, variables) {
@@ -59,7 +62,7 @@ function resolveVerifierCommand(rawCommand, policy, variables) {
59
62
  return { target: declaredCommand, evidence: "policy_denied", exit_code: 126, message: verifierCommandPolicyMessage(declaredCommand) };
60
63
  }
61
64
  const safeVariables = validateVerifierTemplateVariables(declaredCommand, variables, "command");
62
- if (!safeVariables.valid) {
65
+ if (safeVariables.valid === false) {
63
66
  return { target: declaredCommand, evidence: safeVariables.evidence, exit_code: 126, message: safeVariables.message };
64
67
  }
65
68
  const command = resolveVerifierTemplate(declaredCommand, variables).trim();
@@ -137,8 +140,8 @@ export function isVerifierCommandAllowed(policy, command) {
137
140
  return false;
138
141
  return policy.allow_command_prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
139
142
  }
140
- export function verifierCommandPolicyMessage(command) {
141
- return `Verifier command not allowed by verifier_policy: ${command}`;
143
+ export function verifierCommandPolicyMessage(_command) {
144
+ return "Verifier command not allowed by verifier_policy";
142
145
  }
143
146
  export function isBuiltinAssertAllowed(policy, assertName) {
144
147
  return policy?.allow_builtin_asserts?.includes(assertName) ?? false;
@@ -156,12 +159,52 @@ export function trimVerifierEvidence(raw) {
156
159
  ? trimmed.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
157
160
  : trimmed;
158
161
  }
162
+ function sanitizeVerifierDiagnosticLine(raw) {
163
+ return raw
164
+ .replace(/[\u0000-\u0008\u000B\u000C\u000D\u000E-\u001F\u007F]/g, "")
165
+ .trim();
166
+ }
167
+ function trimDiagnosticBytes(raw) {
168
+ const marker = "...[truncated]";
169
+ if (Buffer.byteLength(raw, "utf8") <= MAX_VERIFIER_DIAGNOSTIC_BYTES)
170
+ return raw;
171
+ let trimmed = raw;
172
+ while (trimmed.length > 0 && Buffer.byteLength(`${trimmed}${marker}`, "utf8") > MAX_VERIFIER_DIAGNOSTIC_BYTES) {
173
+ trimmed = trimmed.slice(0, -1);
174
+ }
175
+ return `${trimmed}${marker}`;
176
+ }
177
+ function extractVerifierDiagnostics(raw) {
178
+ return raw
179
+ .split(/\r?\n/)
180
+ .map((line) => sanitizeVerifierDiagnosticLine(line))
181
+ .filter((line) => VERIFIER_DIAGNOSTIC_CODE.test(line));
182
+ }
183
+ function collectVerifierDiagnostics(pendingLine, chunk, diagnostics) {
184
+ const lines = `${pendingLine}${chunk}`.split("\n");
185
+ const nextPendingLine = lines.pop() ?? "";
186
+ diagnostics.push(...extractVerifierDiagnostics(lines.join("\n")));
187
+ return nextPendingLine.length > MAX_VERIFIER_EVIDENCE_BYTES
188
+ ? nextPendingLine.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
189
+ : nextPendingLine;
190
+ }
191
+ function summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, diagnostics = []) {
192
+ const summary = stdoutBytes === 0 && stderrBytes === 0
193
+ ? undefined
194
+ : `stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`;
195
+ const diagnostic = diagnostics.length > 0
196
+ ? trimDiagnosticBytes(diagnostics.join("\n"))
197
+ : undefined;
198
+ return trimVerifierEvidence([summary, diagnostic].filter(Boolean).join("\n"));
199
+ }
159
200
  export function summarizeCommandEvidence(stdout, stderr) {
160
201
  const stdoutBytes = Buffer.byteLength(stdout, "utf8");
161
202
  const stderrBytes = Buffer.byteLength(stderr, "utf8");
162
- if (stdoutBytes === 0 && stderrBytes === 0)
163
- return undefined;
164
- return trimVerifierEvidence(`stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`);
203
+ return summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, extractVerifierDiagnostics(`${stdout}\n${stderr}`));
204
+ }
205
+ function appendFailureDiagnostic(message, evidence) {
206
+ const diagnostics = extractVerifierDiagnostics(evidence ?? "").join("\n");
207
+ return diagnostics ? `${message}\n${diagnostics}` : message;
165
208
  }
166
209
  export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS, env) {
167
210
  return new Promise((resolvePromise) => {
@@ -170,9 +213,12 @@ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAU
170
213
  stdio: ["ignore", "pipe", "pipe"],
171
214
  env: { ...process.env, ...(env ?? {}) },
172
215
  });
173
- let stdout = "";
174
- let stderr = "";
216
+ let stdoutPendingLine = "";
217
+ let stderrPendingLine = "";
218
+ let stdoutBytes = 0;
219
+ let stderrBytes = 0;
175
220
  let settled = false;
221
+ const diagnostics = [];
176
222
  const settle = (result) => {
177
223
  if (settled)
178
224
  return;
@@ -183,29 +229,36 @@ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAU
183
229
  const timeoutId = setTimeout(() => {
184
230
  child.kill("SIGTERM");
185
231
  setTimeout(() => child.kill("SIGKILL"), 1000).unref();
186
- settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidence(stdout, stderr) });
232
+ diagnostics.push(...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`));
233
+ settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, diagnostics) });
187
234
  }, timeoutMs);
188
235
  child.stdout.on("data", (chunk) => {
189
- stdout += String(chunk);
190
- if (stdout.length > MAX_VERIFIER_EVIDENCE_BYTES)
191
- stdout = stdout.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
236
+ const text = String(chunk);
237
+ stdoutBytes += Buffer.byteLength(text, "utf8");
238
+ stdoutPendingLine = collectVerifierDiagnostics(stdoutPendingLine, text, diagnostics);
192
239
  });
193
240
  child.stderr.on("data", (chunk) => {
194
- stderr += String(chunk);
195
- if (stderr.length > MAX_VERIFIER_EVIDENCE_BYTES)
196
- stderr = stderr.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
241
+ const text = String(chunk);
242
+ stderrBytes += Buffer.byteLength(text, "utf8");
243
+ stderrPendingLine = collectVerifierDiagnostics(stderrPendingLine, text, diagnostics);
197
244
  });
198
245
  child.on("error", () => settle({
199
246
  passed: false,
200
247
  timedOut: false,
201
248
  exitCode: 1,
202
- evidence: summarizeCommandEvidence(stdout, stderr),
249
+ evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, [
250
+ ...diagnostics,
251
+ ...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`),
252
+ ]),
203
253
  }));
204
254
  child.on("close", (code) => settle({
205
255
  passed: code === 0,
206
256
  timedOut: false,
207
257
  exitCode: code ?? 1,
208
- evidence: summarizeCommandEvidence(stdout, stderr),
258
+ evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, [
259
+ ...diagnostics,
260
+ ...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`),
261
+ ]),
209
262
  }));
210
263
  });
211
264
  }
@@ -296,7 +349,7 @@ export async function runCompletionVerifier(completion, ir, context) {
296
349
  }
297
350
  if (completion.file_exists) {
298
351
  const safeVariables = validateVerifierTemplateVariables(completion.file_exists, context.variables, "path");
299
- if (!safeVariables.valid)
352
+ if (safeVariables.valid === false)
300
353
  return { passed: false, target: completion.file_exists, evidence: safeVariables.evidence, message: safeVariables.message };
301
354
  const target = resolveVerifierTemplate(completion.file_exists, context.variables);
302
355
  if (hasUnresolvedVerifierTemplate(target))
@@ -316,7 +369,7 @@ export async function runCompletionVerifier(completion, ir, context) {
316
369
  }
317
370
  if (completion.file_not_empty) {
318
371
  const safeVariables = validateVerifierTemplateVariables(completion.file_not_empty, context.variables, "path");
319
- if (!safeVariables.valid)
372
+ if (safeVariables.valid === false)
320
373
  return { passed: false, target: completion.file_not_empty, evidence: safeVariables.evidence, message: safeVariables.message };
321
374
  const target = resolveVerifierTemplate(completion.file_not_empty, context.variables);
322
375
  if (hasUnresolvedVerifierTemplate(target))
@@ -338,10 +391,10 @@ export async function runCompletionVerifier(completion, ir, context) {
338
391
  }
339
392
  if (completion.file_contains) {
340
393
  const safeTargetVariables = validateVerifierTemplateVariables(completion.file_contains.path, context.variables, "path");
341
- if (!safeTargetVariables.valid)
394
+ if (safeTargetVariables.valid === false)
342
395
  return { passed: false, target: completion.file_contains.path, evidence: safeTargetVariables.evidence, message: safeTargetVariables.message };
343
396
  const safePatternVariables = validateVerifierTemplateVariables(completion.file_contains.pattern, context.variables);
344
- if (!safePatternVariables.valid)
397
+ if (safePatternVariables.valid === false)
345
398
  return { passed: false, target: completion.file_contains.pattern, evidence: safePatternVariables.evidence, message: safePatternVariables.message };
346
399
  const target = resolveVerifierTemplate(completion.file_contains.path, context.variables);
347
400
  const pattern = resolveVerifierTemplate(completion.file_contains.pattern, context.variables);
@@ -379,8 +432,8 @@ export async function runCompletionVerifier(completion, ir, context) {
379
432
  message: commandResult.passed
380
433
  ? undefined
381
434
  : commandResult.timedOut
382
- ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${command}`
383
- : `Verifier command failed: ${command}`,
435
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
436
+ : appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
384
437
  };
385
438
  }
386
439
  export async function runCheckpointVerifier(checkpoint, ir, context) {
@@ -405,8 +458,8 @@ export async function runCheckpointVerifier(checkpoint, ir, context) {
405
458
  message: commandResult.passed
406
459
  ? checkpoint.message
407
460
  : commandResult.timedOut
408
- ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${command}`
409
- : `Verifier command failed: ${command}`,
461
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
462
+ : appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
410
463
  };
411
464
  }
412
465
  if (checkpoint.assert === "clean_working_tree") {
@@ -454,8 +507,10 @@ export async function runCheckpointVerifier(checkpoint, ir, context) {
454
507
  message: commandResult.passed
455
508
  ? checkpoint.message
456
509
  : commandResult.timedOut
457
- ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${assertCommands[checkpoint.assert]}`
458
- : checkpoint.message,
510
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
511
+ : checkpoint.message
512
+ ? appendFailureDiagnostic(checkpoint.message, commandResult.evidence)
513
+ : appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
459
514
  };
460
515
  }
461
516
  return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
@@ -166,7 +166,8 @@ context_files:
166
166
 
167
167
  verifier_policy:
168
168
  allow_commands:
169
- - "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{throw new Error(m)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const ids=new Set(observed.map(x=>x&&x.id).filter(Boolean));if(ids.size!==observed.length)fail('observed_failure_ids');const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');inside(data.baseline.source_path);for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}const total=counts.failed+counts.skipped+counts.hung;if(total!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const sha=p=>crypto.createHash('sha256').update(fs.readFileSync(inside(p))).digest('hex');const snap=data.contract_snapshot||{};if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==sha(data.baseline.source_path))fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==sha(diagnosisPath))fail('diagnosis_sha256');if(!data.staleness||data.staleness.is_stale!==false)fail('stale_contract');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
169
+ - "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const legacySame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const modern=s=>s&&typeof s.sidecar_semantic_sha256==='string'&&typeof s.diagnosis_semantic_sha256==='string';const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');if(fs.existsSync(inside(reviewPath))){const prev=readJson(reviewPath);if(prev.verdict==='REQUEST_REANALYSIS'){const ps=prev.contract_snapshot||{};const unchanged=modern(ps)?ps.sidecar_semantic_sha256===sidecarSemanticHash&&ps.diagnosis_semantic_sha256===diagnosisSemanticHash:legacySame(ps,snap);if(unchanged)fail('reanalysis_unchanged');const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(cm<=rm||dm<=rm)fail('reanalysis_not_rewritten')}}\""
170
+ - "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.sidecar_semantic_sha256===b.sidecar_semantic_sha256&&a.diagnosis_semantic_sha256===b.diagnosis_semantic_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
170
171
  allow_builtin_asserts:
171
172
  - clean_working_tree
172
173
 
@@ -248,7 +249,7 @@ roles:
248
249
  analyzer:
249
250
  description: "Reads code and classifies failing tests. Writes diagnosis spec only."
250
251
  tool_permissions:
251
- allow: [Read, Grep, Glob, Write]
252
+ allow: [Read, Grep, Glob, Write, mcp__intentdna__dna_artifact_digest]
252
253
  deny: [Bash, Edit, NotebookEdit]
253
254
  scope:
254
255
  read: ["**/*"]
@@ -263,13 +264,14 @@ roles:
263
264
  - "The sidecar allowed_kinds array must be exactly BUG, UNIMPLEMENTED, INFRA, REMOVED, TEST_BUG in that order; PLACEHOLDER_CONTRACT is never a kind"
264
265
  - "A placeholder-contract symptom may appear only in observed_failures.symptom and must map to concrete TEST_BUG and/or UNIMPLEMENTED work_items with evidence"
265
266
  - "If reanalyzing after REQUEST_REANALYSIS, update every reviewer sections_to_fix item and report sections_updated"
267
+ - "Use mcp__intentdna__dna_artifact_digest for every exact contract SHA-256; never guess a digest or use Bash to compute one"
266
268
  - "DO NOT include fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
267
269
  - "DO NOT run tests. DO NOT edit app code. Analysis only."
268
270
 
269
271
  analysis_reviewer:
270
272
  description: "Reviews analyzer output quality and writes durable diagnosis review verdict."
271
273
  tool_permissions:
272
- allow: [Read, Grep, Glob, Write]
274
+ allow: [Read, Grep, Glob, Write, mcp__intentdna__dna_artifact_digest]
273
275
  deny: [Bash, Edit, NotebookEdit]
274
276
  scope:
275
277
  read: ["**/*"]
@@ -278,6 +280,7 @@ roles:
278
280
  - "REQUIRED FIRST: Read all context files listed in SKILL.md"
279
281
  - "Verify analyzer spec: v1 references exist? Classifications sound?"
280
282
  - "Independently validate .dna/specs/diagnosis-$ARGUMENTS.contract.json against the template sidecar contract; do not use Bash"
283
+ - "Use mcp__intentdna__dna_artifact_digest to independently verify exact raw, normalized-text, and canonical-JSON SHA-256 values"
281
284
  - "Check missing: any failing test not covered?"
282
285
  - "Verify diagnosis remains evidence-only: no fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
283
286
  - "Output a structured verdict block with contract_version, contract_valid, failure_reason, contract_artifact_reviewed, contract_snapshot, verdict, artifact_reviewed, sections_to_fix, evidence_paths, confidence, summary, and updated_at"
@@ -486,16 +489,20 @@ workflows:
486
489
  - observed_failures: one entry per failing/skipped/hung test with id, test_name, symptom, evidence_paths, and no kind field
487
490
  - work_items: one or more entries mapping observed_failure_ids to kind, evidence_paths, v1_evidence_paths, and v2_evidence_paths
488
491
  - summary_counts: exact count of work_items by BUG / UNIMPLEMENTED / INFRA / REMOVED / TEST_BUG
489
- - contract_snapshot: baseline_source_sha256, diagnosis_artifact_sha256, observed_failure_count, work_item_count, allowed_kinds
490
- - staleness: source mtimes or hashes for behavior_doc, baseline, diagnosis_artifact, and is_stale false
492
+ - contract_snapshot: baseline_source_sha256, diagnosis_artifact_sha256, sidecar_semantic_sha256, diagnosis_semantic_sha256, observed_failure_count, work_item_count, allowed_kinds
493
+ - staleness: behavior_doc, baseline, and diagnosis_artifact entries each contain the exact project-relative path and current sha256; is_stale is false
494
+ - Obtain hashes with mcp__intentdna__dna_artifact_digest, which is read-only and project-relative: bytes mode for behavior/baseline/diagnosis, whitespace_normalized_text for diagnosis_semantic_sha256, and canonical_json for the sidecar with exclude_json_pointers=["/contract_snapshot/sidecar_semantic_sha256", "/contract_snapshot/diagnosis_artifact_sha256", "/staleness/diagnosis_artifact"] for sidecar_semantic_sha256. Write the returned values back to the sidecar and re-run the canonical digest to confirm it is stable. Never guess hashes.
491
495
  7. Contract rules:
492
496
  - observed_failures must not be empty; work_items must not be empty.
493
497
  - Every observed_failure id must be mapped by at least one work_item.
494
498
  - Every work_item kind must be one of the five allowed kinds exactly.
495
499
  - PLACEHOLDER_CONTRACT is forbidden as a kind. If a placeholder-contract symptom exists, keep it in observed_failures.symptom and map it to concrete TEST_BUG and/or UNIMPLEMENTED work_items with evidence.
496
500
  - REMOVED may be diagnosed but must be marked manual/user-approved before any future fix can execute it.
497
- - Mixed unknown kinds, missing evidence, count mismatches, missing/changed contract_snapshot, and stale baseline/diagnosis snapshots make the contract invalid.
501
+ - Mixed unknown kinds, missing evidence, count mismatches, missing/changed contract_snapshot, and stale behavior/baseline/diagnosis snapshots make the contract invalid.
498
502
  8. If this is a reanalysis after REQUEST_REANALYSIS or DIAGNOSIS_CONTRACT_INVALID, read the reviewer verdict first, update every section listed in sections_to_fix plus the summary table and contract sidecar, and report sections_updated in your final response. Reanalysis is not successful unless both the spec artifact and contract sidecar are rewritten.
503
+ 9. Before handing off to review, the producer-side diagnosis contract completion gate must pass. It rejects grouped observations that hide per-test failures, duplicate/bad ids, unmapped observed_failures, missing evidence, count mismatches, bad hashes, stale state, and no-material-change reanalysis after REQUEST_REANALYSIS. Reanalysis compares the canonical sidecar with its self-digest and raw diagnosis snapshot fields omitted, plus a whitespace-normalized diagnosis hash, so a real sidecar-only correction is allowed while formatting-only Markdown changes are not.
504
+ completion:
505
+ - command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const legacySame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const modern=s=>s&&typeof s.sidecar_semantic_sha256==='string'&&typeof s.diagnosis_semantic_sha256==='string';const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');if(fs.existsSync(inside(reviewPath))){const prev=readJson(reviewPath);if(prev.verdict==='REQUEST_REANALYSIS'){const ps=prev.contract_snapshot||{};const unchanged=modern(ps)?ps.sidecar_semantic_sha256===sidecarSemanticHash&&ps.diagnosis_semantic_sha256===diagnosisSemanticHash:legacySame(ps,snap);if(unchanged)fail('reanalysis_unchanged');const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(cm<=rm||dm<=rm)fail('reanalysis_not_rewritten')}}\""
499
506
  handoff:
500
507
  produces:
501
508
  - type: file
@@ -527,8 +534,9 @@ workflows:
527
534
  - summary_counts match work_items by kind
528
535
  - every work_item has evidence_paths and only allowed kinds
529
536
  - PLACEHOLDER_CONTRACT is rejected as a kind but placeholder-contract symptoms are allowed when mapped to TEST_BUG and/or UNIMPLEMENTED
530
- - contract_snapshot includes diagnosis_artifact_sha256, baseline_source_sha256, observed_failure_count, work_item_count, and allowed_kinds
531
- - staleness.is_stale is false; stale baseline/diagnosis/contract snapshots are invalid
537
+ - contract_snapshot includes diagnosis_artifact_sha256, baseline_source_sha256, sidecar_semantic_sha256, diagnosis_semantic_sha256, observed_failure_count, work_item_count, and allowed_kinds
538
+ - staleness.is_stale is false and behavior_doc, baseline, and diagnosis_artifact path+sha256 entries match current project files; stale source snapshots are invalid
539
+ - independently recompute hashes with mcp__intentdna__dna_artifact_digest using the same bytes, whitespace_normalized_text, and canonical_json modes; canonical_json must omit /contract_snapshot/sidecar_semantic_sha256, /contract_snapshot/diagnosis_artifact_sha256, and /staleness/diagnosis_artifact
532
540
  - REMOVED work_items are reviewable diagnosis facts only and must not authorize automatic fix execution
533
541
 
534
542
  Output exactly one structured verdict block:
@@ -541,6 +549,8 @@ workflows:
541
549
  "contract_snapshot": {
542
550
  "baseline_source_sha256": "sha256 from contract sidecar",
543
551
  "diagnosis_artifact_sha256": "sha256 from contract sidecar",
552
+ "sidecar_semantic_sha256": "canonical routing sidecar sha256 from contract sidecar",
553
+ "diagnosis_semantic_sha256": "whitespace-normalized diagnosis sha256 from contract sidecar",
544
554
  "observed_failure_count": 0,
545
555
  "work_item_count": 0,
546
556
  "allowed_kinds": ["BUG", "UNIMPLEMENTED", "INFRA", "REMOVED", "TEST_BUG"]
@@ -560,7 +570,7 @@ workflows:
560
570
  APPROVE → diagnosis complete.
561
571
  REQUEST_REANALYSIS → sections_to_fix must be passed back to analyze. If contract_valid is false, set failure_reason to DIAGNOSIS_CONTRACT_INVALID and include the exact invalid rule in sections_to_fix. docs/behavior/blocked_items.md is historical context only and must not be treated as the current run verdict.
562
572
  completion:
563
- - command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{throw new Error(m)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const ids=new Set(observed.map(x=>x&&x.id).filter(Boolean));if(ids.size!==observed.length)fail('observed_failure_ids');const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');inside(data.baseline.source_path);for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}const total=counts.failed+counts.skipped+counts.hung;if(total!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const sha=p=>crypto.createHash('sha256').update(fs.readFileSync(inside(p))).digest('hex');const snap=data.contract_snapshot||{};if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==sha(data.baseline.source_path))fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==sha(diagnosisPath))fail('diagnosis_sha256');if(!data.staleness||data.staleness.is_stale!==false)fail('stale_contract');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
573
+ - command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.sidecar_semantic_sha256===b.sidecar_semantic_sha256&&a.diagnosis_semantic_sha256===b.diagnosis_semantic_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
564
574
  handoff:
565
575
  consumes:
566
576
  - type: file
package/hooks/hooks.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreToolUse", "timeout": 5 }] }],
4
4
  "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PostToolUse", "timeout": 3 }] }],
5
5
  "UserPromptSubmit": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook UserPromptSubmit", "timeout": 5 }] }],
6
- "SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 3 }] }],
6
+ "SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 45 }] }],
7
7
  "PreCompact": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreCompact", "timeout": 3 }] }],
8
8
  "Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Notification", "timeout": 3 }] }],
9
9
  "Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Stop", "timeout": 45 }] }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",