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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -0
- package/dist/cli/commands/run-lifecycle.d.ts +73 -0
- package/dist/cli/commands/run-lifecycle.js +240 -0
- package/dist/cli/commands/run.d.ts +22 -40
- package/dist/cli/commands/run.js +674 -392
- package/dist/cli/index.js +87 -2
- package/dist/compiler/workflow.js +3 -2
- package/dist/hooks/cli.d.ts +1 -2
- package/dist/hooks/cli.js +119 -80
- package/dist/hooks/enforce.d.ts +2 -0
- package/dist/hooks/enforce.js +56 -27
- package/dist/hooks/enforcement-boundary.d.ts +13 -0
- package/dist/hooks/enforcement-boundary.js +33 -0
- package/dist/hooks/index.d.ts +3 -2
- package/dist/hooks/index.js +3 -2
- package/dist/hooks/protocol.d.ts +12 -4
- package/dist/hooks/protocol.js +20 -14
- package/dist/hooks/schema.d.ts +2 -1
- package/dist/hooks/schema.js +6 -2
- package/dist/hooks/state-manager.d.ts +5 -5
- package/dist/hooks/state-manager.js +26 -24
- package/dist/hooks/state.d.ts +19 -3
- package/dist/hooks/state.js +327 -80
- package/dist/mcp/index.js +0 -0
- package/dist/runtime/diagnosis-contract-verifier.d.ts +11 -0
- package/dist/runtime/diagnosis-contract-verifier.js +417 -0
- package/dist/runtime/execution-provider.d.ts +40 -0
- package/dist/runtime/execution-provider.js +138 -0
- package/dist/runtime/handoff-resolver.d.ts +61 -0
- package/dist/runtime/handoff-resolver.js +167 -0
- package/dist/runtime/index.d.ts +24 -0
- package/dist/runtime/index.js +13 -0
- package/dist/runtime/process-tree.d.ts +47 -0
- package/dist/runtime/process-tree.js +402 -0
- package/dist/runtime/providers/claude.d.ts +9 -0
- package/dist/runtime/providers/claude.js +64 -0
- package/dist/runtime/providers/codex.d.ts +8 -0
- package/dist/runtime/providers/codex.js +72 -0
- package/dist/runtime/result-store.d.ts +32 -0
- package/dist/runtime/result-store.js +130 -0
- package/dist/runtime/run-contracts.d.ts +290 -0
- package/dist/runtime/run-contracts.js +58 -0
- package/dist/runtime/run-controller.d.ts +149 -0
- package/dist/runtime/run-controller.js +1108 -0
- package/dist/runtime/run-store.d.ts +96 -0
- package/dist/runtime/run-store.js +725 -0
- package/dist/runtime/worker-executor.d.ts +19 -0
- package/dist/runtime/worker-executor.js +194 -0
- package/dist/runtime/workflow-plan-adapter.d.ts +26 -0
- package/dist/runtime/workflow-plan-adapter.js +416 -0
- package/dist/runtime/workflow-runner.d.ts +15 -3
- package/dist/runtime/workflow-runner.js +13 -1
- package/dist/runtime/workspace-isolation.d.ts +103 -0
- package/dist/runtime/workspace-isolation.js +373 -0
- package/dist/schema/types.d.ts +1 -0
- package/dist/schema/validate.js +64 -6
- package/dist/schema/yaml-parser.js +7 -2
- package/package.json +1 -1
package/dist/hooks/state.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
stepEntry
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
existing
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
412
|
-
results
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
869
|
-
* Removes
|
|
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
|
-
|
|
914
|
-
const rootWfPath = join(projectDir, ".dna", "state", WORKFLOW_FILE);
|
|
1149
|
+
const rootWorkflowPath = join(projectDir, ".dna", "state", WORKFLOW_FILE);
|
|
915
1150
|
try {
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
+
}
|