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/enforce.js
CHANGED
|
@@ -33,7 +33,7 @@ export function enforcePreToolUse(ir, input, state, roles) {
|
|
|
33
33
|
if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
|
|
34
34
|
const relaxed = isStepScopeRelaxed(ir, state);
|
|
35
35
|
if (!relaxed) {
|
|
36
|
-
const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
|
|
36
|
+
const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state), state?.workflowState?.strict_boundary === true);
|
|
37
37
|
if (result)
|
|
38
38
|
return { output: result, trace: { matched_rule: "scope", role: input.agent_type } };
|
|
39
39
|
}
|
|
@@ -46,7 +46,7 @@ export function enforcePreToolUse(ir, input, state, roles) {
|
|
|
46
46
|
}
|
|
47
47
|
// Layer 4: Pre-execution gates
|
|
48
48
|
if (ir.pre_execution_gates.length > 0) {
|
|
49
|
-
const result = enforceGates(ir.pre_execution_gates, input);
|
|
49
|
+
const result = enforceGates(ir.pre_execution_gates, input, state?.workflowState?.strict_boundary === true);
|
|
50
50
|
if (result)
|
|
51
51
|
return { output: result, trace: { matched_rule: "gate" } };
|
|
52
52
|
}
|
|
@@ -56,11 +56,19 @@ export function enforcePreToolUse(ir, input, state, roles) {
|
|
|
56
56
|
if (result)
|
|
57
57
|
return { output: result, trace: { matched_rule: "output_schema" } };
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if (state?.workflowState
|
|
60
|
+
&& !state.workflowState.strict_boundary
|
|
61
|
+
&& ir.workflows_ir) {
|
|
61
62
|
const result = enforceHandoffConsumes(ir, state.workflowState, state.artifactFacts ?? []);
|
|
62
|
-
if (result)
|
|
63
|
-
return {
|
|
63
|
+
if (result) {
|
|
64
|
+
return {
|
|
65
|
+
output: result,
|
|
66
|
+
trace: {
|
|
67
|
+
matched_rule: "handoff",
|
|
68
|
+
step_id: state.workflowState.current_step,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
64
72
|
}
|
|
65
73
|
return null;
|
|
66
74
|
}
|
|
@@ -352,27 +360,26 @@ function getStepAdditionalPaths(ir, state) {
|
|
|
352
360
|
return rule?.additional_write_paths ?? [];
|
|
353
361
|
}
|
|
354
362
|
// ── Internal: Layer Enforcement ────────────────────────────
|
|
355
|
-
/**
|
|
356
|
-
* Enforce handoff consumes — verify that the current step's consumed
|
|
357
|
-
* artifacts have been produced by a preceding step.
|
|
358
|
-
* Returns block output if a required artifact is missing, null if all satisfied.
|
|
359
|
-
*/
|
|
360
363
|
function enforceHandoffConsumes(ir, wfState, artifactFacts) {
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
364
|
+
const activeWorkflow = ir.workflows_ir?.find((workflow) => workflow.workflow_name === wfState.workflow);
|
|
365
|
+
if (!activeWorkflow || (activeWorkflow.handoff_chain?.length ?? 0) === 0) {
|
|
363
366
|
return null;
|
|
364
|
-
|
|
367
|
+
}
|
|
368
|
+
const currentEntry = activeWorkflow.handoff_chain.find((entry) => entry.step_id === wfState.current_step);
|
|
365
369
|
if (!currentEntry?.consumes || currentEntry.consumes.length === 0)
|
|
366
370
|
return null;
|
|
367
371
|
for (const consumed of currentEntry.consumes) {
|
|
368
372
|
if (consumed.required === false)
|
|
369
373
|
continue;
|
|
370
|
-
const producerStep = consumed.from
|
|
371
|
-
|
|
374
|
+
const producerStep = consumed.from
|
|
375
|
+
?? findProducerStep(activeWorkflow.handoff_chain, consumed)
|
|
376
|
+
?? "external";
|
|
377
|
+
const fact = artifactFacts.find((entry) => artifactMatches(entry, consumed, producerStep, wfState.workflow));
|
|
372
378
|
if (!fact) {
|
|
373
379
|
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires artifact '${consumed.description}' from '${producerStep}', but no verified manifest was resolved.`);
|
|
374
380
|
}
|
|
375
|
-
if (consumed.type === "git_commit"
|
|
381
|
+
if (consumed.type === "git_commit"
|
|
382
|
+
&& typeof fact.metadata?.commit !== "string") {
|
|
376
383
|
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires git_commit artifact from '${producerStep}', but no commit identity was recorded.`);
|
|
377
384
|
}
|
|
378
385
|
}
|
|
@@ -411,9 +418,14 @@ export function enforceHandoffProduces(ir, wfState) {
|
|
|
411
418
|
return null;
|
|
412
419
|
}
|
|
413
420
|
function findProducerStep(handoffChain, consumed) {
|
|
414
|
-
const producer = handoffChain.find(
|
|
415
|
-
(
|
|
416
|
-
|
|
421
|
+
const producer = handoffChain.find((entry) => entry.produces?.some((produced) => produced.type === consumed.type
|
|
422
|
+
&& ((produced.path && consumed.path && produced.path === consumed.path)
|
|
423
|
+
|| (produced.name
|
|
424
|
+
&& consumed.name
|
|
425
|
+
&& produced.name === consumed.name)
|
|
426
|
+
|| (produced.artifact_id
|
|
427
|
+
&& consumed.artifact_id
|
|
428
|
+
&& produced.artifact_id === consumed.artifact_id))));
|
|
417
429
|
return producer?.step_id ?? null;
|
|
418
430
|
}
|
|
419
431
|
function artifactMatches(fact, artifact, producerStep, workflow) {
|
|
@@ -431,7 +443,7 @@ function artifactMatches(fact, artifact, producerStep, workflow) {
|
|
|
431
443
|
return false;
|
|
432
444
|
return true;
|
|
433
445
|
}
|
|
434
|
-
function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
446
|
+
function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = [], strictBoundary = false) {
|
|
435
447
|
if (!input.agent_type)
|
|
436
448
|
return null;
|
|
437
449
|
// Standard write tools: check single file path
|
|
@@ -450,7 +462,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
|
450
462
|
if (writePaths.length === 0)
|
|
451
463
|
return null;
|
|
452
464
|
for (const p of writePaths) {
|
|
453
|
-
const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths,
|
|
465
|
+
const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths, strictBoundary, strictBoundary);
|
|
454
466
|
if (result)
|
|
455
467
|
return result;
|
|
456
468
|
}
|
|
@@ -459,7 +471,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
|
459
471
|
return null;
|
|
460
472
|
}
|
|
461
473
|
/** Check a single file path against role scope. Shared by Write tools and Bash. */
|
|
462
|
-
function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = [], blockViolation = true) {
|
|
474
|
+
function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = [], blockViolation = true, blockUnknownRole = false) {
|
|
463
475
|
const cwd = input.cwd;
|
|
464
476
|
let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
|
|
465
477
|
// Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
|
|
@@ -487,7 +499,9 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePa
|
|
|
487
499
|
}
|
|
488
500
|
return null; // Role matched, write allowed
|
|
489
501
|
}
|
|
490
|
-
return
|
|
502
|
+
return blockUnknownRole
|
|
503
|
+
? blockOutput("[Intent DNA] role_mismatch: worker role has no configured role scope.")
|
|
504
|
+
: null;
|
|
491
505
|
}
|
|
492
506
|
/**
|
|
493
507
|
* Verify a list of paths against role scope. Returns violation messages.
|
|
@@ -495,10 +509,12 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePa
|
|
|
495
509
|
*/
|
|
496
510
|
function verifyPathsAgainstScope(rolesScopeMap, agentType, paths, cwd) {
|
|
497
511
|
const violations = [];
|
|
512
|
+
let matchedRole = false;
|
|
498
513
|
for (const entry of rolesScopeMap) {
|
|
499
514
|
const agentTypeName = `dna-${toKebabCase(entry.role_name)}`;
|
|
500
515
|
if (agentType !== agentTypeName)
|
|
501
516
|
continue;
|
|
517
|
+
matchedRole = true;
|
|
502
518
|
const writeGlobs = entry.scope.write ?? [];
|
|
503
519
|
for (const p of paths) {
|
|
504
520
|
const relativePath = cwd && p.startsWith("/") ? relative(cwd, p) : p;
|
|
@@ -508,6 +524,9 @@ function verifyPathsAgainstScope(rolesScopeMap, agentType, paths, cwd) {
|
|
|
508
524
|
}
|
|
509
525
|
break; // Found the role, done
|
|
510
526
|
}
|
|
527
|
+
if (!matchedRole) {
|
|
528
|
+
violations.push(`agent_type '${agentType}' has no configured role scope`);
|
|
529
|
+
}
|
|
511
530
|
return violations;
|
|
512
531
|
}
|
|
513
532
|
function enforceToolFilters(filters, input) {
|
|
@@ -529,11 +548,21 @@ function enforceToolFilters(filters, input) {
|
|
|
529
548
|
}
|
|
530
549
|
return null;
|
|
531
550
|
}
|
|
532
|
-
function enforceGates(gates, input) {
|
|
551
|
+
function enforceGates(gates, input, strictBoundary = false) {
|
|
533
552
|
for (const gate of gates) {
|
|
534
553
|
const matches = evaluateGateCondition(gate.condition, input);
|
|
535
|
-
if (matches === null)
|
|
536
|
-
|
|
554
|
+
if (matches === null) {
|
|
555
|
+
if (!strictBoundary)
|
|
556
|
+
continue;
|
|
557
|
+
const detail = `Gate condition '${gate.condition}' could not be evaluated.`;
|
|
558
|
+
if (gate.action === "block") {
|
|
559
|
+
return blockOutput(`[Intent DNA] gate_evaluation: ${detail}`);
|
|
560
|
+
}
|
|
561
|
+
if (gate.action === "escalate") {
|
|
562
|
+
return escalateOutput(`[Intent DNA] gate_evaluation: ${detail}`);
|
|
563
|
+
}
|
|
564
|
+
return allowOutput(`WARN [Intent DNA] gate_evaluation: ${detail}`);
|
|
565
|
+
}
|
|
537
566
|
if (!matches)
|
|
538
567
|
continue; // Condition not triggered
|
|
539
568
|
if (gate.action === "block") {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HookEvent } from "./event-registry.js";
|
|
2
|
+
import { type HookOutput } from "./protocol.js";
|
|
3
|
+
export type HookFailureKind = "malformed_input" | "missing_ir" | "role_mismatch" | "gate_evaluation" | "bash_write_scope";
|
|
4
|
+
export type HookFailureDisposition = "advisory" | "blocking";
|
|
5
|
+
/**
|
|
6
|
+
* Foundation Hook failure policy.
|
|
7
|
+
*
|
|
8
|
+
* Pre-execution and terminal-boundary events block when enforcement cannot be
|
|
9
|
+
* evaluated safely. Observation-only and post-execution events remain advisory.
|
|
10
|
+
* Bash scope is blocking before execution and advisory after the write occurred.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hookFailureDisposition(event: HookEvent, kind: HookFailureKind): HookFailureDisposition;
|
|
13
|
+
export declare function hookFailureOutput(event: HookEvent, kind: HookFailureKind, detail: string): HookOutput;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { allowOutput, blockOutput } from "./protocol.js";
|
|
2
|
+
const BLOCKING_EVENTS = new Set([
|
|
3
|
+
"PreToolUse",
|
|
4
|
+
"SubagentStop",
|
|
5
|
+
"Stop",
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Foundation Hook failure policy.
|
|
9
|
+
*
|
|
10
|
+
* Pre-execution and terminal-boundary events block when enforcement cannot be
|
|
11
|
+
* evaluated safely. Observation-only and post-execution events remain advisory.
|
|
12
|
+
* Bash scope is blocking before execution and advisory after the write occurred.
|
|
13
|
+
*/
|
|
14
|
+
export function hookFailureDisposition(event, kind) {
|
|
15
|
+
if (kind === "gate_evaluation") {
|
|
16
|
+
return BLOCKING_EVENTS.has(event) ? "blocking" : "advisory";
|
|
17
|
+
}
|
|
18
|
+
if (kind === "bash_write_scope") {
|
|
19
|
+
return event === "PreToolUse" ? "blocking" : "advisory";
|
|
20
|
+
}
|
|
21
|
+
if (kind === "role_mismatch") {
|
|
22
|
+
return event === "PreToolUse" || event === "SubagentStop"
|
|
23
|
+
? "blocking"
|
|
24
|
+
: "advisory";
|
|
25
|
+
}
|
|
26
|
+
return BLOCKING_EVENTS.has(event) ? "blocking" : "advisory";
|
|
27
|
+
}
|
|
28
|
+
export function hookFailureOutput(event, kind, detail) {
|
|
29
|
+
const message = `[Intent DNA] ${kind}: ${detail}`;
|
|
30
|
+
return hookFailureDisposition(event, kind) === "blocking"
|
|
31
|
+
? blockOutput(message)
|
|
32
|
+
: allowOutput(`WARN ${message}`, event);
|
|
33
|
+
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Public exports for the hook enforcement engine.
|
|
5
5
|
* Used by the `dna-hook` CLI and importable for programmatic use.
|
|
6
6
|
*/
|
|
7
|
-
export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
7
|
+
export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, type StdinReadResult, readStdin, readStdinResult, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
8
|
+
export { type HookFailureDisposition, type HookFailureKind, hookFailureDisposition, hookFailureOutput, } from "./enforcement-boundary.js";
|
|
8
9
|
export { type EnforceState, type SessionStartInput, type StopEnforceInput, type StopWorkflowContext, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
9
|
-
export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
|
|
10
|
+
export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, cleanupWorkerHookState, appendAudit, } from "./state.js";
|
|
10
11
|
export { type SessionSummary, computeSummary, formatSummary, } from "./cli.js";
|
package/dist/hooks/index.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Used by the `dna-hook` CLI and importable for programmatic use.
|
|
6
6
|
*/
|
|
7
7
|
// Protocol types and I/O
|
|
8
|
-
export { readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
8
|
+
export { readStdin, readStdinResult, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
9
|
+
export { hookFailureDisposition, hookFailureOutput, } from "./enforcement-boundary.js";
|
|
9
10
|
// Enforcement engine
|
|
10
11
|
export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
11
12
|
// State management
|
|
12
|
-
export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
|
|
13
|
+
export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, cleanupWorkerHookState, appendAudit, } from "./state.js";
|
|
13
14
|
// Session summary
|
|
14
15
|
export { computeSummary, formatSummary, } from "./cli.js";
|
package/dist/hooks/protocol.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export type { HookEvent } from "./event-registry.js";
|
|
|
11
11
|
export interface HookInputBase {
|
|
12
12
|
cwd?: string;
|
|
13
13
|
sessionId?: string;
|
|
14
|
+
worker_session_id?: string;
|
|
15
|
+
run_id?: string;
|
|
16
|
+
step_id?: string;
|
|
17
|
+
attempt_id?: string;
|
|
14
18
|
}
|
|
15
19
|
export interface PreToolUseInput extends HookInputBase {
|
|
16
20
|
tool_name: string;
|
|
@@ -76,10 +80,14 @@ export declare function escalateOutput(reason: string): HookOutput;
|
|
|
76
80
|
export declare function stopOutput(systemMessage?: string): HookOutput;
|
|
77
81
|
/** Create a silent allow output (no visible output to user). */
|
|
78
82
|
export declare function silentOutput(): HookOutput;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
export interface StdinReadResult {
|
|
84
|
+
ok: boolean;
|
|
85
|
+
data: Record<string, unknown>;
|
|
86
|
+
error?: "timeout" | "empty" | "malformed" | "read_error";
|
|
87
|
+
}
|
|
88
|
+
/** Read JSON from stdin without erasing malformed-input evidence. */
|
|
89
|
+
export declare function readStdinResult(timeoutMs?: number): Promise<StdinReadResult>;
|
|
90
|
+
/** Compatibility reader for non-enforcement call sites. */
|
|
83
91
|
export declare function readStdin(timeoutMs?: number): Promise<Record<string, unknown>>;
|
|
84
92
|
/**
|
|
85
93
|
* Write hook output to stdout as JSON.
|
package/dist/hooks/protocol.js
CHANGED
|
@@ -46,16 +46,12 @@ export function stopOutput(systemMessage) {
|
|
|
46
46
|
export function silentOutput() {
|
|
47
47
|
return { continue: true, suppressOutput: true };
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* Read JSON from stdin with timeout protection.
|
|
52
|
-
* Returns parsed object on success, empty object on failure (fail-open).
|
|
53
|
-
*/
|
|
54
|
-
export function readStdin(timeoutMs = 5000) {
|
|
49
|
+
/** Read JSON from stdin without erasing malformed-input evidence. */
|
|
50
|
+
export function readStdinResult(timeoutMs = 5000) {
|
|
55
51
|
return new Promise((resolve) => {
|
|
56
52
|
const chunks = [];
|
|
57
53
|
let settled = false;
|
|
58
|
-
const finish = (
|
|
54
|
+
const finish = (result) => {
|
|
59
55
|
if (settled)
|
|
60
56
|
return;
|
|
61
57
|
settled = true;
|
|
@@ -63,9 +59,9 @@ export function readStdin(timeoutMs = 5000) {
|
|
|
63
59
|
process.stdin.removeAllListeners("data");
|
|
64
60
|
process.stdin.removeAllListeners("end");
|
|
65
61
|
process.stdin.removeAllListeners("error");
|
|
66
|
-
resolve(
|
|
62
|
+
resolve(result);
|
|
67
63
|
};
|
|
68
|
-
const timer = setTimeout(() => finish({}), timeoutMs);
|
|
64
|
+
const timer = setTimeout(() => finish({ ok: false, data: {}, error: "timeout" }), timeoutMs);
|
|
69
65
|
process.stdin.on("data", (chunk) => {
|
|
70
66
|
chunks.push(chunk);
|
|
71
67
|
});
|
|
@@ -73,23 +69,33 @@ export function readStdin(timeoutMs = 5000) {
|
|
|
73
69
|
try {
|
|
74
70
|
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
75
71
|
if (!raw) {
|
|
76
|
-
finish({});
|
|
72
|
+
finish({ ok: false, data: {}, error: "empty" });
|
|
77
73
|
return;
|
|
78
74
|
}
|
|
79
75
|
const parsed = JSON.parse(raw);
|
|
80
|
-
|
|
76
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
77
|
+
finish({ ok: false, data: {}, error: "malformed" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
finish({ ok: true, data: parsed });
|
|
81
81
|
}
|
|
82
82
|
catch {
|
|
83
|
-
finish({});
|
|
83
|
+
finish({ ok: false, data: {}, error: "malformed" });
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
|
-
process.stdin.on("error", () =>
|
|
86
|
+
process.stdin.on("error", () => {
|
|
87
|
+
finish({ ok: false, data: {}, error: "read_error" });
|
|
88
|
+
});
|
|
87
89
|
// If stdin is already ended (piped and closed)
|
|
88
90
|
if (process.stdin.readableEnded) {
|
|
89
|
-
finish({});
|
|
91
|
+
finish({ ok: false, data: {}, error: "empty" });
|
|
90
92
|
}
|
|
91
93
|
});
|
|
92
94
|
}
|
|
95
|
+
/** Compatibility reader for non-enforcement call sites. */
|
|
96
|
+
export async function readStdin(timeoutMs = 5000) {
|
|
97
|
+
return (await readStdinResult(timeoutMs)).data;
|
|
98
|
+
}
|
|
93
99
|
/**
|
|
94
100
|
* Write hook output to stdout as JSON.
|
|
95
101
|
*/
|
package/dist/hooks/schema.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook input validation — CC protocol boundary.
|
|
3
3
|
* Zero external dependencies. Hand-written validators.
|
|
4
|
-
*
|
|
4
|
+
* Classification of invalid input belongs to the event boundary.
|
|
5
5
|
*/
|
|
6
6
|
export interface ValidationResult {
|
|
7
7
|
valid: boolean;
|
|
@@ -15,6 +15,7 @@ export interface ValidationResult {
|
|
|
15
15
|
export declare function validateHookInput(event: string, raw: unknown): ValidationResult;
|
|
16
16
|
/**
|
|
17
17
|
* Normalize session_id field from various formats:
|
|
18
|
+
* - worker_session_id (disposable Controller worker identity)
|
|
18
19
|
* - session_id (snake_case from CC)
|
|
19
20
|
* - sessionId (camelCase internal)
|
|
20
21
|
* - CLAUDE_SESSION_ID (env var fallback)
|
package/dist/hooks/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook input validation — CC protocol boundary.
|
|
3
3
|
* Zero external dependencies. Hand-written validators.
|
|
4
|
-
*
|
|
4
|
+
* Classification of invalid input belongs to the event boundary.
|
|
5
5
|
*/
|
|
6
6
|
import { isHookEventSupportedBySurface } from "./event-registry.js";
|
|
7
7
|
/**
|
|
@@ -15,7 +15,7 @@ export function validateHookInput(event, raw) {
|
|
|
15
15
|
const input = raw;
|
|
16
16
|
const errors = [];
|
|
17
17
|
const normalized = { ...input };
|
|
18
|
-
//
|
|
18
|
+
// The Controller's disposable worker session is the Hook context key.
|
|
19
19
|
const sessionId = normalizeSessionId(input);
|
|
20
20
|
if (sessionId !== undefined) {
|
|
21
21
|
normalized.sessionId = sessionId;
|
|
@@ -72,11 +72,15 @@ export function validateHookInput(event, raw) {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Normalize session_id field from various formats:
|
|
75
|
+
* - worker_session_id (disposable Controller worker identity)
|
|
75
76
|
* - session_id (snake_case from CC)
|
|
76
77
|
* - sessionId (camelCase internal)
|
|
77
78
|
* - CLAUDE_SESSION_ID (env var fallback)
|
|
78
79
|
*/
|
|
79
80
|
export function normalizeSessionId(input) {
|
|
81
|
+
if (typeof input.worker_session_id === "string") {
|
|
82
|
+
return input.worker_session_id;
|
|
83
|
+
}
|
|
80
84
|
if (typeof input.sessionId === "string") {
|
|
81
85
|
return input.sessionId;
|
|
82
86
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Intent DNA — DNAStateManager
|
|
3
3
|
*
|
|
4
4
|
* Class-based facade over state.ts module functions. Bundles projectDir +
|
|
5
|
-
*
|
|
5
|
+
* disposable workerSessionId into a single object so call sites don't repeat it.
|
|
6
6
|
* Adds experience chain persistence (markdown + structured JSON) and
|
|
7
7
|
* lifecycle operations (init / cleanup / isStale) that state.ts lacks.
|
|
8
8
|
*
|
|
@@ -22,8 +22,8 @@ export interface ExperienceEntry {
|
|
|
22
22
|
}
|
|
23
23
|
export declare class DNAStateManager {
|
|
24
24
|
private readonly projectDir;
|
|
25
|
-
private readonly
|
|
26
|
-
constructor(projectDir: string,
|
|
25
|
+
private readonly workerSessionId?;
|
|
26
|
+
constructor(projectDir: string, workerSessionId?: string | undefined);
|
|
27
27
|
readWorkflowState(stalenessMs?: number): Promise<DNAWorkflowState | null>;
|
|
28
28
|
writeWorkflowState(state: DNAWorkflowState): Promise<void>;
|
|
29
29
|
clearWorkflowState(): Promise<void>;
|
|
@@ -38,8 +38,8 @@ export declare class DNAStateManager {
|
|
|
38
38
|
/** Ensure session directory structure exists. */
|
|
39
39
|
init(): Promise<void>;
|
|
40
40
|
/**
|
|
41
|
-
* Remove this
|
|
42
|
-
*
|
|
41
|
+
* Remove only this disposable worker's Hook directory.
|
|
42
|
+
* Durable Controller run/task/result/handoff records are outside this boundary.
|
|
43
43
|
*/
|
|
44
44
|
cleanup(): Promise<void>;
|
|
45
45
|
/** True when workflow.json started_at exceeds the staleness threshold. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Intent DNA — DNAStateManager
|
|
3
3
|
*
|
|
4
4
|
* Class-based facade over state.ts module functions. Bundles projectDir +
|
|
5
|
-
*
|
|
5
|
+
* disposable workerSessionId into a single object so call sites don't repeat it.
|
|
6
6
|
* Adds experience chain persistence (markdown + structured JSON) and
|
|
7
7
|
* lifecycle operations (init / cleanup / isStale) that state.ts lacks.
|
|
8
8
|
*
|
|
@@ -10,29 +10,31 @@
|
|
|
10
10
|
* class delegates to them. Prefer this class in new code; state.ts exports
|
|
11
11
|
* are @deprecated for direct use.
|
|
12
12
|
*/
|
|
13
|
-
import { readFile, mkdir,
|
|
13
|
+
import { readFile, mkdir, stat } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { resolveStateDir, readWorkflowState as rwState, writeWorkflowState as wwState, clearWorkflowState as cwState, appendAudit as aAudit, readSurgeonAttempts as rSurg, writeSurgeonAttempts as wSurg, readSessionReads as rReads, appendSessionRead as aRead, atomicWrite, } from "./state.js";
|
|
15
|
+
import { resolveStateDir, readWorkflowState as rwState, writeWorkflowState as wwState, clearWorkflowState as cwState, appendAudit as aAudit, readSurgeonAttempts as rSurg, writeSurgeonAttempts as wSurg, readSessionReads as rReads, appendSessionRead as aRead, atomicWrite, atomicUpdateJson, cleanupWorkerHookState, } from "./state.js";
|
|
16
16
|
const EXPERIENCE_JSON = "workflow/experience.json";
|
|
17
17
|
const EXPERIENCE_MD = "workflow/experience.md";
|
|
18
18
|
const DEFAULT_STALENESS_MS = 2 * 60 * 60 * 1000; // 2h — matches state.ts
|
|
19
19
|
// ── Manager ────────────────────────────────────────────────
|
|
20
20
|
export class DNAStateManager {
|
|
21
21
|
projectDir;
|
|
22
|
-
|
|
23
|
-
constructor(projectDir,
|
|
22
|
+
workerSessionId;
|
|
23
|
+
constructor(projectDir, workerSessionId) {
|
|
24
24
|
this.projectDir = projectDir;
|
|
25
|
-
this.
|
|
25
|
+
this.workerSessionId = workerSessionId;
|
|
26
26
|
}
|
|
27
27
|
// ── Workflow state ──
|
|
28
28
|
readWorkflowState(stalenessMs = DEFAULT_STALENESS_MS) {
|
|
29
|
-
return rwState(this.projectDir, this.
|
|
29
|
+
return rwState(this.projectDir, this.workerSessionId, stalenessMs);
|
|
30
30
|
}
|
|
31
31
|
writeWorkflowState(state) {
|
|
32
|
-
return wwState(this.projectDir, state, this.
|
|
32
|
+
return wwState(this.projectDir, state, this.workerSessionId);
|
|
33
33
|
}
|
|
34
34
|
clearWorkflowState() {
|
|
35
|
-
return
|
|
35
|
+
return this.workerSessionId
|
|
36
|
+
? cleanupWorkerHookState(this.projectDir, this.workerSessionId)
|
|
37
|
+
: cwState(this.projectDir);
|
|
36
38
|
}
|
|
37
39
|
// ── Audit log ──
|
|
38
40
|
appendAudit(entry) {
|
|
@@ -40,17 +42,17 @@ export class DNAStateManager {
|
|
|
40
42
|
}
|
|
41
43
|
// ── Surgeon attempts ──
|
|
42
44
|
readSurgeonAttempts() {
|
|
43
|
-
return rSurg(this.projectDir, this.
|
|
45
|
+
return rSurg(this.projectDir, this.workerSessionId);
|
|
44
46
|
}
|
|
45
47
|
writeSurgeonAttempts(state) {
|
|
46
|
-
return wSurg(this.projectDir, state, this.
|
|
48
|
+
return wSurg(this.projectDir, state, this.workerSessionId);
|
|
47
49
|
}
|
|
48
50
|
// ── Session reads ──
|
|
49
51
|
readSessionReads() {
|
|
50
|
-
return rReads(this.projectDir, this.
|
|
52
|
+
return rReads(this.projectDir, this.workerSessionId);
|
|
51
53
|
}
|
|
52
54
|
appendSessionRead(filePath) {
|
|
53
|
-
return aRead(this.projectDir, filePath, this.
|
|
55
|
+
return aRead(this.projectDir, filePath, this.workerSessionId);
|
|
54
56
|
}
|
|
55
57
|
// ── Experience chain (new) ──
|
|
56
58
|
async readExperienceChain() {
|
|
@@ -69,28 +71,28 @@ export class DNAStateManager {
|
|
|
69
71
|
await atomicWrite(this.path(EXPERIENCE_MD), renderExperienceMarkdown(chain));
|
|
70
72
|
}
|
|
71
73
|
async appendExperience(entry) {
|
|
72
|
-
const chain = await this.
|
|
73
|
-
|
|
74
|
-
await this.writeExperienceChain(chain);
|
|
74
|
+
const chain = await atomicUpdateJson(this.path(EXPERIENCE_JSON), [], (current) => [...current, entry]);
|
|
75
|
+
await atomicWrite(this.path(EXPERIENCE_MD), renderExperienceMarkdown(chain));
|
|
75
76
|
}
|
|
76
77
|
// ── Lifecycle ──
|
|
77
78
|
/** Ensure session directory structure exists. */
|
|
78
79
|
async init() {
|
|
79
|
-
await mkdir(resolveStateDir(this.projectDir, this.
|
|
80
|
+
await mkdir(resolveStateDir(this.projectDir, this.workerSessionId), { recursive: true });
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
|
-
* Remove this
|
|
83
|
-
*
|
|
83
|
+
* Remove only this disposable worker's Hook directory.
|
|
84
|
+
* Durable Controller run/task/result/handoff records are outside this boundary.
|
|
84
85
|
*/
|
|
85
86
|
async cleanup() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
if (this.workerSessionId) {
|
|
88
|
+
await cleanupWorkerHookState(this.projectDir, this.workerSessionId);
|
|
89
|
+
return;
|
|
89
90
|
}
|
|
91
|
+
await cwState(this.projectDir);
|
|
90
92
|
}
|
|
91
93
|
/** True when workflow.json started_at exceeds the staleness threshold. */
|
|
92
94
|
async isStale(stalenessMs = DEFAULT_STALENESS_MS) {
|
|
93
|
-
const wfPath = join(resolveStateDir(this.projectDir, this.
|
|
95
|
+
const wfPath = join(resolveStateDir(this.projectDir, this.workerSessionId), "workflow.json");
|
|
94
96
|
try {
|
|
95
97
|
const raw = await readFile(wfPath, "utf-8");
|
|
96
98
|
const state = JSON.parse(raw);
|
|
@@ -110,7 +112,7 @@ export class DNAStateManager {
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
path(rel) {
|
|
113
|
-
return join(resolveStateDir(this.projectDir, this.
|
|
115
|
+
return join(resolveStateDir(this.projectDir, this.workerSessionId), rel);
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
// ── Markdown rendering ────────────────────────────────────
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface DNAWorkflowState {
|
|
|
21
21
|
current_role: string;
|
|
22
22
|
iteration: number;
|
|
23
23
|
session_id: string;
|
|
24
|
+
/** Disposable Controller worker identity used as the Hook state key. */
|
|
25
|
+
worker_session_id?: string;
|
|
26
|
+
/** Durable correlation only; Hook state is never authoritative for these IDs. */
|
|
27
|
+
run_id?: string;
|
|
28
|
+
step_id?: string;
|
|
29
|
+
attempt_id?: string;
|
|
24
30
|
started_at: string;
|
|
25
31
|
inputs?: Record<string, string>;
|
|
26
32
|
resolved_variables?: Record<string, string>;
|
|
@@ -145,8 +151,10 @@ export declare function appendCompletedArtifact(projectDir: string, stepId: stri
|
|
|
145
151
|
artifact_id?: string;
|
|
146
152
|
metadata?: Record<string, unknown>;
|
|
147
153
|
}, sessionId?: string): Promise<void>;
|
|
148
|
-
/** Atomic
|
|
154
|
+
/** Atomic owner-checked temp/fsync/rename write adapted from the approved store primitive. */
|
|
149
155
|
export declare function atomicWrite(filePath: string, data: string): Promise<void>;
|
|
156
|
+
export declare function atomicAppend(filePath: string, data: string): Promise<void>;
|
|
157
|
+
export declare function atomicUpdateJson<T>(filePath: string, fallback: T, update: (current: T) => T | undefined): Promise<T>;
|
|
150
158
|
/**
|
|
151
159
|
* Read surgeon attempt state. Returns default state if not found.
|
|
152
160
|
* @deprecated Prefer `DNAStateManager.readSurgeonAttempts()`.
|
|
@@ -157,6 +165,7 @@ export declare function readSurgeonAttempts(projectDir: string, sessionId?: stri
|
|
|
157
165
|
* @deprecated Prefer `DNAStateManager.writeSurgeonAttempts()`.
|
|
158
166
|
*/
|
|
159
167
|
export declare function writeSurgeonAttempts(projectDir: string, state: SurgeonAttemptState, sessionId?: string): Promise<void>;
|
|
168
|
+
export declare function updateSurgeonAttempts(projectDir: string, sessionId: string, update: (current: SurgeonAttemptState) => SurgeonAttemptState): Promise<SurgeonAttemptState>;
|
|
160
169
|
/** Tracks which files a session has Read — for context gate enforcement */
|
|
161
170
|
export interface SessionReadsState {
|
|
162
171
|
session_id: string;
|
|
@@ -238,9 +247,16 @@ export declare function listSessions(projectDir: string): Promise<string[]>;
|
|
|
238
247
|
*/
|
|
239
248
|
export declare function rotateTraces(projectDir: string): Promise<number>;
|
|
240
249
|
/**
|
|
241
|
-
* Clean up stale state from `.dna/state/sessions
|
|
242
|
-
* Removes
|
|
250
|
+
* Clean up stale worker state from `.dna/state/sessions/`.
|
|
251
|
+
* Removes worker directories older than DEFAULT_STALENESS_MS (2h).
|
|
243
252
|
* Called on SessionStart to prevent state accumulation.
|
|
244
253
|
* Fail-open: never throws.
|
|
245
254
|
*/
|
|
246
255
|
export declare function cleanStaleState(projectDir: string): Promise<number>;
|
|
256
|
+
/**
|
|
257
|
+
* Delete exactly one disposable worker's Hook context.
|
|
258
|
+
*
|
|
259
|
+
* Controller ledgers live outside `.dna/state/sessions` and are never inspected
|
|
260
|
+
* or removed by this operation.
|
|
261
|
+
*/
|
|
262
|
+
export declare function cleanupWorkerHookState(projectDir: string, workerSessionId: string): Promise<void>;
|