intentdna 1.3.0 → 1.4.0

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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "intentdna",
3
+ "description": "Declarative policy layer for AI agent governance — compile DNA templates to hooks, agents, and constraints",
4
+ "plugins": [
5
+ {
6
+ "name": "intentdna",
7
+ "description": "DNA template compilation + runtime enforcement",
8
+ "version": "1.2.3",
9
+ "source": "./"
10
+ }
11
+ ]
12
+ }
@@ -1,9 +1,21 @@
1
1
  /**
2
- * dna setup
2
+ * dna setup [--scope user|project] [--yes]
3
3
  *
4
- * Registers intentdna as a Claude Code plugin.
5
- * Auto-detects whether claude CLI is available and prints guidance accordingly.
4
+ * Registers intentdna as a Claude Code plugin:
5
+ * 1. Detect claude CLI availability
6
+ * 2. Resolve package path (npm global root)
7
+ * 3. Confirm with user (unless --yes)
8
+ * 4. Run: claude plugin marketplace add <path>
9
+ * 5. Run: claude plugin install intentdna@intentdna --scope <scope>
10
+ * 6. Verify registration
11
+ *
12
+ * Falls back to bin mode guidance when claude CLI is unavailable.
13
+ *
14
+ * Reference: OMC plugin registration flow (marketplace add → plugin install)
6
15
  */
16
+ export type SetupScope = "user" | "project";
7
17
  export interface SetupOptions {
18
+ scope: SetupScope;
19
+ yes: boolean;
8
20
  }
9
- export declare function runSetup(_opts: SetupOptions): Promise<number>;
21
+ export declare function runSetup(opts: SetupOptions): Promise<number>;
@@ -1,42 +1,192 @@
1
1
  /**
2
- * dna setup
2
+ * dna setup [--scope user|project] [--yes]
3
3
  *
4
- * Registers intentdna as a Claude Code plugin.
5
- * Auto-detects whether claude CLI is available and prints guidance accordingly.
4
+ * Registers intentdna as a Claude Code plugin:
5
+ * 1. Detect claude CLI availability
6
+ * 2. Resolve package path (npm global root)
7
+ * 3. Confirm with user (unless --yes)
8
+ * 4. Run: claude plugin marketplace add <path>
9
+ * 5. Run: claude plugin install intentdna@intentdna --scope <scope>
10
+ * 6. Verify registration
11
+ *
12
+ * Falls back to bin mode guidance when claude CLI is unavailable.
13
+ *
14
+ * Reference: OMC plugin registration flow (marketplace add → plugin install)
6
15
  */
7
16
  import { execSync } from "node:child_process";
8
- export async function runSetup(_opts) {
9
- let claudeAvailable = false;
17
+ import { createInterface } from "node:readline";
18
+ import { resolve, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { stat } from "node:fs/promises";
21
+ // ── Helpers ───────────────────────────────────────────────
22
+ function log(msg) {
23
+ process.stderr.write(msg + "\n");
24
+ }
25
+ async function fileExists(path) {
26
+ try {
27
+ await stat(path);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function confirm(question) {
35
+ return new Promise((res) => {
36
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
37
+ rl.question(question + " [Y/n] ", (answer) => {
38
+ rl.close();
39
+ const a = answer.trim().toLowerCase();
40
+ res(a === "" || a === "y" || a === "yes");
41
+ });
42
+ });
43
+ }
44
+ /**
45
+ * Check if claude CLI is available and return its version string.
46
+ */
47
+ function detectClaudeCLI() {
10
48
  try {
11
- execSync("claude --version", { stdio: "ignore" });
12
- claudeAvailable = true;
49
+ return execSync("claude --version 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim();
13
50
  }
14
51
  catch {
15
- // claude CLI not found — fall through to bin mode
16
- }
17
- if (claudeAvailable) {
18
- process.stderr.write("Claude Code CLI detected.\n");
19
- process.stderr.write("\n");
20
- process.stderr.write("Intent DNA plugin mode:\n");
21
- process.stderr.write(" The .claude-plugin/ directory contains plugin.json and hook scripts.\n");
22
- process.stderr.write(" To register as a plugin, run:\n");
23
- process.stderr.write("\n");
24
- let npmRoot = "";
25
- try {
26
- npmRoot = execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Resolve the intentdna package root directory.
57
+ * Priority: npm global install → local package directory (development).
58
+ */
59
+ async function resolvePackagePath() {
60
+ // 1. Try npm global root
61
+ try {
62
+ const npmRoot = execSync("npm root -g", {
63
+ encoding: "utf-8",
64
+ timeout: 5000,
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ }).trim();
67
+ const globalPath = resolve(npmRoot, "intentdna");
68
+ if (await fileExists(resolve(globalPath, ".claude-plugin", "plugin.json"))) {
69
+ return globalPath;
27
70
  }
28
- catch {
29
- // ignore
71
+ }
72
+ catch { /* not installed globally */ }
73
+ // 2. Try local package (development mode — this file is inside the package)
74
+ const thisDir = dirname(fileURLToPath(import.meta.url));
75
+ // In dist: dist/cli/commands/setup.js → ../../.. = package root
76
+ const localRoot = resolve(thisDir, "..", "..", "..");
77
+ if (await fileExists(resolve(localRoot, ".claude-plugin", "plugin.json"))) {
78
+ return localRoot;
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * Check if intentdna is already registered as a plugin.
84
+ */
85
+ function isPluginRegistered() {
86
+ try {
87
+ const result = execSync("claude plugin list 2>/dev/null", {
88
+ encoding: "utf-8",
89
+ timeout: 5000,
90
+ });
91
+ return result.includes("intentdna");
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ // ── Main ──────────────────────────────────────────────────
98
+ export async function runSetup(opts) {
99
+ const scope = opts.scope;
100
+ // Step 1: Detect claude CLI
101
+ const claudeVersion = detectClaudeCLI();
102
+ if (!claudeVersion) {
103
+ log("Claude Code CLI not detected.");
104
+ log("");
105
+ log("Intent DNA can run in bin mode (standalone dna-hook binary):");
106
+ log(" dna sync Register dna-hook events in settings.json");
107
+ log(" dna sync --plugin Force plugin-style IR generation");
108
+ log("");
109
+ log("To use plugin mode, install Claude Code first, then re-run 'dna setup'.");
110
+ return 0;
111
+ }
112
+ log(`Claude Code detected: ${claudeVersion}`);
113
+ // Step 2: Check if already registered
114
+ if (isPluginRegistered()) {
115
+ log("Intent DNA plugin is already registered.");
116
+ log("Run 'dna sync' to compile and activate your DNA templates.");
117
+ return 0;
118
+ }
119
+ // Step 3: Resolve package path
120
+ const pkgPath = await resolvePackagePath();
121
+ if (!pkgPath) {
122
+ log("Error: Could not find intentdna package with .claude-plugin/ directory.");
123
+ log("Ensure intentdna is installed: npm install -g intentdna");
124
+ return 1;
125
+ }
126
+ log(`Package found: ${pkgPath}`);
127
+ log(`Scope: ${scope}`);
128
+ // Step 4: Confirmation prompt
129
+ if (!opts.yes) {
130
+ log("");
131
+ log("This will:");
132
+ log(` 1. Register '${pkgPath}' as a plugin marketplace`);
133
+ log(` 2. Install intentdna plugin (scope: ${scope})`);
134
+ log("");
135
+ if (process.stdin.isTTY) {
136
+ const ok = await confirm("Proceed?");
137
+ if (!ok) {
138
+ log("Aborted.");
139
+ return 0;
140
+ }
30
141
  }
31
- const pluginPath = npmRoot ? `${npmRoot}/intentdna` : "<npm-root>/intentdna";
32
- process.stderr.write(` claude plugin add ${pluginPath}\n`);
33
- process.stderr.write("\n");
34
- process.stderr.write(" Or run 'dna sync --plugin' to activate hooks via settings.json.\n");
35
- process.stderr.write("Intent DNA plugin registered. Run 'dna sync' to activate.\n");
142
+ }
143
+ // Step 5: Register marketplace
144
+ try {
145
+ log("");
146
+ log(`Running: claude plugin marketplace add ${pkgPath}`);
147
+ execSync(`claude plugin marketplace add "${pkgPath}"`, {
148
+ encoding: "utf-8",
149
+ timeout: 30000,
150
+ stdio: ["inherit", "pipe", "pipe"],
151
+ });
152
+ log("Marketplace registered.");
153
+ }
154
+ catch (err) {
155
+ const msg = err instanceof Error ? err.message : String(err);
156
+ log(`Warning: marketplace add failed: ${msg}`);
157
+ log("You may need to run manually:");
158
+ log(` claude plugin marketplace add "${pkgPath}"`);
159
+ // Continue — install may still work if marketplace was already added
160
+ }
161
+ // Step 6: Install plugin
162
+ try {
163
+ const installCmd = `claude plugin install intentdna@intentdna --scope ${scope}`;
164
+ log(`Running: ${installCmd}`);
165
+ execSync(installCmd, {
166
+ encoding: "utf-8",
167
+ timeout: 30000,
168
+ stdio: ["inherit", "pipe", "pipe"],
169
+ });
170
+ log("Plugin installed.");
171
+ }
172
+ catch (err) {
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ log(`Warning: plugin install failed: ${msg}`);
175
+ log("You may need to run manually:");
176
+ log(` claude plugin install intentdna@intentdna --scope ${scope}`);
177
+ return 1;
178
+ }
179
+ // Step 7: Verify
180
+ if (isPluginRegistered()) {
181
+ log("");
182
+ log("Intent DNA plugin registered successfully.");
183
+ log("Run 'dna sync' to compile and activate your DNA templates.");
184
+ return 0;
36
185
  }
37
186
  else {
38
- process.stderr.write("Claude Code CLI not detected. Using bin mode.\n");
39
- process.stderr.write("Run 'dna sync' to register dna-hook in settings.json\n");
187
+ log("");
188
+ log("Plugin install completed but verification failed.");
189
+ log("Try: claude plugin list");
190
+ return 1;
40
191
  }
41
- return 0;
42
192
  }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  export interface VerifyOptions {
13
13
  lockFile: string;
14
+ stats?: boolean;
14
15
  }
15
16
  export interface LockFileEntry {
16
17
  sha256: string;
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { readFile } from "node:fs/promises";
13
13
  import { createHash } from "node:crypto";
14
+ import { readTraces } from "../../hooks/state.js";
14
15
  // ── Helpers ────────────────────────────────────────────────
15
16
  export function sha256(content) {
16
17
  return createHash("sha256").update(content, "utf-8").digest("hex");
@@ -53,6 +54,10 @@ export async function verifyLock(lock) {
53
54
  }
54
55
  // ── CLI entry ──────────────────────────────────────────────
55
56
  export async function runVerify(opts) {
57
+ // Handle --stats mode
58
+ if (opts.stats) {
59
+ return runStats();
60
+ }
56
61
  // Read lock file
57
62
  let raw;
58
63
  try {
@@ -99,3 +104,67 @@ export async function runVerify(opts) {
99
104
  return 0;
100
105
  }
101
106
  }
107
+ // ── Stats Mode ────────────────────────────────────────────
108
+ async function runStats() {
109
+ const cwd = process.cwd();
110
+ const entries = await readTraces(cwd, 1); // last 24h
111
+ if (entries.length === 0) {
112
+ process.stderr.write("No trace data found (last 24h).\n");
113
+ process.stderr.write("Traces are written to .dna/state/trace/ during hook execution.\n");
114
+ return 0;
115
+ }
116
+ // Group by event
117
+ const byEvent = new Map();
118
+ for (const e of entries) {
119
+ const list = byEvent.get(e.event) ?? [];
120
+ list.push(e);
121
+ byEvent.set(e.event, list);
122
+ }
123
+ process.stderr.write("Hook call statistics (last 24h):\n");
124
+ for (const [event, list] of byEvent) {
125
+ const blocks = list.filter(e => e.decision === "block").length;
126
+ const warns = list.filter(e => e.decision === "warn").length;
127
+ const avgMs = list.reduce((sum, e) => sum + e.duration_ms, 0) / list.length;
128
+ const parts = [`${list.length} calls`];
129
+ if (blocks > 0)
130
+ parts.push(`${blocks} blocks`);
131
+ if (warns > 0)
132
+ parts.push(`${warns} warns`);
133
+ parts.push(`avg ${avgMs.toFixed(1)}ms`);
134
+ process.stderr.write(` ${event.padEnd(18)} ${parts.join(", ")}\n`);
135
+ }
136
+ // Workflow stats
137
+ const withWorkflow = entries.filter(e => e.workflow);
138
+ if (withWorkflow.length > 0) {
139
+ const byWorkflow = new Map();
140
+ for (const e of withWorkflow) {
141
+ const key = e.workflow;
142
+ const list = byWorkflow.get(key) ?? [];
143
+ list.push(e);
144
+ byWorkflow.set(key, list);
145
+ }
146
+ process.stderr.write("\nWorkflow statistics:\n");
147
+ for (const [wf, list] of byWorkflow) {
148
+ const steps = new Set(list.map(e => e.step).filter(Boolean));
149
+ const blocks = list.filter(e => e.decision === "block").length;
150
+ process.stderr.write(` ${wf}: ${list.length} tool calls, ${steps.size} steps, ${blocks} blocks\n`);
151
+ }
152
+ }
153
+ // Block reasons TOP 3
154
+ const blockEntries = entries.filter(e => e.decision === "block");
155
+ if (blockEntries.length > 0) {
156
+ const reasons = new Map();
157
+ for (const e of blockEntries) {
158
+ const key = `${e.event}:${e.tool_name ?? "unknown"}`;
159
+ reasons.set(key, (reasons.get(key) ?? 0) + 1);
160
+ }
161
+ const sorted = [...reasons.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
162
+ process.stderr.write("\nBlock reasons TOP 3:\n");
163
+ let rank = 0;
164
+ for (const [reason, count] of sorted) {
165
+ rank++;
166
+ process.stderr.write(` ${rank}. ${reason} x ${count}\n`);
167
+ }
168
+ }
169
+ return 0;
170
+ }
package/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ const HELP = `Intent DNA CLI v0.3.0
17
17
  Usage: dna <command> [options]
18
18
 
19
19
  Commands:
20
- setup Register intentdna as a Claude Code plugin (auto-detect mode)
20
+ setup Register intentdna as a Claude Code plugin (--scope user|project, --yes)
21
21
  guard Zero-config guardrails — detect environment, apply safety rules
22
22
  sync Compile + inject DNA into target file (CLAUDE.md, SOUL.md, etc.)
23
23
  verify Verify synced files match .dna/lock checksums (drift detection)
@@ -53,6 +53,7 @@ Examples:
53
53
  dna sync --remove --inject CLAUDE.md --hooks .claude/hooks --agents .claude/agents
54
54
  dna verify Verify synced files against .dna/lock
55
55
  dna verify --lock /path/to/.dna/lock
56
+ dna verify --stats Show hook call statistics (last 24h)
56
57
  dna import . Import existing harness configs into DNA format
57
58
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8
58
59
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8 --dry-run
@@ -120,6 +121,7 @@ async function main() {
120
121
  args: rest,
121
122
  options: {
122
123
  lock: { type: "string", default: ".dna/lock" },
124
+ stats: { type: "boolean", default: false },
123
125
  },
124
126
  allowPositionals: true,
125
127
  strict: false,
@@ -127,6 +129,7 @@ async function main() {
127
129
  const { runVerify } = await import("./commands/verify.js");
128
130
  const code = await runVerify({
129
131
  lockFile: verifyValues.lock,
132
+ stats: verifyValues.stats,
130
133
  });
131
134
  process.exit(code);
132
135
  break;
@@ -357,8 +360,24 @@ async function main() {
357
360
  break;
358
361
  }
359
362
  case "setup": {
363
+ const { values: setupValues } = parseArgs({
364
+ args: rest,
365
+ options: {
366
+ scope: { type: "string", short: "s", default: "user" },
367
+ yes: { type: "boolean", short: "y", default: false },
368
+ },
369
+ strict: false,
370
+ });
371
+ const setupScope = setupValues.scope;
372
+ if (setupScope !== "user" && setupScope !== "project") {
373
+ process.stderr.write(`Error: --scope must be 'user' or 'project', got '${setupScope}'\n`);
374
+ process.exit(2);
375
+ }
360
376
  const { runSetup } = await import("./commands/setup.js");
361
- const code = await runSetup({});
377
+ const code = await runSetup({
378
+ scope: setupScope,
379
+ yes: setupValues.yes,
380
+ });
362
381
  process.exit(code);
363
382
  break;
364
383
  }
@@ -218,6 +218,7 @@ export function compileDNA(activated, cascaded) {
218
218
  for (const [wfKey, wf] of Object.entries(cascaded.workflows)) {
219
219
  const wfStepCheckpoints = [];
220
220
  const activeRoles = [];
221
+ const handoffChain = [];
221
222
  for (const step of wf.steps ?? []) {
222
223
  if (!activeRoles.includes(step.role)) {
223
224
  activeRoles.push(step.role);
@@ -229,6 +230,14 @@ export function compileDNA(activated, cascaded) {
229
230
  checkpoints: [...step.checkpoints],
230
231
  });
231
232
  }
233
+ // Collect handoff chain entries
234
+ if (step.handoff) {
235
+ handoffChain.push({
236
+ step_id: step.id,
237
+ produces: step.handoff.produces,
238
+ consumes: step.handoff.consumes,
239
+ });
240
+ }
232
241
  }
233
242
  // Derive namespace from workflow key: "ns_wfname" → "ns", "wfname" → ""
234
243
  const underscoreIdx = wfKey.indexOf("_");
@@ -238,6 +247,7 @@ export function compileDNA(activated, cascaded) {
238
247
  namespace,
239
248
  step_checkpoints: wfStepCheckpoints,
240
249
  active_roles: activeRoles,
250
+ handoff_chain: handoffChain,
241
251
  });
242
252
  }
243
253
  }
@@ -173,6 +173,7 @@ function toWorkflowStep(def) {
173
173
  prompt: def.prompt ?? null,
174
174
  completion: def.completion && def.completion.length > 0 ? [...def.completion] : null,
175
175
  checkpoints: def.checkpoints && def.checkpoints.length > 0 ? [...def.checkpoints] : null,
176
+ handoff: def.handoff ?? null,
176
177
  };
177
178
  }
178
179
  /**
package/dist/hooks/cli.js CHANGED
@@ -16,9 +16,10 @@
16
16
  */
17
17
  import { readFile } from "node:fs/promises";
18
18
  import { resolve } from "node:path";
19
+ import { randomUUID } from "node:crypto";
19
20
  import { readStdin, writeOutput, silentOutput } from "./protocol.js";
20
21
  import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, } from "./enforce.js";
21
- import { appendAudit, readWorkflowState } from "./state.js";
22
+ import { appendAudit, readWorkflowState, appendTrace, rotateTraces } from "./state.js";
22
23
  // ── Constants ──────────────────────────────────────────────
23
24
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
24
25
  const VALID_EVENTS = new Set([
@@ -52,6 +53,17 @@ async function main() {
52
53
  return;
53
54
  }
54
55
  const state = {};
56
+ // Load workflow state for events that need handoff context
57
+ if (event === "PreToolUse") {
58
+ const wfState = await readWorkflowState(projectDir, sessionId);
59
+ if (wfState && wfState.active) {
60
+ state.workflowState = {
61
+ current_step: wfState.current_step,
62
+ workflow: wfState.workflow,
63
+ completed_artifacts: wfState.completed_artifacts,
64
+ };
65
+ }
66
+ }
55
67
  // Special handling for Stop — needs async workflow state read
56
68
  if (event === "Stop") {
57
69
  const wfState = await readWorkflowState(projectDir, sessionId);
@@ -61,6 +73,7 @@ async function main() {
61
73
  current_step: wfState.current_step,
62
74
  current_role: wfState.current_role,
63
75
  started_at: wfState.started_at,
76
+ completed_artifacts: wfState.completed_artifacts,
64
77
  } : null;
65
78
  const stopOutput = enforceStop(ir, {
66
79
  cwd: typeof rawInput.cwd === "string" ? rawInput.cwd : undefined,
@@ -68,11 +81,42 @@ async function main() {
68
81
  stop_reason: typeof rawInput.stop_reason === "string" ? rawInput.stop_reason : undefined,
69
82
  }, stopContext);
70
83
  writeOutput(stopOutput);
84
+ // Trace for Stop
85
+ appendTrace(projectDir, {
86
+ trace_id: randomUUID(),
87
+ event: "Stop",
88
+ workflow: wfState?.workflow,
89
+ step: wfState?.current_step,
90
+ decision: stopOutput.continue === false ? "block" : "allow",
91
+ duration_ms: Date.now() - Date.now(), // minimal
92
+ timestamp: new Date().toISOString(),
93
+ }).catch(() => { });
71
94
  return;
72
95
  }
73
- // Dispatch to enforcement engine
96
+ // Dispatch to enforcement engine with timing
97
+ const start = Date.now();
74
98
  const output = dispatch(event, ir, rawInput, state);
99
+ const durationMs = Date.now() - start;
75
100
  writeOutput(output);
101
+ // Trace logging (async, fail-open)
102
+ const decision = output.continue === false ? "block"
103
+ : output.hookSpecificOutput?.additionalContext?.startsWith("WARN") ? "warn"
104
+ : "allow";
105
+ appendTrace(projectDir, {
106
+ trace_id: randomUUID(),
107
+ event,
108
+ tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
109
+ agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
110
+ workflow: state.workflowState?.workflow,
111
+ step: state.workflowState?.current_step,
112
+ decision: decision,
113
+ duration_ms: durationMs,
114
+ timestamp: new Date().toISOString(),
115
+ }).catch(() => { }); // Fail-open
116
+ // Side effect: rotate traces on SessionStart
117
+ if (event === "SessionStart") {
118
+ rotateTraces(projectDir).catch(() => { });
119
+ }
76
120
  // Side effect: audit log for notifications
77
121
  if (event === "Notification" && output.hookSpecificOutput?.additionalContext?.includes("violation")) {
78
122
  const message = typeof rawInput.message === "string" ? rawInput.message : "";
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * Step checkpoints are handled by the CLI, not here.
15
15
  */
16
- import type { ConstraintIR, RoleDef } from "../schema/types.js";
16
+ import type { ConstraintIR, RoleDef, CompletedArtifactEntry } from "../schema/types.js";
17
17
  import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, HookOutput } from "./protocol.js";
18
18
  /** SessionStart input fields */
19
19
  export interface SessionStartInput {
@@ -22,6 +22,12 @@ export interface SessionStartInput {
22
22
  trigger?: string;
23
23
  }
24
24
  export interface EnforceState {
25
+ /** Workflow handoff context — passed from CLI after reading workflow state */
26
+ workflowState?: {
27
+ current_step: string;
28
+ workflow: string;
29
+ completed_artifacts?: CompletedArtifactEntry[];
30
+ };
25
31
  }
26
32
  /**
27
33
  * Enforce PreToolUse constraints.
@@ -71,6 +77,7 @@ export interface StopWorkflowContext {
71
77
  current_step: string;
72
78
  current_role: string;
73
79
  started_at: string;
80
+ completed_artifacts?: CompletedArtifactEntry[];
74
81
  }
75
82
  /**
76
83
  * Enforce Stop hook — verify workflow checkpoint completion.
@@ -85,6 +92,16 @@ export interface StopWorkflowContext {
85
92
  * - Workflow active + unmet checkpoints for current step
86
93
  */
87
94
  export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null): HookOutput;
95
+ /**
96
+ * Enforce handoff produces — verify that the current step has produced
97
+ * its declared artifacts. Called at stop/checkpoint time.
98
+ * Returns block output if a required artifact is missing, null if all produced.
99
+ */
100
+ export declare function enforceHandoffProduces(ir: ConstraintIR, wfState: {
101
+ current_step: string;
102
+ workflow: string;
103
+ completed_artifacts?: CompletedArtifactEntry[];
104
+ }): HookOutput | null;
88
105
  /** Check if a file path is allowed by a list of write globs (prefix matching). */
89
106
  export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
90
107
  /**
@@ -46,6 +46,12 @@ export function enforcePreToolUse(ir, input, state, roles) {
46
46
  if (result)
47
47
  return result;
48
48
  }
49
+ // Layer 6: Handoff — check consumed artifacts are available
50
+ if (state?.workflowState && ir.workflows_ir) {
51
+ const result = enforceHandoffConsumes(ir, state.workflowState);
52
+ if (result)
53
+ return result;
54
+ }
49
55
  return silentOutput();
50
56
  }
51
57
  // ── PostToolUse Enforcement ────────────────────────────────
@@ -237,6 +243,16 @@ export function enforceStop(ir, input, workflowState) {
237
243
  // Build list of blocking checkpoints
238
244
  const blocking = workflowCheckpoints.flatMap(cp => cp.checkpoints.filter(c => (c.action ?? "block") === "block"));
239
245
  if (blocking.length === 0) {
246
+ // No checkpoint blocking — check handoff produces
247
+ if (workflowState.workflow) {
248
+ const handoffResult = enforceHandoffProduces(ir, {
249
+ current_step: workflowState.current_step,
250
+ workflow: workflowState.workflow,
251
+ completed_artifacts: workflowState.completed_artifacts,
252
+ });
253
+ if (handoffResult && !handoffResult.continue)
254
+ return handoffResult;
255
+ }
240
256
  return silentOutput();
241
257
  }
242
258
  // Block: workflow active with unmet checkpoints
@@ -245,6 +261,51 @@ export function enforceStop(ir, input, workflowState) {
245
261
  messages.map(m => ` - ${m}`).join("\n"));
246
262
  }
247
263
  // ── Internal: Layer Enforcement ────────────────────────────
264
+ /**
265
+ * Enforce handoff consumes — verify that the current step's consumed
266
+ * artifacts have been produced by a preceding step.
267
+ * Returns block output if a required artifact is missing, null if all satisfied.
268
+ */
269
+ function enforceHandoffConsumes(ir, wfState) {
270
+ const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
271
+ if (!activeWf || activeWf.handoff_chain.length === 0)
272
+ return null;
273
+ const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
274
+ if (!currentEntry?.consumes || currentEntry.consumes.length === 0)
275
+ return null;
276
+ const completedStepIds = new Set((wfState.completed_artifacts ?? []).map(a => a.step_id));
277
+ // For each consumed artifact, find the producing step and check it completed
278
+ for (const consumed of currentEntry.consumes) {
279
+ // Find which step produces this artifact
280
+ const producer = activeWf.handoff_chain.find(h => h.produces?.some(p => p.type === consumed.type && ((p.path && consumed.path && p.path === consumed.path) ||
281
+ (consumed.from && h.step_id === consumed.from))));
282
+ if (producer && !completedStepIds.has(producer.step_id)) {
283
+ return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires artifact from step '${producer.step_id}' (${consumed.description}). Run step '${producer.step_id}' first.`);
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ /**
289
+ * Enforce handoff produces — verify that the current step has produced
290
+ * its declared artifacts. Called at stop/checkpoint time.
291
+ * Returns block output if a required artifact is missing, null if all produced.
292
+ */
293
+ export function enforceHandoffProduces(ir, wfState) {
294
+ const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
295
+ if (!activeWf || activeWf.handoff_chain.length === 0)
296
+ return null;
297
+ const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
298
+ if (!currentEntry?.produces || currentEntry.produces.length === 0)
299
+ return null;
300
+ const currentArtifacts = (wfState.completed_artifacts ?? []).find(a => a.step_id === wfState.current_step);
301
+ // Check that each produced artifact with a path has been recorded
302
+ for (const produced of currentEntry.produces) {
303
+ if (produced.path && (!currentArtifacts || !currentArtifacts.artifacts.some(a => a.path === produced.path))) {
304
+ return blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}' (path: ${produced.path}) before proceeding.`);
305
+ }
306
+ }
307
+ return null;
308
+ }
248
309
  function enforceRoleScope(rolesScopeMap, input) {
249
310
  if (!input.agent_type)
250
311
  return null;
@@ -9,6 +9,7 @@
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 type { CompletedArtifactEntry } from "../schema/types.js";
12
13
  /** Workflow state written by runtime and read by hooks */
13
14
  export interface DNAWorkflowState {
14
15
  active: boolean;
@@ -18,6 +19,7 @@ export interface DNAWorkflowState {
18
19
  iteration: number;
19
20
  session_id: string;
20
21
  started_at: string;
22
+ completed_artifacts?: CompletedArtifactEntry[];
21
23
  }
22
24
  /** Audit log entry (one per line in JSON Lines format) */
23
25
  export interface AuditEntry {
@@ -47,7 +49,44 @@ export declare function writeWorkflowState(projectDir: string, state: DNAWorkflo
47
49
  */
48
50
  export declare function clearWorkflowState(projectDir: string, sessionId?: string): Promise<void>;
49
51
  /**
50
- * Append an entry to the audit log.
52
+ * Append an entry to the audit log with dedup protection.
53
+ * Dedup key: event + tool_name + timestamp (second-level).
51
54
  * Log file: `.dna/audit/violations-YYYY-MM-DD.log` (JSON Lines format)
52
55
  */
53
56
  export declare function appendAudit(projectDir: string, entry: AuditEntry): Promise<void>;
57
+ /**
58
+ * Record a completed artifact for a workflow step.
59
+ * Reads current workflow state, appends the artifact, writes back atomically.
60
+ */
61
+ export declare function appendCompletedArtifact(projectDir: string, stepId: string, artifact: {
62
+ type: string;
63
+ path: string;
64
+ }, sessionId?: string): Promise<void>;
65
+ /** Trace entry for hook call observability */
66
+ export interface TraceEntry {
67
+ trace_id: string;
68
+ event: string;
69
+ tool_name?: string;
70
+ agent_type?: string;
71
+ workflow?: string;
72
+ step?: string;
73
+ decision: "allow" | "block" | "warn";
74
+ duration_ms: number;
75
+ timestamp: string;
76
+ }
77
+ /**
78
+ * Append a trace entry to the daily trace file.
79
+ * File: `.dna/state/trace/trace-YYYY-MM-DD.jsonl`
80
+ * Fail-open: never throws.
81
+ */
82
+ export declare function appendTrace(projectDir: string, entry: TraceEntry): Promise<void>;
83
+ /**
84
+ * Read trace entries from the last N days.
85
+ * Returns parsed entries sorted by timestamp.
86
+ */
87
+ export declare function readTraces(projectDir: string, days?: number): Promise<TraceEntry[]>;
88
+ /**
89
+ * Clean up trace files older than retention period.
90
+ * Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
91
+ */
92
+ export declare function rotateTraces(projectDir: string): Promise<number>;
@@ -9,7 +9,7 @@
9
9
  * All read operations are fail-safe (return null on error, never throw).
10
10
  * All write operations use atomic temp+rename pattern.
11
11
  */
12
- import { readFile, writeFile, rename, mkdir, appendFile, unlink } from "node:fs/promises";
12
+ import { readFile, writeFile, rename, mkdir, appendFile, unlink, readdir, stat } from "node:fs/promises";
13
13
  import { join, dirname } from "node:path";
14
14
  // ── Constants ──────────────────────────────────────────────
15
15
  const WORKFLOW_FILE = "workflow.json";
@@ -73,7 +73,8 @@ export async function clearWorkflowState(projectDir, sessionId) {
73
73
  }
74
74
  // ── Audit Log ──────────────────────────────────────────────
75
75
  /**
76
- * Append an entry to the audit log.
76
+ * Append an entry to the audit log with dedup protection.
77
+ * Dedup key: event + tool_name + timestamp (second-level).
77
78
  * Log file: `.dna/audit/violations-YYYY-MM-DD.log` (JSON Lines format)
78
79
  */
79
80
  export async function appendAudit(projectDir, entry) {
@@ -81,8 +82,52 @@ export async function appendAudit(projectDir, entry) {
81
82
  await mkdir(auditDir, { recursive: true });
82
83
  const date = entry.timestamp.slice(0, 10); // YYYY-MM-DD
83
84
  const logPath = join(auditDir, `violations-${date}.log`);
85
+ // Dedup: check if identical entry (same event+tool+second) already exists
86
+ const dedupKey = `${entry.event}|${entry.tool_name ?? ""}|${entry.timestamp.slice(0, 19)}`;
87
+ try {
88
+ const existing = await readFile(logPath, "utf-8");
89
+ const lines = existing.trimEnd().split("\n");
90
+ // Check last 10 lines for dedup (avoid scanning entire file)
91
+ const recentLines = lines.slice(-10);
92
+ for (const line of recentLines) {
93
+ try {
94
+ const prev = JSON.parse(line);
95
+ const prevKey = `${prev.event}|${prev.tool_name ?? ""}|${prev.timestamp.slice(0, 19)}`;
96
+ if (prevKey === dedupKey)
97
+ return; // Already logged
98
+ }
99
+ catch { /* skip malformed lines */ }
100
+ }
101
+ }
102
+ catch { /* file doesn't exist yet */ }
84
103
  await appendFile(logPath, JSON.stringify(entry) + "\n", "utf-8");
85
104
  }
105
+ // ── Completed Artifacts ───────────────────────────────────
106
+ /**
107
+ * Record a completed artifact for a workflow step.
108
+ * Reads current workflow state, appends the artifact, writes back atomically.
109
+ */
110
+ export async function appendCompletedArtifact(projectDir, stepId, artifact, sessionId) {
111
+ const state = await readWorkflowState(projectDir, sessionId, 0);
112
+ if (!state)
113
+ return;
114
+ const artifacts = state.completed_artifacts ?? [];
115
+ let stepEntry = artifacts.find(a => a.step_id === stepId);
116
+ if (!stepEntry) {
117
+ stepEntry = { step_id: stepId, artifacts: [] };
118
+ artifacts.push(stepEntry);
119
+ }
120
+ // Dedup: don't add if same path already recorded
121
+ if (stepEntry.artifacts.some(a => a.path === artifact.path))
122
+ return;
123
+ stepEntry.artifacts.push({
124
+ type: artifact.type,
125
+ path: artifact.path,
126
+ verified_at: new Date().toISOString(),
127
+ });
128
+ state.completed_artifacts = artifacts;
129
+ await writeWorkflowState(projectDir, state, sessionId);
130
+ }
86
131
  // ── Internal ───────────────────────────────────────────────
87
132
  /** Atomic write: write to temp file then rename. */
88
133
  async function atomicWrite(filePath, data) {
@@ -91,3 +136,93 @@ async function atomicWrite(filePath, data) {
91
136
  await writeFile(tmpPath, data, "utf-8");
92
137
  await rename(tmpPath, filePath);
93
138
  }
139
+ const TRACE_DIR = "trace";
140
+ const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
141
+ const TRACE_RETENTION_DAYS = 7;
142
+ /**
143
+ * Append a trace entry to the daily trace file.
144
+ * File: `.dna/state/trace/trace-YYYY-MM-DD.jsonl`
145
+ * Fail-open: never throws.
146
+ */
147
+ export async function appendTrace(projectDir, entry) {
148
+ try {
149
+ const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
150
+ await mkdir(traceDir, { recursive: true });
151
+ const date = entry.timestamp.slice(0, 10);
152
+ const tracePath = join(traceDir, `trace-${date}.jsonl`);
153
+ // Check file size — rotate if over limit
154
+ try {
155
+ const stats = await stat(tracePath);
156
+ if (stats.size >= MAX_TRACE_SIZE)
157
+ return; // silently skip if file too large
158
+ }
159
+ catch { /* file doesn't exist yet */ }
160
+ await appendFile(tracePath, JSON.stringify(entry) + "\n", "utf-8");
161
+ }
162
+ catch {
163
+ // Fail-open: trace write failure never affects hook execution
164
+ }
165
+ }
166
+ /**
167
+ * Read trace entries from the last N days.
168
+ * Returns parsed entries sorted by timestamp.
169
+ */
170
+ export async function readTraces(projectDir, days = 1) {
171
+ const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
172
+ const entries = [];
173
+ const cutoff = new Date();
174
+ cutoff.setDate(cutoff.getDate() - days);
175
+ const cutoffDate = cutoff.toISOString().slice(0, 10);
176
+ try {
177
+ const files = await readdir(traceDir);
178
+ const traceFiles = files
179
+ .filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"))
180
+ .filter(f => {
181
+ const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD.jsonl" → "YYYY-MM-DD"
182
+ return fileDate >= cutoffDate;
183
+ })
184
+ .sort();
185
+ for (const file of traceFiles) {
186
+ const content = await readFile(join(traceDir, file), "utf-8");
187
+ for (const line of content.trim().split("\n")) {
188
+ if (!line)
189
+ continue;
190
+ try {
191
+ entries.push(JSON.parse(line));
192
+ }
193
+ catch { /* skip malformed */ }
194
+ }
195
+ }
196
+ }
197
+ catch {
198
+ // No trace dir yet
199
+ }
200
+ return entries;
201
+ }
202
+ /**
203
+ * Clean up trace files older than retention period.
204
+ * Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
205
+ */
206
+ export async function rotateTraces(projectDir) {
207
+ const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
208
+ let removed = 0;
209
+ const cutoff = new Date();
210
+ cutoff.setDate(cutoff.getDate() - TRACE_RETENTION_DAYS);
211
+ const cutoffDate = cutoff.toISOString().slice(0, 10);
212
+ try {
213
+ const files = await readdir(traceDir);
214
+ for (const file of files) {
215
+ if (!file.startsWith("trace-") || !file.endsWith(".jsonl"))
216
+ continue;
217
+ const fileDate = file.slice(6, 16);
218
+ if (fileDate < cutoffDate) {
219
+ await unlink(join(traceDir, file)).catch(() => { });
220
+ removed++;
221
+ }
222
+ }
223
+ }
224
+ catch {
225
+ // No trace dir
226
+ }
227
+ return removed;
228
+ }
@@ -124,6 +124,27 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
124
124
  lines.push("</Checkpoints>");
125
125
  lines.push("");
126
126
  }
127
+ // Handoff — artifact flow between steps
128
+ const stepsWithHandoff = plan.steps.filter((s) => s.handoff && (s.handoff.consumes?.length || s.handoff.produces?.length));
129
+ if (stepsWithHandoff.length > 0) {
130
+ lines.push("<Handoff>");
131
+ for (const step of stepsWithHandoff) {
132
+ const h = step.handoff;
133
+ if (h.produces && h.produces.length > 0) {
134
+ for (const p of h.produces) {
135
+ lines.push(` Step ${step.id} produces: ${p.description} (${p.type}${p.path ? `, ${p.path}` : ""})`);
136
+ }
137
+ }
138
+ if (h.consumes && h.consumes.length > 0) {
139
+ for (const c of h.consumes) {
140
+ const fromNote = c.from ? ` from ${c.from}` : "";
141
+ lines.push(` Step ${step.id} consumes: ${c.description}${fromNote} (${c.type}${c.path ? `, ${c.path}` : ""})`);
142
+ }
143
+ }
144
+ }
145
+ lines.push("</Handoff>");
146
+ lines.push("");
147
+ }
127
148
  // Variables — collect {{var_name}} references from the whole plan
128
149
  const varRefs = collectVariableRefs(plan);
129
150
  if (varRefs.size > 0) {
@@ -82,6 +82,20 @@ export interface RoleDef {
82
82
  success_criteria?: string[];
83
83
  failure_modes?: string[];
84
84
  }
85
+ /** Handoff artifact type — what kind of artifact is passed between steps */
86
+ export type HandoffType = "file" | "directory" | "test_result" | "git_commit" | "summary" | "state";
87
+ /** A single artifact consumed or produced by a workflow step */
88
+ export interface HandoffArtifact {
89
+ type: HandoffType;
90
+ path?: string;
91
+ from?: string;
92
+ description: string;
93
+ }
94
+ /** Handoff declaration for a workflow step */
95
+ export interface StepHandoff {
96
+ consumes?: HandoffArtifact[];
97
+ produces?: HandoffArtifact[];
98
+ }
85
99
  /** Transition condition between workflow steps */
86
100
  export interface TransitionDef {
87
101
  from: string;
@@ -124,6 +138,7 @@ export interface WorkflowStepDef {
124
138
  prompt?: string;
125
139
  completion?: CompletionCheck[];
126
140
  checkpoints?: StepCheckpoint[];
141
+ handoff?: StepHandoff;
127
142
  }
128
143
  /** Top-level workflow definition */
129
144
  export interface WorkflowDef {
@@ -133,6 +148,8 @@ export interface WorkflowDef {
133
148
  transitions?: TransitionDef[];
134
149
  retry_policy?: RetryPolicy;
135
150
  max_rounds?: number;
151
+ produces?: HandoffArtifact[];
152
+ consumes?: HandoffArtifact[];
136
153
  }
137
154
  export interface EpigeneticEffect {
138
155
  gene?: string;
@@ -236,12 +253,28 @@ export interface StepCheckpointIR {
236
253
  step_id: string;
237
254
  checkpoints: StepCheckpoint[];
238
255
  }
256
+ /** Handoff chain entry — tracks what each step consumes/produces */
257
+ export interface HandoffChainEntry {
258
+ step_id: string;
259
+ produces?: HandoffArtifact[];
260
+ consumes?: HandoffArtifact[];
261
+ }
239
262
  /** Workflow-partitioned IR — isolates constraints per workflow */
240
263
  export interface WorkflowIR {
241
264
  workflow_name: string;
242
265
  namespace: string;
243
266
  step_checkpoints: StepCheckpointIR[];
244
267
  active_roles: string[];
268
+ handoff_chain: HandoffChainEntry[];
269
+ }
270
+ /** Record of artifacts completed by a step — used for cross-session resume */
271
+ export interface CompletedArtifactEntry {
272
+ step_id: string;
273
+ artifacts: {
274
+ type: string;
275
+ path: string;
276
+ verified_at: string;
277
+ }[];
245
278
  }
246
279
  /** Runtime workflow state written to .dna/state/workflow.json */
247
280
  export interface WorkflowState {
@@ -252,6 +285,7 @@ export interface WorkflowState {
252
285
  iteration: number;
253
286
  session_id: string;
254
287
  started_at: string;
288
+ completed_artifacts?: CompletedArtifactEntry[];
255
289
  }
256
290
  export interface ConstraintIR {
257
291
  prompt_directives: PromptDirective[];
@@ -280,6 +314,7 @@ export interface WorkflowStep {
280
314
  prompt: string | null;
281
315
  completion: CompletionCheck[] | null;
282
316
  checkpoints: StepCheckpoint[] | null;
317
+ handoff: StepHandoff | null;
283
318
  }
284
319
  /** A group of steps that can execute in parallel */
285
320
  export interface ParallelGroup {
@@ -80,10 +80,22 @@ workflow:
80
80
  role: implementer
81
81
  description: Implement the feature
82
82
  prompt: "Implement task {{task_id}} according to the design document."
83
+ handoff:
84
+ produces:
85
+ - type: git_commit
86
+ description: "Implementation commit"
83
87
  - id: review
84
88
  role: reviewer
85
89
  description: Review the implementation
86
90
  prompt: "Review all changes for task {{task_id}}. Output PASS or FAIL."
91
+ handoff:
92
+ consumes:
93
+ - type: git_commit
94
+ from: implement
95
+ description: "Implementation to review"
96
+ produces:
97
+ - type: summary
98
+ description: "Review verdict (PASS/FAIL)"
87
99
  transitions:
88
100
  - from: review
89
101
  to: implement
@@ -130,13 +130,33 @@ workflows:
130
130
  role: investigator
131
131
  description: Trace the broken chain in v1 and v2, identify first breakpoint
132
132
  prompt: "Trace the call chain for '{{feature}}' in v1 ({{v1_path}}) and v2 ({{v2_path}}). Find where v2 breaks."
133
+ handoff:
134
+ produces:
135
+ - type: summary
136
+ description: "Breakpoint analysis with file paths and line numbers"
133
137
  - id: fix
134
138
  role: surgeon
135
139
  depends_on: [trace]
136
140
  description: Fix the identified breakpoint
137
141
  prompt: "Fix the breakpoint identified by investigator. Copy logic from v1, change one file only."
142
+ handoff:
143
+ consumes:
144
+ - type: summary
145
+ from: trace
146
+ description: "Breakpoint analysis from investigator"
147
+ produces:
148
+ - type: git_commit
149
+ description: "Single-file fix commit"
138
150
  - id: verify
139
151
  role: investigator
140
152
  depends_on: [fix]
141
153
  description: Verify the fix with adb
142
154
  prompt: "Run the feature on device. adb screencap + logcat. Compare with v1 behavior."
155
+ handoff:
156
+ consumes:
157
+ - type: git_commit
158
+ from: fix
159
+ description: "Fix commit to verify"
160
+ produces:
161
+ - type: summary
162
+ description: "Verification result with screenshots"
@@ -190,11 +190,27 @@ workflows:
190
190
  role: scanner
191
191
  description: "Scan v1 module in {{v1_path}}/. Output behavior doc to {{behavior_docs}}/$ARGUMENTS.md. List all user actions with call chains."
192
192
  prompt: "Scan module '$ARGUMENTS' in {{v1_path}}/. For each page, list: action → function() → return value. Output to {{behavior_docs}}/$ARGUMENTS.md. Then immediately proceed to write_tests."
193
+ handoff:
194
+ produces:
195
+ - type: file
196
+ path: "{{behavior_docs}}/$ARGUMENTS.md"
197
+ description: "Behavior document for module"
193
198
  - id: write_tests
194
199
  role: test_writer
195
200
  depends_on: [scan]
196
201
  description: "Read {{behavior_docs}}/$ARGUMENTS.md, write tests in {{test_path}}/$ARGUMENTS/, run baseline, commit."
197
202
  prompt: "Read {{behavior_docs}}/$ARGUMENTS.md. Write tests in {{test_path}}/$ARGUMENTS/. Test ALL layers: logic, widget, navigation. Run tests, record red/green baseline. Append baseline to behavior doc. Git commit: behavior-lock($ARGUMENTS): X tests (Y red, Z skipped)"
203
+ handoff:
204
+ consumes:
205
+ - type: file
206
+ path: "{{behavior_docs}}/$ARGUMENTS.md"
207
+ description: "Behavior document from scan step"
208
+ produces:
209
+ - type: directory
210
+ path: "{{test_path}}/$ARGUMENTS/"
211
+ description: "Test files for module"
212
+ - type: git_commit
213
+ description: "Behavior lock commit"
198
214
 
199
215
  rescue:
200
216
  name: Rescue
@@ -204,6 +220,13 @@ workflows:
204
220
  role: investigator
205
221
  description: "Run tests, assess current state, pick next targets."
206
222
  prompt: "Run tests in {{test_path}}/$ARGUMENTS/. Categorize all non-passing tests: 1) RED (failing) — highest priority, fix first. 2) SKIPPED-logic — state/notifier/service tests, fix second. 3) SKIPPED-widget — widget/UI/navigation tests, fix after logic is done. For the highest priority category, pick a batch of related tests. Trace: what does v1 do vs what does v2 do? Find the breakpoints. Report findings and the plan for this round."
223
+ handoff:
224
+ produces:
225
+ - type: summary
226
+ description: "Investigation findings and breakpoint analysis"
227
+ - type: test_result
228
+ path: "{{test_path}}/$ARGUMENTS/"
229
+ description: "Current test state assessment"
207
230
  - id: fix
208
231
  role: surgeon
209
232
  depends_on: [investigate]
@@ -212,11 +235,27 @@ workflows:
212
235
  - assert: clean_working_tree
213
236
  message: "Commit all changes before proceeding to report step"
214
237
  prompt: "Fix the identified breakpoints. Read v1 in {{v1_path}}/. Rewrite in v2 style in {{v2_path}}/. Logic tests: implement notifier/state/service code. Widget tests: copy widget from v1, change bindings (Obx→Consumer, Get.to→context.go), set up widget test infra (ProviderScope, mock providers, GoRouter) if needed. Run tests after each fix. Red→green or Skipped→green = done. Still failing = revert and re-analyze. Maximize test coverage per round. When done, commit all changes: rescue($ARGUMENTS): round N — X passed (+Y)"
238
+ handoff:
239
+ consumes:
240
+ - type: summary
241
+ from: investigate
242
+ description: "Investigation findings from investigate step"
243
+ produces:
244
+ - type: git_commit
245
+ description: "Rescue round commit"
215
246
  - id: report
216
247
  role: investigator
217
248
  depends_on: [fix]
218
249
  description: "Summarize round, guide next steps."
219
250
  prompt: "Run full test suite in {{test_path}}/$ARGUMENTS/. Report: 1) passed/skipped/failed delta vs last round. 2) List remaining skipped tests by category (logic vs widget vs platform). 3) If skipped tests remain, end with: Run /rescue $ARGUMENTS to continue. 4) If all tests pass, end with: Module $ARGUMENTS rescue complete."
251
+ handoff:
252
+ consumes:
253
+ - type: git_commit
254
+ from: fix
255
+ description: "Committed fix from surgeon"
256
+ produces:
257
+ - type: summary
258
+ description: "Round summary with test delta"
220
259
 
221
260
  core-align:
222
261
  name: Core Align
@@ -110,18 +110,47 @@ workflow:
110
110
  description: Read task and produce design document
111
111
  run_if: "round == 1"
112
112
  prompt: "Read task {{task_id}} from BOARD.md. Produce a design document."
113
+ handoff:
114
+ produces:
115
+ - type: file
116
+ path: "docs/tasks/{{task_id}}.md"
117
+ description: "Design document for task"
113
118
  - id: implement
114
119
  role: implementer
115
120
  description: Implement according to design
116
121
  prompt: "Implement task {{task_id}} following the design document."
122
+ handoff:
123
+ consumes:
124
+ - type: file
125
+ path: "docs/tasks/{{task_id}}.md"
126
+ description: "Design document from planner"
127
+ produces:
128
+ - type: git_commit
129
+ description: "Implementation commit"
117
130
  - id: test
118
131
  role: tester
119
132
  description: Run tests and fix failures
120
133
  prompt: "Run tests. Fix failures. Add missing tests for {{task_id}}."
134
+ handoff:
135
+ consumes:
136
+ - type: git_commit
137
+ from: implement
138
+ description: "Implementation to test"
139
+ produces:
140
+ - type: test_result
141
+ description: "Test results after fixes"
121
142
  - id: review
122
143
  role: reviewer
123
144
  description: Review and output verdict
124
145
  prompt: "Review changes for {{task_id}}. Output PASS or FAIL."
146
+ handoff:
147
+ consumes:
148
+ - type: test_result
149
+ from: test
150
+ description: "Test results to review"
151
+ produces:
152
+ - type: summary
153
+ description: "Review verdict (PASS/FAIL)"
125
154
  transitions:
126
155
  - from: review
127
156
  to: implement
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",