intentdna 1.8.6 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -0
  4. package/dist/cli/commands/run-lifecycle.d.ts +73 -0
  5. package/dist/cli/commands/run-lifecycle.js +240 -0
  6. package/dist/cli/commands/run.d.ts +22 -40
  7. package/dist/cli/commands/run.js +674 -392
  8. package/dist/cli/index.js +87 -2
  9. package/dist/compiler/workflow.js +3 -2
  10. package/dist/hooks/cli.d.ts +1 -2
  11. package/dist/hooks/cli.js +119 -80
  12. package/dist/hooks/enforce.d.ts +2 -0
  13. package/dist/hooks/enforce.js +56 -27
  14. package/dist/hooks/enforcement-boundary.d.ts +13 -0
  15. package/dist/hooks/enforcement-boundary.js +33 -0
  16. package/dist/hooks/index.d.ts +3 -2
  17. package/dist/hooks/index.js +3 -2
  18. package/dist/hooks/protocol.d.ts +12 -4
  19. package/dist/hooks/protocol.js +20 -14
  20. package/dist/hooks/schema.d.ts +2 -1
  21. package/dist/hooks/schema.js +6 -2
  22. package/dist/hooks/state-manager.d.ts +5 -5
  23. package/dist/hooks/state-manager.js +26 -24
  24. package/dist/hooks/state.d.ts +19 -3
  25. package/dist/hooks/state.js +327 -80
  26. package/dist/mcp/index.js +0 -0
  27. package/dist/runtime/diagnosis-contract-verifier.d.ts +11 -0
  28. package/dist/runtime/diagnosis-contract-verifier.js +417 -0
  29. package/dist/runtime/execution-provider.d.ts +40 -0
  30. package/dist/runtime/execution-provider.js +138 -0
  31. package/dist/runtime/handoff-resolver.d.ts +61 -0
  32. package/dist/runtime/handoff-resolver.js +167 -0
  33. package/dist/runtime/index.d.ts +24 -0
  34. package/dist/runtime/index.js +13 -0
  35. package/dist/runtime/process-tree.d.ts +47 -0
  36. package/dist/runtime/process-tree.js +402 -0
  37. package/dist/runtime/providers/claude.d.ts +9 -0
  38. package/dist/runtime/providers/claude.js +64 -0
  39. package/dist/runtime/providers/codex.d.ts +8 -0
  40. package/dist/runtime/providers/codex.js +72 -0
  41. package/dist/runtime/result-store.d.ts +32 -0
  42. package/dist/runtime/result-store.js +130 -0
  43. package/dist/runtime/run-contracts.d.ts +290 -0
  44. package/dist/runtime/run-contracts.js +58 -0
  45. package/dist/runtime/run-controller.d.ts +149 -0
  46. package/dist/runtime/run-controller.js +1108 -0
  47. package/dist/runtime/run-store.d.ts +96 -0
  48. package/dist/runtime/run-store.js +725 -0
  49. package/dist/runtime/worker-executor.d.ts +19 -0
  50. package/dist/runtime/worker-executor.js +194 -0
  51. package/dist/runtime/workflow-plan-adapter.d.ts +26 -0
  52. package/dist/runtime/workflow-plan-adapter.js +416 -0
  53. package/dist/runtime/workflow-runner.d.ts +15 -3
  54. package/dist/runtime/workflow-runner.js +13 -1
  55. package/dist/runtime/workspace-isolation.d.ts +103 -0
  56. package/dist/runtime/workspace-isolation.js +373 -0
  57. package/dist/schema/types.d.ts +1 -0
  58. package/dist/schema/validate.js +64 -6
  59. package/dist/schema/yaml-parser.js +7 -2
  60. package/package.json +1 -1
@@ -9,8 +9,8 @@
9
9
  * All read operations are fail-safe (return null on error, never throw).
10
10
  * All write operations use atomic temp+rename pattern.
11
11
  */
12
- import { readFile, writeFile, rename, mkdir, appendFile, unlink, readdir, stat } from "node:fs/promises";
13
- import { createHash } from "node:crypto";
12
+ import { readFile, rename, mkdir, unlink, readdir, stat, open, rm, utimes } from "node:fs/promises";
13
+ import { createHash, randomUUID } from "node:crypto";
14
14
  import { join, dirname } from "node:path";
15
15
  import { CASCADE_LAYER_NAMES, RUNTIME_DECISION_EVENT_SCHEMA_VERSION } from "../governance/index.js";
16
16
  // ── Types ──────────────────────────────────────────────────
@@ -145,6 +145,11 @@ export async function readWorkflowState(projectDir, sessionId, stalenessMs = DEF
145
145
  try {
146
146
  const raw = await readFile(filePath, "utf-8");
147
147
  const state = JSON.parse(raw);
148
+ if (sessionId) {
149
+ const storedWorkerSessionId = state.worker_session_id ?? state.session_id;
150
+ if (storedWorkerSessionId !== sessionId)
151
+ return null;
152
+ }
148
153
  // Staleness check based on started_at
149
154
  if (stalenessMs > 0 && state.started_at) {
150
155
  const age = Date.now() - new Date(state.started_at).getTime();
@@ -165,7 +170,16 @@ export async function writeWorkflowState(projectDir, state, sessionId) {
165
170
  const stateDir = resolveStateDir(projectDir, sessionId);
166
171
  await mkdir(stateDir, { recursive: true });
167
172
  const workflowPath = join(stateDir, WORKFLOW_FILE);
168
- await atomicWrite(workflowPath, JSON.stringify(state, null, 2));
173
+ const workerState = sessionId
174
+ ? {
175
+ ...state,
176
+ session_id: sessionId,
177
+ ...(state.worker_session_id === undefined
178
+ ? {}
179
+ : { worker_session_id: sessionId }),
180
+ }
181
+ : state;
182
+ await atomicWrite(workflowPath, JSON.stringify(workerState, null, 2));
169
183
  }
170
184
  /**
171
185
  * Clear workflow state (workflow complete).
@@ -193,25 +207,25 @@ export async function appendAudit(projectDir, entry) {
193
207
  await mkdir(auditDir, { recursive: true });
194
208
  const date = entry.timestamp.slice(0, 10); // YYYY-MM-DD
195
209
  const logPath = join(auditDir, `violations-${date}.log`);
196
- // Dedup: check if identical entry (same event+tool+second) already exists
197
- const dedupKey = `${entry.event}|${entry.tool_name ?? ""}|${entry.timestamp.slice(0, 19)}`;
198
- try {
199
- const existing = await readFile(logPath, "utf-8");
200
- const lines = existing.trimEnd().split("\n");
201
- // Check last 10 lines for dedup (avoid scanning entire file)
202
- const recentLines = lines.slice(-10);
203
- for (const line of recentLines) {
204
- try {
205
- const prev = JSON.parse(line);
206
- const prevKey = `${prev.event}|${prev.tool_name ?? ""}|${prev.timestamp.slice(0, 19)}`;
207
- if (prevKey === dedupKey)
208
- return; // Already logged
210
+ await withStateFileLock(logPath, async (lock) => {
211
+ const dedupKey = `${entry.event}|${entry.tool_name ?? ""}|${entry.timestamp.slice(0, 19)}`;
212
+ try {
213
+ const existing = await readFile(logPath, "utf-8");
214
+ const recentLines = existing.trimEnd().split("\n").slice(-10);
215
+ for (const line of recentLines) {
216
+ try {
217
+ const prev = JSON.parse(line);
218
+ const prevKey = `${prev.event}|${prev.tool_name ?? ""}|${prev.timestamp.slice(0, 19)}`;
219
+ if (prevKey === dedupKey)
220
+ return;
221
+ }
222
+ catch { /* skip malformed historical lines */ }
209
223
  }
210
- catch { /* skip malformed lines */ }
211
224
  }
212
- }
213
- catch { /* file doesn't exist yet */ }
214
- await appendFile(logPath, JSON.stringify(entry) + "\n", "utf-8");
225
+ catch { /* file doesn't exist yet */ }
226
+ await lock.assertOwned();
227
+ await atomicAppendUnlocked(logPath, JSON.stringify(entry) + "\n");
228
+ });
215
229
  }
216
230
  // ── Completed Artifacts ───────────────────────────────────
217
231
  /**
@@ -219,44 +233,264 @@ export async function appendAudit(projectDir, entry) {
219
233
  * Reads current workflow state, appends the artifact, writes back atomically.
220
234
  */
221
235
  export async function appendCompletedArtifact(projectDir, stepId, artifact, sessionId) {
222
- const state = await readWorkflowState(projectDir, sessionId, 0);
223
- if (!state)
224
- return;
225
- const artifacts = state.completed_artifacts ?? [];
226
- let stepEntry = artifacts.find(a => a.step_id === stepId);
227
- if (!stepEntry) {
228
- stepEntry = { step_id: stepId, artifacts: [] };
229
- artifacts.push(stepEntry);
230
- }
231
- const verifiedAt = new Date().toISOString();
232
- const existing = stepEntry.artifacts.find(a => a.type === artifact.type && a.path === artifact.path);
233
- if (existing) {
234
- existing.verified_at = verifiedAt;
235
- if (artifact.artifact_id)
236
- existing.artifact_id = artifact.artifact_id;
237
- if (artifact.metadata)
238
- existing.metadata = artifact.metadata;
239
- state.completed_artifacts = artifacts;
240
- await writeWorkflowState(projectDir, state, sessionId);
241
- return;
242
- }
243
- stepEntry.artifacts.push({
244
- type: artifact.type,
245
- path: artifact.path,
246
- verified_at: verifiedAt,
247
- ...(artifact.artifact_id ? { artifact_id: artifact.artifact_id } : {}),
248
- ...(artifact.metadata ? { metadata: artifact.metadata } : {}),
236
+ const workflowPath = join(resolveStateDir(projectDir, sessionId), WORKFLOW_FILE);
237
+ await atomicUpdateJson(workflowPath, null, (state) => {
238
+ if (!state)
239
+ return undefined;
240
+ const artifacts = state.completed_artifacts ?? [];
241
+ let stepEntry = artifacts.find(a => a.step_id === stepId);
242
+ if (!stepEntry) {
243
+ stepEntry = { step_id: stepId, artifacts: [] };
244
+ artifacts.push(stepEntry);
245
+ }
246
+ const verifiedAt = new Date().toISOString();
247
+ const existing = stepEntry.artifacts.find(a => a.type === artifact.type && a.path === artifact.path);
248
+ if (existing) {
249
+ existing.verified_at = verifiedAt;
250
+ if (artifact.artifact_id)
251
+ existing.artifact_id = artifact.artifact_id;
252
+ if (artifact.metadata)
253
+ existing.metadata = artifact.metadata;
254
+ }
255
+ else {
256
+ stepEntry.artifacts.push({
257
+ type: artifact.type,
258
+ path: artifact.path,
259
+ verified_at: verifiedAt,
260
+ ...(artifact.artifact_id ? { artifact_id: artifact.artifact_id } : {}),
261
+ ...(artifact.metadata ? { metadata: artifact.metadata } : {}),
262
+ });
263
+ }
264
+ return { ...state, completed_artifacts: artifacts };
249
265
  });
250
- state.completed_artifacts = artifacts;
251
- await writeWorkflowState(projectDir, state, sessionId);
252
266
  }
253
267
  // ── Internal ───────────────────────────────────────────────
254
- /** Atomic write: write to temp file then rename. */
268
+ const STATE_LOCK_WAIT_MS = 5_000;
269
+ const STATE_LOCK_RETRY_MS = 25;
270
+ const STATE_LOCK_STALE_MS = 5 * 60_000;
271
+ const STATE_LOCK_HEARTBEAT_MS = 100_000;
272
+ const DIRECTORY_MODE = 0o700;
273
+ const FILE_MODE = 0o600;
274
+ function isErrno(error, code) {
275
+ return error instanceof Error
276
+ && "code" in error
277
+ && error.code === code;
278
+ }
279
+ function isUnsupportedDirectorySync(error) {
280
+ if (!(error instanceof Error) || !("code" in error))
281
+ return false;
282
+ const code = error.code;
283
+ return code === "EINVAL"
284
+ || code === "ENOTSUP"
285
+ || code === "EISDIR"
286
+ || (code === "EPERM" && process.platform === "win32");
287
+ }
288
+ function sleep(ms) {
289
+ return new Promise(resolve => setTimeout(resolve, ms));
290
+ }
291
+ async function syncParentDirectory(filePath) {
292
+ let handle;
293
+ try {
294
+ handle = await open(dirname(filePath), "r");
295
+ }
296
+ catch (error) {
297
+ if (isUnsupportedDirectorySync(error))
298
+ return;
299
+ throw error;
300
+ }
301
+ try {
302
+ await handle.sync();
303
+ }
304
+ catch (error) {
305
+ if (!isUnsupportedDirectorySync(error))
306
+ throw error;
307
+ }
308
+ finally {
309
+ await handle.close();
310
+ }
311
+ }
312
+ async function atomicWriteUnlocked(filePath, data) {
313
+ const tmpPath = `${filePath}.tmp.${process.pid}.${randomUUID()}`;
314
+ await mkdir(dirname(filePath), { recursive: true, mode: DIRECTORY_MODE });
315
+ let handle;
316
+ try {
317
+ handle = await open(tmpPath, "wx", FILE_MODE);
318
+ await handle.writeFile(data, "utf-8");
319
+ await handle.sync();
320
+ await handle.close();
321
+ handle = undefined;
322
+ await rename(tmpPath, filePath);
323
+ await syncParentDirectory(filePath);
324
+ }
325
+ catch (error) {
326
+ await handle?.close().catch(() => undefined);
327
+ await rm(tmpPath, { force: true }).catch(() => undefined);
328
+ throw error;
329
+ }
330
+ }
331
+ async function atomicAppendUnlocked(filePath, data) {
332
+ await mkdir(dirname(filePath), { recursive: true, mode: DIRECTORY_MODE });
333
+ const handle = await open(filePath, "a", FILE_MODE);
334
+ try {
335
+ await handle.writeFile(data, "utf-8");
336
+ await handle.sync();
337
+ }
338
+ finally {
339
+ await handle.close();
340
+ }
341
+ }
342
+ async function readLockOwner(ownerPath) {
343
+ try {
344
+ return (await readFile(ownerPath, "utf-8")).trim();
345
+ }
346
+ catch (error) {
347
+ if (isErrno(error, "ENOENT"))
348
+ return null;
349
+ throw error;
350
+ }
351
+ }
352
+ async function lockIsStale(lockDirectory) {
353
+ try {
354
+ const lease = await stat(join(lockDirectory, "lease"));
355
+ return Date.now() - lease.mtimeMs > STATE_LOCK_STALE_MS;
356
+ }
357
+ catch (error) {
358
+ if (!isErrno(error, "ENOENT"))
359
+ return false;
360
+ }
361
+ try {
362
+ const lock = await stat(lockDirectory);
363
+ return Date.now() - lock.mtimeMs > STATE_LOCK_STALE_MS;
364
+ }
365
+ catch {
366
+ return false;
367
+ }
368
+ }
369
+ async function recoverStaleLock(lockDirectory) {
370
+ if (!(await lockIsStale(lockDirectory)))
371
+ return false;
372
+ const quarantine = `${lockDirectory}.reclaimed.${randomUUID()}`;
373
+ try {
374
+ await rename(lockDirectory, quarantine);
375
+ }
376
+ catch (error) {
377
+ if (isErrno(error, "ENOENT"))
378
+ return true;
379
+ throw error;
380
+ }
381
+ await rm(quarantine, { recursive: true, force: true });
382
+ return true;
383
+ }
384
+ async function withStateFileLock(filePath, operation) {
385
+ const lockDirectory = `${filePath}.lock`;
386
+ const owner = `${process.pid}.${Date.now()}.${randomUUID()}`;
387
+ const ownerPath = join(lockDirectory, "owner");
388
+ const leasePath = join(lockDirectory, "lease");
389
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
390
+ await mkdir(dirname(filePath), { recursive: true, mode: DIRECTORY_MODE });
391
+ while (true) {
392
+ try {
393
+ await mkdir(lockDirectory, { mode: DIRECTORY_MODE });
394
+ try {
395
+ const ownerHandle = await open(ownerPath, "wx", FILE_MODE);
396
+ await ownerHandle.writeFile(owner, "utf-8");
397
+ await ownerHandle.sync();
398
+ await ownerHandle.close();
399
+ const leaseHandle = await open(leasePath, "wx", FILE_MODE);
400
+ await leaseHandle.writeFile(owner, "utf-8");
401
+ await leaseHandle.sync();
402
+ await leaseHandle.close();
403
+ }
404
+ catch (error) {
405
+ await rm(lockDirectory, { recursive: true, force: true });
406
+ throw error;
407
+ }
408
+ break;
409
+ }
410
+ catch (error) {
411
+ if (!isErrno(error, "EEXIST"))
412
+ throw error;
413
+ if (await recoverStaleLock(lockDirectory))
414
+ continue;
415
+ if (Date.now() >= deadline) {
416
+ throw new Error(`Timed out acquiring Hook state lock for ${filePath}`);
417
+ }
418
+ await sleep(STATE_LOCK_RETRY_MS);
419
+ }
420
+ }
421
+ let heartbeatRunning = false;
422
+ let lostOwnership = false;
423
+ const refresh = async () => {
424
+ if (heartbeatRunning || lostOwnership)
425
+ return;
426
+ heartbeatRunning = true;
427
+ try {
428
+ if ((await readLockOwner(ownerPath)) !== owner) {
429
+ lostOwnership = true;
430
+ return;
431
+ }
432
+ const now = new Date();
433
+ await utimes(leasePath, now, now);
434
+ }
435
+ catch {
436
+ lostOwnership = true;
437
+ }
438
+ finally {
439
+ heartbeatRunning = false;
440
+ }
441
+ };
442
+ const timer = setInterval(() => void refresh(), STATE_LOCK_HEARTBEAT_MS);
443
+ timer.unref();
444
+ const lock = {
445
+ async assertOwned() {
446
+ if (lostOwnership || (await readLockOwner(ownerPath)) !== owner) {
447
+ lostOwnership = true;
448
+ throw new Error(`Hook state lock ownership was lost for ${filePath}`);
449
+ }
450
+ },
451
+ };
452
+ try {
453
+ return await operation(lock);
454
+ }
455
+ finally {
456
+ clearInterval(timer);
457
+ while (heartbeatRunning)
458
+ await sleep(1);
459
+ if ((await readLockOwner(ownerPath).catch(() => null)) === owner) {
460
+ await rm(lockDirectory, { recursive: true, force: true });
461
+ }
462
+ }
463
+ }
464
+ /** Atomic owner-checked temp/fsync/rename write adapted from the approved store primitive. */
255
465
  export async function atomicWrite(filePath, data) {
256
- const tmpPath = filePath + ".tmp." + process.pid;
257
- await mkdir(dirname(filePath), { recursive: true });
258
- await writeFile(tmpPath, data, "utf-8");
259
- await rename(tmpPath, filePath);
466
+ await withStateFileLock(filePath, async (lock) => {
467
+ await lock.assertOwned();
468
+ await atomicWriteUnlocked(filePath, data);
469
+ });
470
+ }
471
+ export async function atomicAppend(filePath, data) {
472
+ await withStateFileLock(filePath, async (lock) => {
473
+ await lock.assertOwned();
474
+ await atomicAppendUnlocked(filePath, data);
475
+ });
476
+ }
477
+ export async function atomicUpdateJson(filePath, fallback, update) {
478
+ return withStateFileLock(filePath, async (lock) => {
479
+ let current = fallback;
480
+ try {
481
+ current = JSON.parse(await readFile(filePath, "utf-8"));
482
+ }
483
+ catch (error) {
484
+ if (!isErrno(error, "ENOENT"))
485
+ throw error;
486
+ }
487
+ const next = update(current);
488
+ if (next === undefined)
489
+ return current;
490
+ await lock.assertOwned();
491
+ await atomicWriteUnlocked(filePath, JSON.stringify(next, null, 2));
492
+ return next;
493
+ });
260
494
  }
261
495
  // ── Surgeon Attempt State ────────────────────────────────
262
496
  const SURGEON_ATTEMPTS_FILE = "workflow/surgeon-attempts.json";
@@ -297,6 +531,10 @@ export async function writeSurgeonAttempts(projectDir, state, sessionId) {
297
531
  const filePath = join(stateDir, SURGEON_ATTEMPTS_FILE);
298
532
  await atomicWrite(filePath, JSON.stringify(state, null, 2));
299
533
  }
534
+ export async function updateSurgeonAttempts(projectDir, sessionId, update) {
535
+ const filePath = join(resolveStateDir(projectDir, sessionId), SURGEON_ATTEMPTS_FILE);
536
+ return atomicUpdateJson(filePath, defaultSurgeonState(), update);
537
+ }
300
538
  const SESSION_READS_FILE = "workflow/session-reads.json";
301
539
  /**
302
540
  * Read session reads state. Returns empty reads if not found.
@@ -326,11 +564,10 @@ export async function writeSessionReads(projectDir, state, sessionId) {
326
564
  * @deprecated Prefer `DNAStateManager.appendSessionRead()`.
327
565
  */
328
566
  export async function appendSessionRead(projectDir, filePath, sessionId) {
329
- const state = await readSessionReads(projectDir, sessionId);
330
- if (!state.read_files.includes(filePath)) {
331
- state.read_files.push(filePath);
332
- await writeSessionReads(projectDir, state, sessionId);
333
- }
567
+ const statePath = join(resolveStateDir(projectDir, sessionId), SESSION_READS_FILE);
568
+ await atomicUpdateJson(statePath, { session_id: sessionId ?? "", read_files: [] }, (state) => state.read_files.includes(filePath)
569
+ ? undefined
570
+ : { ...state, read_files: [...state.read_files, filePath] });
334
571
  }
335
572
  const VERIFIER_RESULTS_FILE = "workflow/verifier-results.json";
336
573
  export function verifierResultId(result) {
@@ -408,9 +645,8 @@ export async function writeVerifierResults(projectDir, results, sessionId) {
408
645
  await atomicWrite(filePath, JSON.stringify(results, null, 2));
409
646
  }
410
647
  export async function appendVerifierResult(projectDir, result, sessionId) {
411
- const results = await readVerifierResultsFile(projectDir, sessionId);
412
- results.push(normalizeVerifierResult(result));
413
- await writeVerifierResults(projectDir, results, sessionId);
648
+ const statePath = join(resolveStateDir(projectDir, sessionId), VERIFIER_RESULTS_FILE);
649
+ await atomicUpdateJson(statePath, [], (results) => [...results, normalizeVerifierResult(result)]);
414
650
  }
415
651
  const TRACE_DIR = "trace";
416
652
  const RUNTIME_DECISION_DIR = "runtime-decisions";
@@ -447,7 +683,7 @@ export async function appendTrace(projectDir, entry, sessionId) {
447
683
  return;
448
684
  }
449
685
  catch { /* file doesn't exist yet */ }
450
- await appendFile(globalPath, line, "utf-8");
686
+ await atomicAppend(globalPath, line);
451
687
  // 2. Session-local trace (if session isolated)
452
688
  if (sessionId) {
453
689
  const sessionDir = resolveStateDir(projectDir, sessionId);
@@ -459,7 +695,7 @@ export async function appendTrace(projectDir, entry, sessionId) {
459
695
  return;
460
696
  }
461
697
  catch { /* file doesn't exist yet */ }
462
- await appendFile(sessionTracePath, line, "utf-8");
698
+ await atomicAppend(sessionTracePath, line);
463
699
  }
464
700
  }
465
701
  catch {
@@ -595,7 +831,7 @@ export async function appendEvidenceCaptureEvent(projectDir, event) {
595
831
  const date = dateFromIso(sanitized.captured_at);
596
832
  const captureDir = join(projectDir, ".dna", EVIDENCE_CAPTURE_DIR);
597
833
  await mkdir(captureDir, { recursive: true });
598
- await appendFile(join(captureDir, evidenceCaptureFileName(date)), line, "utf-8");
834
+ await atomicAppend(join(captureDir, evidenceCaptureFileName(date)), line);
599
835
  }
600
836
  catch {
601
837
  // Fail-open: evidence capture write failure never affects hook execution
@@ -608,11 +844,11 @@ export async function appendRuntimeDecisionEvent(projectDir, event, sessionId) {
608
844
  const date = new Date().toISOString().slice(0, 10);
609
845
  const globalDir = join(projectDir, ".dna", "state", RUNTIME_DECISION_DIR);
610
846
  await mkdir(globalDir, { recursive: true });
611
- await appendFile(join(globalDir, decisionFileName(date)), line, "utf-8");
847
+ await atomicAppend(join(globalDir, decisionFileName(date)), line);
612
848
  if (sessionId) {
613
849
  const sessionDir = resolveStateDir(projectDir, projectedSessionDirName(sessionId));
614
850
  await mkdir(sessionDir, { recursive: true });
615
- await appendFile(join(sessionDir, "runtime-decisions.jsonl"), line, "utf-8");
851
+ await atomicAppend(join(sessionDir, "runtime-decisions.jsonl"), line);
616
852
  }
617
853
  }
618
854
  catch {
@@ -865,8 +1101,8 @@ export async function rotateTraces(projectDir) {
865
1101
  }
866
1102
  // ── Stale State Cleanup ──────────────────────────────────
867
1103
  /**
868
- * Clean up stale state from `.dna/state/sessions/` and root state.
869
- * Removes workflow.json files older than DEFAULT_STALENESS_MS (2h).
1104
+ * Clean up stale worker state from `.dna/state/sessions/`.
1105
+ * Removes worker directories older than DEFAULT_STALENESS_MS (2h).
870
1106
  * Called on SessionStart to prevent state accumulation.
871
1107
  * Fail-open: never throws.
872
1108
  */
@@ -910,21 +1146,32 @@ export async function cleanStaleState(projectDir) {
910
1146
  catch {
911
1147
  // No sessions dir — nothing to clean
912
1148
  }
913
- // Also clean root-level stale workflow state
914
- const rootWfPath = join(projectDir, ".dna", "state", WORKFLOW_FILE);
1149
+ const rootWorkflowPath = join(projectDir, ".dna", "state", WORKFLOW_FILE);
915
1150
  try {
916
- const raw = await readFile(rootWfPath, "utf-8");
917
- const state = JSON.parse(raw);
918
- if (state.started_at) {
919
- const age = Date.now() - new Date(state.started_at).getTime();
920
- if (age > DEFAULT_STALENESS_MS) {
921
- await unlink(rootWfPath).catch(() => { });
922
- removed++;
923
- }
1151
+ const state = JSON.parse(await readFile(rootWorkflowPath, "utf-8"));
1152
+ if (state.started_at
1153
+ && Date.now() - new Date(state.started_at).getTime()
1154
+ > DEFAULT_STALENESS_MS) {
1155
+ await unlink(rootWorkflowPath).catch(() => { });
1156
+ removed += 1;
924
1157
  }
925
1158
  }
926
1159
  catch {
927
- // No root workflow state
1160
+ // No valid root workflow state.
928
1161
  }
929
1162
  return removed;
930
1163
  }
1164
+ /**
1165
+ * Delete exactly one disposable worker's Hook context.
1166
+ *
1167
+ * Controller ledgers live outside `.dna/state/sessions` and are never inspected
1168
+ * or removed by this operation.
1169
+ */
1170
+ export async function cleanupWorkerHookState(projectDir, workerSessionId) {
1171
+ const workerDir = resolveStateDir(projectDir, safePathComponent(workerSessionId, "worker_session_id"));
1172
+ const sessionsDir = join(projectDir, ".dna", "state", "sessions");
1173
+ if (dirname(workerDir) !== sessionsDir) {
1174
+ throw new Error("Refusing to clean non-worker Hook state");
1175
+ }
1176
+ await rm(workerDir, { recursive: true, force: true });
1177
+ }
package/dist/mcp/index.js CHANGED
File without changes
@@ -0,0 +1,11 @@
1
+ export type DiagnosisContractVerifyMode = "analyze" | "review";
2
+ export interface DiagnosisContractVerifyOptions {
3
+ projectDir: string;
4
+ moduleName: string;
5
+ mode: DiagnosisContractVerifyMode;
6
+ }
7
+ export interface DiagnosisContractVerifyResult {
8
+ passed: boolean;
9
+ reason?: string;
10
+ evidence?: string;
11
+ }