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/cli/index.js
CHANGED
|
@@ -25,7 +25,7 @@ Commands:
|
|
|
25
25
|
guard Zero-config guardrails — detect environment, apply safety rules
|
|
26
26
|
sync Compile + inject DNA into target file (CLAUDE.md, SOUL.md, etc.)
|
|
27
27
|
verify Verify synced files match .dna/lock checksums (drift detection)
|
|
28
|
-
run Execute a
|
|
28
|
+
run Execute and inspect a durable Controller workflow run
|
|
29
29
|
init Create a new DNA file interactively
|
|
30
30
|
generate Generate DNA config from natural language description
|
|
31
31
|
compile Compile DNA files to framework configuration
|
|
@@ -138,6 +138,10 @@ Examples:
|
|
|
138
138
|
dna import . Import existing harness configs into DNA format
|
|
139
139
|
dna run --dna my.dna.json --workflow dev-pipeline --task P5.8
|
|
140
140
|
dna run --dna my.dna.json --workflow dev-pipeline --task P5.8 --dry-run
|
|
141
|
+
dna run status <run-id> --json
|
|
142
|
+
dna run inspect <run-id>
|
|
143
|
+
dna run resume <run-id>
|
|
144
|
+
dna run cancel <run-id> --reason "operator request"
|
|
141
145
|
dna compile my.dna.json --context work
|
|
142
146
|
dna show my.dna.json --context work
|
|
143
147
|
dna evolve my_dna_id Preview marker changes without persisting
|
|
@@ -229,7 +233,35 @@ Boundaries:
|
|
|
229
233
|
guide: `Usage: dna guide [asset] [--asset <name>] [--name <org>] [--project <project>] [--goal <workflow>] [--sync-target <target>] [--brief] [--json]\n\nShows the current workflow governance route: organization setup, external-agent authoring/validated asset editing, asset governance, runtime sync, evidence review, and manager review.\n\nOptions:\n --asset <name> Focus the guide on a workflow asset (default prefers ${WORKFLOW_ASSET_IDS.default})\n --name <org> Organization/workspace name to use in suggested setup commands\n --project <project> Project name to use in suggested commands\n --goal <workflow> Natural-language workflow goal for intake/authoring-packet commands\n --sync-target <target> Runtime surface such as codex or claude for suggested commands\n --project-dir <path> Read organization state and project-local assets from another project directory\n --brief Print the current stage, current command, checkpoints, and near next steps only\n --json Print JSON to stdout\n\nBoundaries:\n - dna CLI/core/runtime owns product behavior and governance decisions\n - Claude Code and Codex are runtime surfaces/adapters\n - Dashboard is read-only projection\n - Obsidian/wiki is internal project memory, not product runtime\n - Web/public surface changes require Vercel CLI deploy + deployed URL verification\n`,
|
|
230
234
|
quickstart: `Usage: dna quickstart [asset] [--asset <name>] [--sync-target <target>] [--project-dir <path>] [--json]\n\nShows the shortest local route for getting value without Online or Supabase: choose a workflow asset, initialize local dna, preview sync, sync the harness, run in the harness, package/sign/verify local evidence, then inspect sync status.\n\nOptions:\n --asset <name> Workflow asset to start from (default prefers ${WORKFLOW_ASSET_IDS.default})\n --sync-target <target> Runtime surface such as codex or claude for suggested commands\n --project-dir <path> Read organization state and project-local assets from another project directory\n --json Print a concise intentdna.local_quickstart.v1 packet\n\nBoundaries:\n - quickstart is read-only guidance; it does not initialize, sync, run workflows, sign reports, upload reports, or deploy anything\n - local dna/dna-hook remain runtime truth\n - Online, marketplace, and Supabase do not execute workflows or make allow/warn/block/validate decisions\n - MCP remains hidden transport\n`,
|
|
231
235
|
context: `Usage: dna context <index|search|doc> [query|id] [files...]\n\nSubcommands:\n index List explicitly declared planning-only advisory context sources\n search <query> Search source metadata\n doc <id> Read a local file/doc source body\n`,
|
|
232
|
-
run: `Usage:
|
|
236
|
+
run: `Usage:
|
|
237
|
+
dna run [start] --dna <file> --workflow <name> --task <id> [options]
|
|
238
|
+
dna run status <run-id> [--json] [--project-dir <path>]
|
|
239
|
+
dna run inspect <run-id> [--json] [--project-dir <path>]
|
|
240
|
+
dna run resume <run-id> [--json] [--project-dir <path>]
|
|
241
|
+
dna run cancel <run-id> [--reason <text>] [--json] [--project-dir <path>]
|
|
242
|
+
|
|
243
|
+
Actions:
|
|
244
|
+
start Create a durable run and execute until terminal (default)
|
|
245
|
+
status Read the durable run summary
|
|
246
|
+
inspect Read attempts, worker sessions, handoffs, events, and results
|
|
247
|
+
resume Resume from durable task/attempt/result state
|
|
248
|
+
cancel Persist durable cancellation intent
|
|
249
|
+
|
|
250
|
+
Options:
|
|
251
|
+
--run-id <id> Explicit run id; lifecycle actions also accept a positional id
|
|
252
|
+
--dry-run Print the compiled Controller plan without starting a run
|
|
253
|
+
--provider <name> claude|codex (default: claude)
|
|
254
|
+
--provider-executable <path> Override the selected provider executable
|
|
255
|
+
--max-concurrency <n> Maximum simultaneously active Steps (default: 1)
|
|
256
|
+
--timeout-ms <n> Per-attempt timeout in milliseconds
|
|
257
|
+
--cancellation-grace-ms <n> Process-tree cancellation grace period (default: 5000)
|
|
258
|
+
--max-budget <usd> Claude budget per Step attempt (default: 5)
|
|
259
|
+
--permission-mode <mode> Claude permission mode (default: default)
|
|
260
|
+
--agents <dir> Project agent output directory (default: .claude/agents)
|
|
261
|
+
--reason <text> Cancellation reason
|
|
262
|
+
--json Print lifecycle output as JSON
|
|
263
|
+
--project-dir <path> Read/write the project-scoped runtime in another directory
|
|
264
|
+
`,
|
|
233
265
|
compile: `Usage: dna compile <files...> [--target claude-sdk] [--context <name>] [--output <path>] [--ir]\n`,
|
|
234
266
|
evolve: `Usage: dna evolve <dna-id> [--store-dir <path>] [--preview] [--apply]\n\nPreview self-evolution marker changes from recorded outcomes. This command is read-only; it never writes markers or runtime artifacts.\n\nOptions:\n --store-dir <path> Evolution store directory (default .dna/evolution)\n --preview Explicit preview flag; preview is the default behavior\n --apply Rejected in this kernel; use reviewed dna sync --evolve for mutation\n\nRoute:\n 1. Record outcomes: dna epigenetic record <dna-id> --action <action> --genes <gene[,gene]> --feedback '[{\"gene\":\"quality\",\"effect\":\"helped\"}]'\n 2. Inspect outcomes: dna epigenetic summary <dna-id>\n 3. Preview marker changes: dna evolve <dna-id>\n 4. Mutate only after review: dna sync --evolve\n\nBoundaries:\n - No outcomes means no evidence to evolve\n - dna evolve is preview-only and does not create missing outcome or marker directories\n - Marker mutation is allowed only through explicit reviewed sync/evolve gates\n`,
|
|
235
267
|
epigenetic: `Usage: dna epigenetic <record|summary|markers> <dna-id> [options]\n\nSubcommands:\n record Record an execution outcome for self-evolution\n summary Show outcome summary for a DNA id\n markers Show persisted epigenetic markers\n\nOptions:\n --store-dir <path> Evolution store directory (default .dna/evolution)\n --action <action> Action name for record\n --success Mark record outcome as success (default true)\n --genes <a,b> Active genes for record\n --feedback <json> Constraint feedback JSON array\n --json Print JSON for summary or markers\n\nExamples:\n dna epigenetic record template_flutter_rewrite --action fix-loop --genes quality --feedback '[{\"gene\":\"quality\",\"effect\":\"helped\"}]'\n dna epigenetic summary template_flutter_rewrite\n dna epigenetic markers template_flutter_rewrite\n`,
|
|
@@ -400,10 +432,48 @@ async function main() {
|
|
|
400
432
|
agents: { type: "string" },
|
|
401
433
|
hooks: { type: "string" },
|
|
402
434
|
"project-dir": { type: "string" },
|
|
435
|
+
"run-id": { type: "string" },
|
|
436
|
+
provider: { type: "string" },
|
|
437
|
+
"provider-executable": { type: "string" },
|
|
438
|
+
"max-concurrency": { type: "string" },
|
|
439
|
+
"timeout-ms": { type: "string" },
|
|
440
|
+
"cancellation-grace-ms": { type: "string" },
|
|
441
|
+
reason: { type: "string" },
|
|
442
|
+
json: { type: "boolean", default: false },
|
|
403
443
|
},
|
|
404
444
|
allowPositionals: true,
|
|
405
445
|
strict: false,
|
|
406
446
|
});
|
|
447
|
+
const runActions = new Set([
|
|
448
|
+
"start",
|
|
449
|
+
"status",
|
|
450
|
+
"inspect",
|
|
451
|
+
"resume",
|
|
452
|
+
"cancel",
|
|
453
|
+
]);
|
|
454
|
+
const actionCandidate = runPositionals[0] ?? "";
|
|
455
|
+
const runAction = runActions.has(actionCandidate)
|
|
456
|
+
? runPositionals.shift()
|
|
457
|
+
: "start";
|
|
458
|
+
const positionalRunId = runAction === "start"
|
|
459
|
+
? undefined
|
|
460
|
+
: runPositionals.shift();
|
|
461
|
+
const optionRunId = runValues["run-id"];
|
|
462
|
+
if (positionalRunId
|
|
463
|
+
&& optionRunId
|
|
464
|
+
&& positionalRunId !== optionRunId) {
|
|
465
|
+
process.stderr.write("Error: positional run id and --run-id must match when both are provided\n");
|
|
466
|
+
return exitWith(2);
|
|
467
|
+
}
|
|
468
|
+
if (runAction !== "start" && runPositionals.length > 0) {
|
|
469
|
+
process.stderr.write(`Error: dna run ${runAction} accepts exactly one run id\n`);
|
|
470
|
+
return exitWith(2);
|
|
471
|
+
}
|
|
472
|
+
const provider = runValues.provider;
|
|
473
|
+
if (provider && provider !== "claude" && provider !== "codex") {
|
|
474
|
+
process.stderr.write("Error: --provider must be one of: claude, codex\n");
|
|
475
|
+
return exitWith(2);
|
|
476
|
+
}
|
|
407
477
|
// Parse --var key=value pairs
|
|
408
478
|
const varEntries = runValues.var ?? [];
|
|
409
479
|
const vars = {};
|
|
@@ -415,6 +485,8 @@ async function main() {
|
|
|
415
485
|
}
|
|
416
486
|
const { runRun } = await import("./commands/run.js");
|
|
417
487
|
const code = await runRun({
|
|
488
|
+
action: runAction,
|
|
489
|
+
runId: optionRunId ?? positionalRunId,
|
|
418
490
|
dnaFiles: runValues.dna ?? runPositionals,
|
|
419
491
|
workflowName: runValues.workflow,
|
|
420
492
|
taskId: runValues.task,
|
|
@@ -429,6 +501,19 @@ async function main() {
|
|
|
429
501
|
agentsDir: runValues.agents,
|
|
430
502
|
hooksDir: runValues.hooks,
|
|
431
503
|
projectDir: runValues["project-dir"],
|
|
504
|
+
provider: provider,
|
|
505
|
+
providerExecutable: runValues["provider-executable"],
|
|
506
|
+
maxConcurrency: runValues["max-concurrency"] === undefined
|
|
507
|
+
? undefined
|
|
508
|
+
: Number(runValues["max-concurrency"]),
|
|
509
|
+
timeoutMs: runValues["timeout-ms"] === undefined
|
|
510
|
+
? undefined
|
|
511
|
+
: Number(runValues["timeout-ms"]),
|
|
512
|
+
cancellationGraceMs: runValues["cancellation-grace-ms"] === undefined
|
|
513
|
+
? undefined
|
|
514
|
+
: Number(runValues["cancellation-grace-ms"]),
|
|
515
|
+
reason: runValues.reason,
|
|
516
|
+
json: runValues.json,
|
|
432
517
|
});
|
|
433
518
|
return exitWith(code);
|
|
434
519
|
break;
|
|
@@ -40,7 +40,7 @@ export function expandParallelBlocks(steps) {
|
|
|
40
40
|
// Add depends_on from last step before this block
|
|
41
41
|
depends_on: lastStepId
|
|
42
42
|
? [...(inner.depends_on ?? []), lastStepId]
|
|
43
|
-
: inner.depends_on,
|
|
43
|
+
: (inner.depends_on ?? []),
|
|
44
44
|
// Inherit isolation from parallel block if step doesn't override
|
|
45
45
|
isolation: inner.isolation ?? block.isolation,
|
|
46
46
|
};
|
|
@@ -72,7 +72,7 @@ export function expandParallelBlocks(steps) {
|
|
|
72
72
|
* Returns a new array of steps with injected deps (never mutates input).
|
|
73
73
|
*/
|
|
74
74
|
function inferImplicitDeps(steps) {
|
|
75
|
-
const anyExplicit = steps.some((s) => s.depends_on !== undefined
|
|
75
|
+
const anyExplicit = steps.some((s) => s.depends_on !== undefined);
|
|
76
76
|
if (anyExplicit) {
|
|
77
77
|
return steps;
|
|
78
78
|
}
|
|
@@ -237,6 +237,7 @@ function toWorkflowStep(def) {
|
|
|
237
237
|
completion: def.completion && def.completion.length > 0 ? [...def.completion] : null,
|
|
238
238
|
checkpoints: def.checkpoints && def.checkpoints.length > 0 ? [...def.checkpoints] : null,
|
|
239
239
|
handoff: def.handoff ?? null,
|
|
240
|
+
enforce: def.enforce ?? null,
|
|
240
241
|
};
|
|
241
242
|
if (def.max_attempts !== undefined)
|
|
242
243
|
step.max_attempts = def.max_attempts;
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 4. Call enforcement engine
|
|
13
13
|
* 5. Write JSON result to stdout
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Enforcement failures use the explicit advisory/blocking boundary.
|
|
16
16
|
*/
|
|
17
17
|
import type { ArtifactFact, ConstraintIR, RoleDef, VerifierSpec } from "../schema/types.js";
|
|
18
18
|
import type { HookEvent, HookOutput } from "./protocol.js";
|
|
@@ -122,7 +122,6 @@ export declare function runStopVerifiersForTest(projectDir: string, ir: Constrai
|
|
|
122
122
|
* 2. Context Gate — block Edit/Write/Bash when required context files unread
|
|
123
123
|
*
|
|
124
124
|
* Returns the first triggered block, or null if all gates pass.
|
|
125
|
-
* Fail-open on any exception.
|
|
126
125
|
*/
|
|
127
126
|
export declare function handlePreToolGates(ir: ConstraintIR, rawInput: Record<string, unknown>, wfState: Awaited<ReturnType<typeof readWorkflowState>>, projectDir: string, sessionId?: string): Promise<{
|
|
128
127
|
output: ReturnType<typeof blockOutput>;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 4. Call enforcement engine
|
|
13
13
|
* 5. Write JSON result to stdout
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Enforcement failures use the explicit advisory/blocking boundary.
|
|
16
16
|
*/
|
|
17
17
|
import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
18
18
|
import { realpathSync } from "node:fs";
|
|
@@ -20,10 +20,11 @@ import { spawn } from "node:child_process";
|
|
|
20
20
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
21
21
|
import { createHash, randomUUID } from "node:crypto";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import {
|
|
23
|
+
import { readStdinResult, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
|
|
24
24
|
import { validateHookInput } from "./schema.js";
|
|
25
|
+
import { hookFailureOutput } from "./enforcement-boundary.js";
|
|
25
26
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
26
|
-
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts,
|
|
27
|
+
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, updateSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
|
|
27
28
|
import { hookEventsForSurface } from "./event-registry.js";
|
|
28
29
|
import { writeAuditEvent } from "../audit/index.js";
|
|
29
30
|
import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
|
|
@@ -201,7 +202,9 @@ function buildClaudeEvidenceCaptureEvent(params) {
|
|
|
201
202
|
workflow_asset: attribution.workflowAsset,
|
|
202
203
|
workflow_id: attribution.workflowId,
|
|
203
204
|
run_id: params.runtimeEvent.run_id || params.runtimeEvent.session_id || "unknown",
|
|
204
|
-
step_id: attribution.stepId === "unknown"
|
|
205
|
+
step_id: (attribution.stepId === "unknown"
|
|
206
|
+
&& attribution.workflowId === "unknown"
|
|
207
|
+
&& !params.wfState)
|
|
205
208
|
? `${params.event}:direct`
|
|
206
209
|
: attribution.stepId,
|
|
207
210
|
fact,
|
|
@@ -279,13 +282,17 @@ async function main() {
|
|
|
279
282
|
irPath = args[irFlagIdx + 1];
|
|
280
283
|
}
|
|
281
284
|
// Read stdin
|
|
282
|
-
const
|
|
285
|
+
const stdin = await readStdinResult(5000);
|
|
286
|
+
if (!stdin.ok) {
|
|
287
|
+
writeOutput(hookFailureOutput(event, "malformed_input", `Hook input could not be read (${stdin.error ?? "unknown"}).`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const rawStdin = stdin.data;
|
|
283
291
|
// Validate + normalize input against CC hook protocol schema.
|
|
284
|
-
// Fail-open: invalid input → write stderr warning + silent exit.
|
|
285
292
|
const validation = validateHookInput(event, rawStdin);
|
|
286
293
|
if (!validation.valid) {
|
|
287
294
|
process.stderr.write(`\n Intent DNA: invalid ${event} input — ${validation.errors.join("; ")}\n`);
|
|
288
|
-
writeOutput(
|
|
295
|
+
writeOutput(hookFailureOutput(event, "malformed_input", validation.errors.join("; ")));
|
|
289
296
|
return;
|
|
290
297
|
}
|
|
291
298
|
const rawInput = validation.normalized;
|
|
@@ -296,7 +303,7 @@ async function main() {
|
|
|
296
303
|
const resolvedIRPath = resolve(projectDir, irPath);
|
|
297
304
|
const ir = await loadIR(resolvedIRPath);
|
|
298
305
|
if (!ir) {
|
|
299
|
-
writeOutput(
|
|
306
|
+
writeOutput(hookFailureOutput(event, "missing_ir", `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolvedIRPath}.`));
|
|
300
307
|
return;
|
|
301
308
|
}
|
|
302
309
|
const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
|
|
@@ -354,7 +361,16 @@ export async function runHookEvent(options) {
|
|
|
354
361
|
return finish(workflowState.output);
|
|
355
362
|
}
|
|
356
363
|
wfStateRaw = workflowState.state;
|
|
357
|
-
if (
|
|
364
|
+
if (wfStateRaw?.active
|
|
365
|
+
&& typeof rawInput.agent_type === "string"
|
|
366
|
+
&& !subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
|
|
367
|
+
if (wfStateRaw.worker_session_id
|
|
368
|
+
&& wfStateRaw.run_id
|
|
369
|
+
&& wfStateRaw.attempt_id) {
|
|
370
|
+
const mismatch = hookFailureOutput(event, "role_mismatch", "Worker role does not match the Controller-owned Step role.");
|
|
371
|
+
await writeRuntimeDecision(mismatch, { output: mismatch, trace: { matched_rule: "scope", role: wfStateRaw.current_role } }, wfStateRaw);
|
|
372
|
+
return finish(mismatch);
|
|
373
|
+
}
|
|
358
374
|
wfStateRaw = null;
|
|
359
375
|
}
|
|
360
376
|
if (wfStateRaw && wfStateRaw.active) {
|
|
@@ -365,16 +381,10 @@ export async function runHookEvent(options) {
|
|
|
365
381
|
current_role: wfStateRaw.current_role,
|
|
366
382
|
completed_artifacts: wfStateRaw.completed_artifacts,
|
|
367
383
|
iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
|
|
384
|
+
strict_boundary: Boolean(wfStateRaw.worker_session_id
|
|
385
|
+
&& wfStateRaw.run_id
|
|
386
|
+
&& wfStateRaw.attempt_id),
|
|
368
387
|
};
|
|
369
|
-
if (event !== "SubagentStop") {
|
|
370
|
-
const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
|
|
371
|
-
if ("output" in artifactFacts) {
|
|
372
|
-
appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
|
|
373
|
-
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
|
|
374
|
-
return finish(artifactFacts.output);
|
|
375
|
-
}
|
|
376
|
-
state.artifactFacts = artifactFacts.facts;
|
|
377
|
-
}
|
|
378
388
|
}
|
|
379
389
|
}
|
|
380
390
|
if (event === "PreToolUse" && wfStateRaw?.active) {
|
|
@@ -715,12 +725,14 @@ function subagentMatchesWorkflowRole(rawInput, wfState) {
|
|
|
715
725
|
return typeof rawInput.agent_type === "string" && rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
|
|
716
726
|
}
|
|
717
727
|
async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
|
|
718
|
-
const sessionId = typeof rawInput.
|
|
719
|
-
? rawInput.
|
|
720
|
-
: typeof rawInput.
|
|
721
|
-
? rawInput.
|
|
722
|
-
:
|
|
723
|
-
|
|
728
|
+
const sessionId = typeof rawInput.worker_session_id === "string"
|
|
729
|
+
? rawInput.worker_session_id
|
|
730
|
+
: typeof rawInput.session_id === "string"
|
|
731
|
+
? rawInput.session_id
|
|
732
|
+
: typeof rawInput.sessionId === "string"
|
|
733
|
+
? rawInput.sessionId
|
|
734
|
+
: undefined;
|
|
735
|
+
const workflowState = await readWorkerWorkflowState(projectDir, sessionId, typeof rawInput.worker_session_id !== "string");
|
|
724
736
|
const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
|
|
725
737
|
return runCodexHook({ ...rawInput, event: codexHookEventForInput(rawInput, args) }, {
|
|
726
738
|
ir,
|
|
@@ -735,21 +747,32 @@ async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
|
|
|
735
747
|
});
|
|
736
748
|
}
|
|
737
749
|
async function runCodexHookCli(args) {
|
|
750
|
+
let policyEvent = "PostToolUse";
|
|
738
751
|
try {
|
|
739
|
-
const
|
|
752
|
+
const stdin = await readStdinResult(5000);
|
|
753
|
+
const candidateEvent = codexHookEventForInput(stdin.data, args);
|
|
754
|
+
if (typeof candidateEvent === "string" && VALID_EVENTS.has(candidateEvent)) {
|
|
755
|
+
policyEvent = candidateEvent;
|
|
756
|
+
}
|
|
757
|
+
if (!stdin.ok) {
|
|
758
|
+
writeOutput(hookFailureOutput(policyEvent, "malformed_input", `Codex Hook input could not be read (${stdin.error ?? "unknown"}).`));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const rawInput = stdin.data;
|
|
740
762
|
const projectDir = typeof rawInput.cwd === "string" ? rawInput.cwd : process.cwd();
|
|
741
763
|
const irPath = argValue(args, "--ir") ?? DEFAULT_IR_PATH;
|
|
742
764
|
const ir = await loadIR(resolve(projectDir, irPath));
|
|
743
765
|
if (!ir) {
|
|
744
|
-
writeOutput(
|
|
766
|
+
writeOutput(hookFailureOutput(policyEvent, "missing_ir", `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolve(projectDir, irPath)}.`));
|
|
745
767
|
return;
|
|
746
768
|
}
|
|
747
769
|
const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, ir);
|
|
748
770
|
writeOutput(result.output);
|
|
749
771
|
}
|
|
750
772
|
catch (err) {
|
|
751
|
-
|
|
752
|
-
|
|
773
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
774
|
+
process.stderr.write(`\n Intent DNA: CodexHook enforcement failed — ${detail}\n`);
|
|
775
|
+
writeOutput(hookFailureOutput(policyEvent, "gate_evaluation", detail));
|
|
753
776
|
}
|
|
754
777
|
}
|
|
755
778
|
// ── IR Loading ─────────────────────────────────────────────
|
|
@@ -764,6 +787,7 @@ async function loadIR(irPath) {
|
|
|
764
787
|
if (data.ir_version !== EXPECTED_IR_VERSION) {
|
|
765
788
|
process.stderr.write(`\n Intent DNA: IR version mismatch (v${data.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
|
|
766
789
|
` Fix: run \`dna sync\` to recompile\n\n`);
|
|
790
|
+
return null;
|
|
767
791
|
}
|
|
768
792
|
return data.ir;
|
|
769
793
|
}
|
|
@@ -938,20 +962,25 @@ function artifactResolverErrorOutput(event, error) {
|
|
|
938
962
|
}
|
|
939
963
|
async function readWorkflowStateForHook(projectDir, event, sessionId) {
|
|
940
964
|
try {
|
|
941
|
-
|
|
942
|
-
? await
|
|
943
|
-
:
|
|
965
|
+
let state = sessionId
|
|
966
|
+
? await readWorkflowState(projectDir, sessionId)
|
|
967
|
+
: null;
|
|
968
|
+
if (!state && event === "SubagentStop") {
|
|
969
|
+
state = await readWorkflowState(projectDir);
|
|
970
|
+
}
|
|
944
971
|
return { state };
|
|
945
972
|
}
|
|
946
973
|
catch (error) {
|
|
947
974
|
return { output: artifactResolverErrorOutput(event, error) };
|
|
948
975
|
}
|
|
949
976
|
}
|
|
950
|
-
async function
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
977
|
+
async function readWorkerWorkflowState(projectDir, sessionId, allowRootFallback = false) {
|
|
978
|
+
const workerState = sessionId
|
|
979
|
+
? await readWorkflowState(projectDir, sessionId)
|
|
980
|
+
: null;
|
|
981
|
+
if (workerState || !allowRootFallback)
|
|
982
|
+
return workerState;
|
|
983
|
+
return readWorkflowState(projectDir);
|
|
955
984
|
}
|
|
956
985
|
async function resolveWorkflowArtifactFactsForHook(projectDir, ir, wfState, event, sessionId) {
|
|
957
986
|
try {
|
|
@@ -1302,7 +1331,7 @@ const TEST_PASSED_RE = /(\d+)\s+(?:tests?\s+)?passed/i;
|
|
|
1302
1331
|
*/
|
|
1303
1332
|
async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
|
|
1304
1333
|
const agentType = typeof input.agent_type === "string" ? input.agent_type : undefined;
|
|
1305
|
-
if (!agentType)
|
|
1334
|
+
if (!agentType || !sessionId)
|
|
1306
1335
|
return null;
|
|
1307
1336
|
const toolName = String(input.tool_name ?? "");
|
|
1308
1337
|
const toolOutput = typeof input.tool_output === "string" ? input.tool_output : undefined;
|
|
@@ -1341,12 +1370,14 @@ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
|
|
|
1341
1370
|
blockedItemsPath = reflectionConfig.blocked_items_path;
|
|
1342
1371
|
if (!maxAttempts)
|
|
1343
1372
|
return null;
|
|
1344
|
-
// Read current surgeon state
|
|
1345
|
-
const surgeonState = await readSurgeonAttempts(projectDir, sessionId);
|
|
1346
1373
|
// Track edits
|
|
1347
1374
|
if (toolName === "Edit") {
|
|
1348
|
-
|
|
1349
|
-
|
|
1375
|
+
if (!sessionId)
|
|
1376
|
+
return null;
|
|
1377
|
+
await updateSurgeonAttempts(projectDir, sessionId, (state) => ({
|
|
1378
|
+
...state,
|
|
1379
|
+
edit_count: state.edit_count + 1,
|
|
1380
|
+
}));
|
|
1350
1381
|
return null;
|
|
1351
1382
|
}
|
|
1352
1383
|
// Track test results from Bash
|
|
@@ -1354,43 +1385,44 @@ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
|
|
|
1354
1385
|
const match = toolOutput.match(TEST_PASSED_RE);
|
|
1355
1386
|
if (!match)
|
|
1356
1387
|
return null; // Can't parse — fail-open
|
|
1357
|
-
|
|
1358
|
-
surgeonState.bash_test_count++;
|
|
1359
|
-
surgeonState.current_green_count = greenCount;
|
|
1360
|
-
if (!surgeonState.baseline_established) {
|
|
1361
|
-
surgeonState.baseline_established = true;
|
|
1362
|
-
surgeonState.last_green_count = greenCount;
|
|
1363
|
-
await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
|
|
1388
|
+
if (!sessionId)
|
|
1364
1389
|
return null;
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
surgeonState.
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
if (result.action === "handoff" || result.action === "warn") {
|
|
1375
|
-
const target = handoffTo ? ` Hand off to '${handoffTo}' for re-analysis.` : "";
|
|
1376
|
-
return allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${target}\n` +
|
|
1377
|
-
`Review experience_chain in .dna/state/workflow/surgeon-attempts.json before next attempt.`);
|
|
1390
|
+
const greenCount = parseInt(match[1], 10);
|
|
1391
|
+
let reflectionOutput = null;
|
|
1392
|
+
await updateSurgeonAttempts(projectDir, sessionId, (surgeonState) => {
|
|
1393
|
+
surgeonState.bash_test_count++;
|
|
1394
|
+
surgeonState.current_green_count = greenCount;
|
|
1395
|
+
if (!surgeonState.baseline_established) {
|
|
1396
|
+
surgeonState.baseline_established = true;
|
|
1397
|
+
surgeonState.last_green_count = greenCount;
|
|
1398
|
+
return surgeonState;
|
|
1378
1399
|
}
|
|
1379
|
-
if (
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1400
|
+
if (surgeonState.edit_count >= 2 && greenCount <= surgeonState.last_green_count) {
|
|
1401
|
+
surgeonState.fail_count++;
|
|
1402
|
+
const result = checkReflectionLimit(surgeonState.fail_count, maxAttempts, surgeonState.handoff_count, maxHandoffs);
|
|
1403
|
+
surgeonState.last_green_count = greenCount;
|
|
1404
|
+
surgeonState.edit_count = 0;
|
|
1405
|
+
if (result.action === "handoff" || result.action === "warn") {
|
|
1406
|
+
const target = handoffTo ? ` Hand off to '${handoffTo}' for re-analysis.` : "";
|
|
1407
|
+
reflectionOutput = allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${target}\n` +
|
|
1408
|
+
`Review experience_chain in the worker Hook state before next attempt.`);
|
|
1409
|
+
}
|
|
1410
|
+
else if (result.action === "skip") {
|
|
1411
|
+
const path = blockedItemsPath ? ` Record blocked item to ${blockedItemsPath}.` : "";
|
|
1412
|
+
reflectionOutput = allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${path}\n` +
|
|
1413
|
+
`SKIP this item and move to the next.`);
|
|
1414
|
+
}
|
|
1383
1415
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
surgeonState.fail_count = 0;
|
|
1416
|
+
else {
|
|
1417
|
+
const madeProgress = greenCount > surgeonState.last_green_count;
|
|
1418
|
+
surgeonState.last_green_count = greenCount;
|
|
1419
|
+
surgeonState.edit_count = 0;
|
|
1420
|
+
if (madeProgress)
|
|
1421
|
+
surgeonState.fail_count = 0;
|
|
1391
1422
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1423
|
+
return surgeonState;
|
|
1424
|
+
});
|
|
1425
|
+
return reflectionOutput;
|
|
1394
1426
|
}
|
|
1395
1427
|
return null;
|
|
1396
1428
|
}
|
|
@@ -1418,7 +1450,6 @@ async function loadReflectionConfig(projectDir, _workflowName, _stepId) {
|
|
|
1418
1450
|
* 2. Context Gate — block Edit/Write/Bash when required context files unread
|
|
1419
1451
|
*
|
|
1420
1452
|
* Returns the first triggered block, or null if all gates pass.
|
|
1421
|
-
* Fail-open on any exception.
|
|
1422
1453
|
*/
|
|
1423
1454
|
export async function handlePreToolGates(ir, rawInput, wfState, projectDir, sessionId) {
|
|
1424
1455
|
try {
|
|
@@ -1477,8 +1508,12 @@ export async function handlePreToolGates(ir, rawInput, wfState, projectDir, sess
|
|
|
1477
1508
|
}
|
|
1478
1509
|
}
|
|
1479
1510
|
}
|
|
1480
|
-
catch {
|
|
1481
|
-
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1513
|
+
return {
|
|
1514
|
+
output: blockOutput(`[Intent DNA] gate_evaluation: PreToolUse gates could not be evaluated safely (${detail}).`),
|
|
1515
|
+
matched_rule: "context_gate",
|
|
1516
|
+
};
|
|
1482
1517
|
}
|
|
1483
1518
|
return null;
|
|
1484
1519
|
}
|
|
@@ -1548,8 +1583,12 @@ function isDirectEntryPoint() {
|
|
|
1548
1583
|
}
|
|
1549
1584
|
}
|
|
1550
1585
|
if (isDirectEntryPoint()) {
|
|
1551
|
-
main().catch(() => {
|
|
1552
|
-
|
|
1553
|
-
|
|
1586
|
+
main().catch((error) => {
|
|
1587
|
+
const eventArg = process.argv[2];
|
|
1588
|
+
const event = VALID_EVENTS.has(eventArg)
|
|
1589
|
+
? eventArg
|
|
1590
|
+
: "PostToolUse";
|
|
1591
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1592
|
+
writeOutput(hookFailureOutput(event, "gate_evaluation", detail));
|
|
1554
1593
|
});
|
|
1555
1594
|
}
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface EnforceState {
|
|
|
31
31
|
completed_artifacts?: CompletedArtifactEntry[];
|
|
32
32
|
/** G4: current workflow iteration (for relax_after_iteration rules) */
|
|
33
33
|
iteration?: number;
|
|
34
|
+
/** True only for Controller-owned disposable worker Hook state. */
|
|
35
|
+
strict_boundary?: boolean;
|
|
34
36
|
};
|
|
35
37
|
/** Resolved artifact facts loaded by CLI/state before pure enforcement. */
|
|
36
38
|
artifactFacts?: ArtifactFact[];
|