intentdna 1.4.4 → 1.4.6

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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "DNA template compilation + runtime enforcement",
12
- "version": "1.4.4",
12
+ "version": "1.4.6",
13
13
  "source": "./"
14
14
  }
15
15
  ]
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "Declarative policy layer for AI agent governance"
5
5
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * dna feedback [--days <N>] [--json]
3
+ *
4
+ * Analyze trace/audit data and produce actionable feedback for template optimization.
5
+ *
6
+ * Reads: .dna/state/trace/trace-*.jsonl
7
+ * Outputs: human-readable report (or JSON with --json)
8
+ */
9
+ export interface FeedbackOptions {
10
+ days: number;
11
+ json: boolean;
12
+ }
13
+ export interface FeedbackReport {
14
+ period_days: number;
15
+ total_events: number;
16
+ blocks: number;
17
+ warns: number;
18
+ allows: number;
19
+ top_blocked_tools: Array<{
20
+ tool: string;
21
+ count: number;
22
+ }>;
23
+ top_blocked_paths: Array<{
24
+ path: string;
25
+ count: number;
26
+ }>;
27
+ top_block_reasons: Array<{
28
+ reason: string;
29
+ count: number;
30
+ }>;
31
+ suggestions: string[];
32
+ }
33
+ export declare function runFeedback(opts: FeedbackOptions): Promise<number>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * dna feedback [--days <N>] [--json]
3
+ *
4
+ * Analyze trace/audit data and produce actionable feedback for template optimization.
5
+ *
6
+ * Reads: .dna/state/trace/trace-*.jsonl
7
+ * Outputs: human-readable report (or JSON with --json)
8
+ */
9
+ import { readTraces } from "../../hooks/state.js";
10
+ function aggregate(traces) {
11
+ let blocks = 0, warns = 0, allows = 0;
12
+ const toolCounts = new Map();
13
+ const pathCounts = new Map();
14
+ const reasonCounts = new Map();
15
+ for (const t of traces) {
16
+ if (t.decision === "block") {
17
+ blocks++;
18
+ if (t.tool_name)
19
+ toolCounts.set(t.tool_name, (toolCounts.get(t.tool_name) ?? 0) + 1);
20
+ if (t.target_path)
21
+ pathCounts.set(t.target_path, (pathCounts.get(t.target_path) ?? 0) + 1);
22
+ if (t.reason) {
23
+ // Normalize reason to first line
24
+ const shortReason = t.reason.split("\n")[0].replace(/^\[Intent DNA\]\s*/, "").slice(0, 100);
25
+ reasonCounts.set(shortReason, (reasonCounts.get(shortReason) ?? 0) + 1);
26
+ }
27
+ }
28
+ else if (t.decision === "warn") {
29
+ warns++;
30
+ }
31
+ else {
32
+ allows++;
33
+ }
34
+ }
35
+ const sorted = (map, limit, key) => [...map.entries()]
36
+ .sort((a, b) => b[1] - a[1])
37
+ .slice(0, limit)
38
+ .map(([k, count]) => ({ [key]: k, count }));
39
+ return {
40
+ total_events: traces.length,
41
+ blocks,
42
+ warns,
43
+ allows,
44
+ top_blocked_tools: sorted(toolCounts, 5, "tool"),
45
+ top_blocked_paths: sorted(pathCounts, 5, "path"),
46
+ top_block_reasons: sorted(reasonCounts, 5, "reason"),
47
+ };
48
+ }
49
+ function generateSuggestions(report) {
50
+ const suggestions = [];
51
+ if (report.blocks === 0 && report.warns === 0) {
52
+ suggestions.push("No blocks or warnings — DNA constraints are not firing. Consider tightening rules or verifying hooks are active.");
53
+ return suggestions;
54
+ }
55
+ const blockRate = report.total_events > 0 ? report.blocks / report.total_events : 0;
56
+ if (blockRate > 0.3) {
57
+ suggestions.push(`High block rate (${(blockRate * 100).toFixed(0)}%). Review if DNA rules are too restrictive — this may slow development.`);
58
+ }
59
+ // Tool-specific suggestions
60
+ for (const t of report.top_blocked_tools) {
61
+ if (t.count >= 5) {
62
+ suggestions.push(`Tool '${t.tool}' blocked ${t.count} times. Consider expanding permissions if this is expected usage.`);
63
+ }
64
+ }
65
+ // Path-specific suggestions
66
+ for (const p of report.top_blocked_paths) {
67
+ if (p.count >= 3) {
68
+ suggestions.push(`Path '${p.path}' blocked ${p.count} times. Consider adding to write scope if agents need access.`);
69
+ }
70
+ }
71
+ // Repeated reason patterns
72
+ for (const r of report.top_block_reasons) {
73
+ if (r.count >= 3) {
74
+ suggestions.push(`Repeated block: "${r.reason}" (${r.count}x). May indicate a rule-scope mismatch.`);
75
+ }
76
+ }
77
+ if (suggestions.length === 0) {
78
+ suggestions.push("Constraints are working within normal parameters. No action needed.");
79
+ }
80
+ return suggestions;
81
+ }
82
+ function formatReport(report) {
83
+ const lines = [];
84
+ lines.push(`DNA Feedback Report (last ${report.period_days} day${report.period_days > 1 ? "s" : ""})`);
85
+ lines.push("=".repeat(50));
86
+ lines.push("");
87
+ lines.push(`Total events: ${report.total_events}`);
88
+ lines.push(` Allowed: ${report.allows}`);
89
+ lines.push(` Blocked: ${report.blocks}`);
90
+ lines.push(` Warned: ${report.warns}`);
91
+ lines.push("");
92
+ if (report.top_blocked_tools.length > 0) {
93
+ lines.push("Top blocked tools:");
94
+ for (const t of report.top_blocked_tools) {
95
+ lines.push(` ${t.tool}: ${t.count}`);
96
+ }
97
+ lines.push("");
98
+ }
99
+ if (report.top_blocked_paths.length > 0) {
100
+ lines.push("Top blocked paths:");
101
+ for (const p of report.top_blocked_paths) {
102
+ lines.push(` ${p.path}: ${p.count}`);
103
+ }
104
+ lines.push("");
105
+ }
106
+ if (report.top_block_reasons.length > 0) {
107
+ lines.push("Top block reasons:");
108
+ for (const r of report.top_block_reasons) {
109
+ lines.push(` "${r.reason}": ${r.count}`);
110
+ }
111
+ lines.push("");
112
+ }
113
+ if (report.suggestions.length > 0) {
114
+ lines.push("Suggestions:");
115
+ for (const s of report.suggestions) {
116
+ lines.push(` - ${s}`);
117
+ }
118
+ lines.push("");
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+ export async function runFeedback(opts) {
123
+ const projectDir = process.cwd();
124
+ const traces = await readTraces(projectDir, opts.days);
125
+ if (traces.length === 0) {
126
+ process.stderr.write(`No trace data found for the last ${opts.days} day(s).\n`);
127
+ process.stderr.write("Traces are generated by dna-hook during Claude Code sessions.\n");
128
+ return 0;
129
+ }
130
+ const stats = aggregate(traces);
131
+ const suggestions = generateSuggestions(stats);
132
+ const report = { ...stats, period_days: opts.days, suggestions };
133
+ if (opts.json) {
134
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
135
+ }
136
+ else {
137
+ process.stderr.write(formatReport(report));
138
+ }
139
+ return 0;
140
+ }
@@ -38,6 +38,9 @@ const TEMPLATE_KEYWORDS = {
38
38
  "brainstorming-first": ["brainstorm", "explore", "ideation", "探索"],
39
39
  "branch-finishing": ["branch", "finish", "cleanup", "pr prep"],
40
40
  "pr-submitter": ["pr", "pull request", "submit"],
41
+ "migration-safety": ["migration", "migrate", "upgrade", "database migration", "dependency upgrade", "迁移"],
42
+ "incident-response": ["incident", "production issue", "hotfix", "outage", "postmortem", "事故"],
43
+ "performance-audit": ["performance", "profiling", "benchmark", "optimize", "latency", "性能"],
41
44
  };
42
45
  function scoreTemplates(description) {
43
46
  const lower = description.toLowerCase();
@@ -17,5 +17,6 @@ export type SetupScope = "user" | "project";
17
17
  export interface SetupOptions {
18
18
  scope: SetupScope;
19
19
  yes: boolean;
20
+ upgrade: boolean;
20
21
  }
21
22
  export declare function runSetup(opts: SetupOptions): Promise<number>;
@@ -95,7 +95,132 @@ function isPluginRegistered() {
95
95
  }
96
96
  }
97
97
  // ── Main ──────────────────────────────────────────────────
98
+ /**
99
+ * Get the currently installed version of intentdna.
100
+ */
101
+ function getInstalledVersion() {
102
+ try {
103
+ const result = execSync("npm list -g intentdna --json 2>/dev/null", {
104
+ encoding: "utf-8",
105
+ timeout: 10000,
106
+ });
107
+ const data = JSON.parse(result);
108
+ return data?.dependencies?.intentdna?.version ?? null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /**
115
+ * Get the latest published version from npm registry.
116
+ */
117
+ function getLatestVersion() {
118
+ try {
119
+ return execSync("npm view intentdna version 2>/dev/null", {
120
+ encoding: "utf-8",
121
+ timeout: 10000,
122
+ }).trim();
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Upgrade flow: npm update → re-register marketplace → re-install plugin.
130
+ */
131
+ async function runUpgrade(opts) {
132
+ // Step 1: Detect claude CLI
133
+ const claudeVersion = detectClaudeCLI();
134
+ if (!claudeVersion) {
135
+ log("Claude Code CLI not detected. Cannot upgrade plugin.");
136
+ return 1;
137
+ }
138
+ log(`Claude Code detected: ${claudeVersion}`);
139
+ // Step 2: Check versions
140
+ const currentVer = getInstalledVersion();
141
+ const latestVer = getLatestVersion();
142
+ if (currentVer)
143
+ log(`Current version: ${currentVer}`);
144
+ if (latestVer)
145
+ log(`Latest version: ${latestVer}`);
146
+ if (currentVer && latestVer && currentVer === latestVer) {
147
+ log("Already at latest version.");
148
+ }
149
+ else {
150
+ // Step 3: npm update
151
+ log("");
152
+ log("Updating intentdna...");
153
+ try {
154
+ execSync("npm update -g intentdna", {
155
+ encoding: "utf-8",
156
+ timeout: 60000,
157
+ stdio: ["inherit", "pipe", "pipe"],
158
+ });
159
+ const newVer = getInstalledVersion();
160
+ log(`Updated to: ${newVer ?? "unknown"}`);
161
+ }
162
+ catch (err) {
163
+ const msg = err instanceof Error ? err.message : String(err);
164
+ log(`npm update failed: ${msg}`);
165
+ log("Try manually: npm update -g intentdna");
166
+ return 1;
167
+ }
168
+ }
169
+ // Step 4: Re-resolve package path
170
+ const pkgPath = await resolvePackagePath();
171
+ if (!pkgPath) {
172
+ log("Error: Could not find intentdna package after update.");
173
+ return 1;
174
+ }
175
+ log(`Package path: ${pkgPath}`);
176
+ // Step 5: Re-register marketplace (picks up new plugin.json version)
177
+ try {
178
+ log("");
179
+ log(`Re-registering marketplace: ${pkgPath}`);
180
+ execSync(`claude plugin marketplace add "${pkgPath}"`, {
181
+ encoding: "utf-8",
182
+ timeout: 30000,
183
+ stdio: ["inherit", "pipe", "pipe"],
184
+ });
185
+ log("Marketplace updated.");
186
+ }
187
+ catch (err) {
188
+ const msg = err instanceof Error ? err.message : String(err);
189
+ log(`Warning: marketplace re-register failed: ${msg}`);
190
+ }
191
+ // Step 6: Re-install plugin (picks up new hooks.json)
192
+ try {
193
+ const installCmd = `claude plugin install intentdna@intentdna --scope ${opts.scope}`;
194
+ log(`Re-installing plugin: ${installCmd}`);
195
+ execSync(installCmd, {
196
+ encoding: "utf-8",
197
+ timeout: 30000,
198
+ stdio: ["inherit", "pipe", "pipe"],
199
+ });
200
+ log("Plugin re-installed.");
201
+ }
202
+ catch (err) {
203
+ const msg = err instanceof Error ? err.message : String(err);
204
+ log(`Warning: plugin re-install failed: ${msg}`);
205
+ }
206
+ // Step 7: Verify
207
+ if (isPluginRegistered()) {
208
+ log("");
209
+ log("Upgrade complete. Run 'dna sync' to recompile your DNA templates.");
210
+ return 0;
211
+ }
212
+ else {
213
+ log("");
214
+ log("Upgrade completed but plugin verification failed.");
215
+ log("Try: claude plugin list");
216
+ return 1;
217
+ }
218
+ }
98
219
  export async function runSetup(opts) {
220
+ // Upgrade mode
221
+ if (opts.upgrade) {
222
+ return runUpgrade(opts);
223
+ }
99
224
  const scope = opts.scope;
100
225
  // Step 1: Detect claude CLI
101
226
  const claudeVersion = detectClaudeCLI();
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 (--scope user|project, --yes)
20
+ setup Register intentdna as a Claude Code plugin (--scope user|project, --upgrade, --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)
@@ -29,6 +29,7 @@ Commands:
29
29
  show Show gene expression state
30
30
  evolve Generate epigenetic markers from outcomes
31
31
  epigenetic Record outcomes, view markers and summaries
32
+ feedback Analyze trace data, suggest template optimizations (--days <N>, --json)
32
33
 
33
34
  Options:
34
35
  --help, -h Show help for a command
@@ -387,6 +388,7 @@ async function main() {
387
388
  options: {
388
389
  scope: { type: "string", short: "s", default: "user" },
389
390
  yes: { type: "boolean", short: "y", default: false },
391
+ upgrade: { type: "boolean", short: "u", default: false },
390
392
  },
391
393
  strict: false,
392
394
  });
@@ -399,6 +401,24 @@ async function main() {
399
401
  const code = await runSetup({
400
402
  scope: setupScope,
401
403
  yes: setupValues.yes,
404
+ upgrade: setupValues.upgrade,
405
+ });
406
+ process.exit(code);
407
+ break;
408
+ }
409
+ case "feedback": {
410
+ const { values: fbValues } = parseArgs({
411
+ args: rest,
412
+ options: {
413
+ days: { type: "string", short: "d", default: "7" },
414
+ json: { type: "boolean", default: false },
415
+ },
416
+ strict: false,
417
+ });
418
+ const { runFeedback } = await import("./commands/feedback.js");
419
+ const code = await runFeedback({
420
+ days: parseInt(fbValues.days, 10) || 7,
421
+ json: fbValues.json,
402
422
  });
403
423
  process.exit(code);
404
424
  break;
@@ -5,7 +5,7 @@
5
5
  * Performs topological sort, parallel group detection, transition compilation,
6
6
  * retry config resolution, cycle detection, and optional Mermaid diagram generation.
7
7
  */
8
- import type { WorkflowDef, WorkflowPlan, RoleDef } from "../schema/types.js";
8
+ import type { WorkflowDef, WorkflowStepDef, WorkflowPlan, RoleDef, RawWorkflowStepEntry } from "../schema/types.js";
9
9
  export interface CompileWorkflowOptions {
10
10
  /** Generate Mermaid diagram (default: false) */
11
11
  mermaid?: boolean;
@@ -21,6 +21,22 @@ export interface CompileWorkflowResult {
21
21
  plan?: WorkflowPlan;
22
22
  errors: CompileWorkflowError[];
23
23
  }
24
+ /**
25
+ * Expand parallel blocks into regular steps with depends_on.
26
+ *
27
+ * A parallel block:
28
+ * - parallel:
29
+ * isolation: worktree
30
+ * steps: [{id: "a", role: "x"}, {id: "b", role: "y"}]
31
+ *
32
+ * Becomes:
33
+ * - {id: "a", role: "x", depends_on: [<prev_step>], isolation: worktree}
34
+ * - {id: "b", role: "y", depends_on: [<prev_step>], isolation: worktree}
35
+ *
36
+ * The step immediately before the parallel block is the dependency.
37
+ * If no previous step, the parallel steps have no depends_on.
38
+ */
39
+ export declare function expandParallelBlocks(steps: RawWorkflowStepEntry[]): WorkflowStepDef[];
24
40
  /**
25
41
  * Check if two sets of write glob patterns have overlapping scope.
26
42
  * Uses prefix-based matching: "src/xx" and "src/lib/xx" overlap.
@@ -6,6 +6,66 @@
6
6
  * retry config resolution, cycle detection, and optional Mermaid diagram generation.
7
7
  */
8
8
  // ── Internal helpers ─────────────────────────────────────────
9
+ /**
10
+ * Type guard: check if a step entry is a parallel block (syntax sugar).
11
+ */
12
+ function isParallelBlock(entry) {
13
+ return "parallel" in entry && typeof entry.parallel === "object";
14
+ }
15
+ /**
16
+ * Expand parallel blocks into regular steps with depends_on.
17
+ *
18
+ * A parallel block:
19
+ * - parallel:
20
+ * isolation: worktree
21
+ * steps: [{id: "a", role: "x"}, {id: "b", role: "y"}]
22
+ *
23
+ * Becomes:
24
+ * - {id: "a", role: "x", depends_on: [<prev_step>], isolation: worktree}
25
+ * - {id: "b", role: "y", depends_on: [<prev_step>], isolation: worktree}
26
+ *
27
+ * The step immediately before the parallel block is the dependency.
28
+ * If no previous step, the parallel steps have no depends_on.
29
+ */
30
+ export function expandParallelBlocks(steps) {
31
+ const result = [];
32
+ let lastStepId = null;
33
+ for (const entry of steps) {
34
+ if (isParallelBlock(entry)) {
35
+ const block = entry.parallel;
36
+ const expandedIds = [];
37
+ for (const inner of block.steps) {
38
+ const expanded = {
39
+ ...inner,
40
+ // Add depends_on from last step before this block
41
+ depends_on: lastStepId
42
+ ? [...(inner.depends_on ?? []), lastStepId]
43
+ : inner.depends_on,
44
+ // Inherit isolation from parallel block if step doesn't override
45
+ isolation: inner.isolation ?? block.isolation,
46
+ };
47
+ result.push(expanded);
48
+ expandedIds.push(inner.id);
49
+ }
50
+ // After expansion, "lastStepId" stays as the set of expanded IDs.
51
+ // The next step should depend on ALL parallel steps.
52
+ // We can't set a single lastStepId, so we track it differently:
53
+ // We set lastStepId to null and let the NEXT regular step
54
+ // depend on all expanded IDs via explicit depends_on in the input.
55
+ // Actually, the simplest approach: peek at the next step.
56
+ // If the next step already has depends_on referencing these IDs, good.
57
+ // Otherwise, we need a way to inject deps.
58
+ // Since this is sugar, we rely on the user specifying depends_on
59
+ // on the join step (like in the spec example).
60
+ lastStepId = null; // Reset — next step must use explicit depends_on
61
+ }
62
+ else {
63
+ result.push(entry);
64
+ lastStepId = entry.id;
65
+ }
66
+ }
67
+ return result;
68
+ }
9
69
  /**
10
70
  * If no step in the workflow has depends_on, inject implicit sequential
11
71
  * dependencies based on array order (step[i] depends_on step[i-1]).
@@ -332,8 +392,10 @@ export function compileWorkflow(workflow, options) {
332
392
  });
333
393
  return { ok: false, errors };
334
394
  }
395
+ // 1.5. Expand parallel blocks (syntax sugar) into regular steps
396
+ const expandedSteps = expandParallelBlocks(workflow.steps);
335
397
  // 2. Infer implicit deps if no step has depends_on
336
- const resolvedSteps = inferImplicitDeps(workflow.steps);
398
+ const resolvedSteps = inferImplicitDeps(expandedSteps);
337
399
  // 3. Build dependency graph
338
400
  const graph = buildDependencyGraph(resolvedSteps);
339
401
  // 4. Topological sort + cycle detection
@@ -14,4 +14,23 @@
14
14
  *
15
15
  * Fail-open: all errors → { continue: true, suppressOutput: true }
16
16
  */
17
- export {};
17
+ export interface SessionSummary {
18
+ total: number;
19
+ blocks: number;
20
+ warns: number;
21
+ topBlockedTools: Array<{
22
+ tool: string;
23
+ count: number;
24
+ }>;
25
+ topBlockedPaths: Array<{
26
+ path: string;
27
+ count: number;
28
+ }>;
29
+ }
30
+ export declare function computeSummary(traces: Array<{
31
+ decision: string;
32
+ tool_name?: string;
33
+ target_path?: string;
34
+ reason?: string;
35
+ }>): SessionSummary;
36
+ export declare function formatSummary(s: SessionSummary): string | null;
package/dist/hooks/cli.js CHANGED
@@ -17,15 +17,54 @@
17
17
  import { readFile, stat } from "node:fs/promises";
18
18
  import { resolve } from "node:path";
19
19
  import { randomUUID } from "node:crypto";
20
- import { readStdin, writeOutput, silentOutput } from "./protocol.js";
20
+ import { readStdin, writeOutput, silentOutput, allowOutput } from "./protocol.js";
21
21
  import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, } from "./enforce.js";
22
- import { appendAudit, readWorkflowState, appendTrace, rotateTraces } from "./state.js";
22
+ import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces } from "./state.js";
23
23
  // ── Constants ──────────────────────────────────────────────
24
24
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
25
25
  const VALID_EVENTS = new Set([
26
26
  "PreToolUse", "PostToolUse", "UserPromptSubmit",
27
27
  "SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart",
28
28
  ]);
29
+ export function computeSummary(traces) {
30
+ let blocks = 0, warns = 0;
31
+ const toolCounts = new Map();
32
+ const pathCounts = new Map();
33
+ for (const t of traces) {
34
+ if (t.decision === "block") {
35
+ blocks++;
36
+ if (t.tool_name)
37
+ toolCounts.set(t.tool_name, (toolCounts.get(t.tool_name) ?? 0) + 1);
38
+ if (t.target_path)
39
+ pathCounts.set(t.target_path, (pathCounts.get(t.target_path) ?? 0) + 1);
40
+ }
41
+ else if (t.decision === "warn") {
42
+ warns++;
43
+ }
44
+ }
45
+ const topBlockedTools = [...toolCounts.entries()]
46
+ .sort((a, b) => b[1] - a[1])
47
+ .slice(0, 3)
48
+ .map(([tool, count]) => ({ tool, count }));
49
+ const topBlockedPaths = [...pathCounts.entries()]
50
+ .sort((a, b) => b[1] - a[1])
51
+ .slice(0, 3)
52
+ .map(([path, count]) => ({ path, count }));
53
+ return { total: traces.length, blocks, warns, topBlockedTools, topBlockedPaths };
54
+ }
55
+ export function formatSummary(s) {
56
+ if (s.blocks === 0 && s.warns === 0)
57
+ return null;
58
+ const lines = [];
59
+ lines.push(`[Intent DNA] Session summary: ${s.total} events, ${s.blocks} blocked, ${s.warns} warned`);
60
+ if (s.topBlockedTools.length > 0) {
61
+ lines.push(" Top blocked tools: " + s.topBlockedTools.map(t => `${t.tool}(${t.count})`).join(", "));
62
+ }
63
+ if (s.topBlockedPaths.length > 0) {
64
+ lines.push(" Top blocked paths: " + s.topBlockedPaths.map(p => `${p.path}(${p.count})`).join(", "));
65
+ }
66
+ return lines.join("\n");
67
+ }
29
68
  // ── Main ───────────────────────────────────────────────────
30
69
  async function main() {
31
70
  // Parse args
@@ -67,7 +106,7 @@ async function main() {
67
106
  state.existingArtifactPaths = await scanConsumedArtifactPaths(projectDir, ir, wfState.workflow, wfState.current_step);
68
107
  }
69
108
  }
70
- // Special handling for Stop — needs async workflow state read
109
+ // Special handling for Stop — needs async workflow state read + session summary
71
110
  if (event === "Stop") {
72
111
  const wfState = await readWorkflowState(projectDir, sessionId);
73
112
  const stopContext = wfState ? {
@@ -78,11 +117,28 @@ async function main() {
78
117
  started_at: wfState.started_at,
79
118
  completed_artifacts: wfState.completed_artifacts,
80
119
  } : null;
81
- const stopOutput = enforceStop(ir, {
120
+ let stopOutput = enforceStop(ir, {
82
121
  cwd: typeof rawInput.cwd === "string" ? rawInput.cwd : undefined,
83
122
  session_id: sessionId,
84
123
  stop_reason: typeof rawInput.stop_reason === "string" ? rawInput.stop_reason : undefined,
85
124
  }, stopContext);
125
+ // Session summary: aggregate block/warn stats from trace
126
+ try {
127
+ const traces = await readTraces(projectDir, 1);
128
+ const summary = computeSummary(traces);
129
+ const summaryText = formatSummary(summary);
130
+ if (summaryText) {
131
+ if (stopOutput.suppressOutput) {
132
+ // Was silent → upgrade to allow with summary
133
+ stopOutput = allowOutput(summaryText, "Stop");
134
+ }
135
+ else if (stopOutput.reason) {
136
+ // Was a block → append summary to reason
137
+ stopOutput.reason += "\n\n" + summaryText;
138
+ }
139
+ }
140
+ }
141
+ catch { /* fail-open: summary failure never blocks */ }
86
142
  writeOutput(stopOutput);
87
143
  // Trace for Stop
88
144
  appendTrace(projectDir, {
@@ -91,7 +147,7 @@ async function main() {
91
147
  workflow: wfState?.workflow,
92
148
  step: wfState?.current_step,
93
149
  decision: stopOutput.continue === false ? "block" : "allow",
94
- duration_ms: Date.now() - Date.now(), // minimal
150
+ duration_ms: 0,
95
151
  timestamp: new Date().toISOString(),
96
152
  }).catch(() => { });
97
153
  return;
@@ -7,3 +7,4 @@
7
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";
8
8
  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
9
  export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
10
+ export { type SessionSummary, computeSummary, formatSummary, } from "./cli.js";
@@ -10,3 +10,5 @@ export { readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silen
10
10
  export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
11
11
  // State management
12
12
  export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
13
+ // Session summary
14
+ export { computeSummary, formatSummary, } from "./cli.js";
@@ -63,6 +63,14 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
63
63
  }
64
64
  lines.push("</Steps>");
65
65
  lines.push("");
66
+ // Execution Policy — force-complete standard
67
+ lines.push("<Execution_Policy>");
68
+ lines.push("- Use TodoWrite to track each step as pending/in_progress/completed.");
69
+ lines.push("- After completing each step, immediately proceed to the next — do not stop, summarize, or wait for confirmation.");
70
+ lines.push("- If a step fails, mark it as failed in TodoWrite, log the error, and continue to the next non-dependent step.");
71
+ lines.push("- Do not ask the user for permission between steps — the workflow is pre-approved.");
72
+ lines.push("</Execution_Policy>");
73
+ lines.push("");
66
74
  // Tool_Usage — only if roles have permissions or scope
67
75
  const toolUsageLines = [];
68
76
  for (const roleName of usedRoles) {
@@ -22,6 +22,13 @@ const SENTINEL_COMMENT = "# intentdna:managed -- do not edit manually";
22
22
  function sanitizeForBash(id) {
23
23
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
24
24
  }
25
+ /**
26
+ * Sanitize a step ID for safe use in shell paths and git branch names.
27
+ * Allows alphanumeric, hyphen, underscore, and dot only.
28
+ */
29
+ function sanitizeForShell(id) {
30
+ return id.replace(/[^a-zA-Z0-9_.-]/g, "_");
31
+ }
25
32
  /**
26
33
  * Get the agent name for a role, using toKebabCase and the configured prefix.
27
34
  */
@@ -203,7 +210,8 @@ function generateGroupExecution(group, stepMap, options, plan) {
203
210
  const lines = [];
204
211
  const isParallel = group.step_ids.length > 1;
205
212
  if (isParallel) {
206
- lines.push(` # Group ${group.group_index}: ${group.step_ids.join(", ")} (parallel)`);
213
+ const isoLabel = group.isolation === "worktree" ? "parallel, worktree" : "parallel";
214
+ lines.push(` # Group ${group.group_index}: ${group.step_ids.join(", ")} (${isoLabel})`);
207
215
  }
208
216
  else {
209
217
  lines.push(` # Group ${group.group_index}: ${group.step_ids[0]} (sequential)`);
@@ -213,8 +221,62 @@ function generateGroupExecution(group, stepMap, options, plan) {
213
221
  const varId = sanitizeForBash(id);
214
222
  lines.push(` STEP_${varId}_STATUS=0`);
215
223
  }
216
- if (isParallel) {
217
- // Background jobs
224
+ if (isParallel && group.isolation === "worktree") {
225
+ // Worktree-isolated parallel execution
226
+ lines.push("");
227
+ lines.push(' MAIN_BRANCH=$(git rev-parse --abbrev-ref HEAD)');
228
+ // Create worktrees
229
+ for (const id of group.step_ids) {
230
+ const safeId = sanitizeForShell(id);
231
+ lines.push(` git worktree add ".dna/worktrees/${safeId}" -b "dna-wt-${safeId}" HEAD 2>/dev/null`);
232
+ }
233
+ // Background jobs in worktree dirs
234
+ for (const id of group.step_ids) {
235
+ const step = stepMap.get(id);
236
+ const varId = sanitizeForBash(id);
237
+ const safeId = sanitizeForShell(id);
238
+ const agent = agentName(step.role, options.agentPrefix);
239
+ const prompt = escapeShellSingleQuote(step.prompt ?? step.description);
240
+ lines.push("");
241
+ lines.push(` (cd ".dna/worktrees/${safeId}" && run_agent "${agent}" '${prompt}' "${safeId}") &`);
242
+ lines.push(` PID_${varId}=$!`);
243
+ }
244
+ lines.push("");
245
+ // Wait for all
246
+ for (const id of group.step_ids) {
247
+ const step = stepMap.get(id);
248
+ const varId = sanitizeForBash(id);
249
+ if (step.optional) {
250
+ lines.push(` wait $PID_${varId} || {`);
251
+ lines.push(` STEP_${varId}_STATUS=1`);
252
+ lines.push(` echo "[$(date '+%H:%M:%S')] Optional step '${step.id}' failed, continuing..."`);
253
+ lines.push(" }");
254
+ }
255
+ else {
256
+ lines.push(` wait $PID_${varId} || STEP_${varId}_STATUS=1`);
257
+ }
258
+ }
259
+ // Merge worktrees back (escalate on conflict)
260
+ lines.push("");
261
+ lines.push(" # Merge worktrees back to main branch");
262
+ for (const id of group.step_ids) {
263
+ const varId = sanitizeForBash(id);
264
+ const safeId = sanitizeForShell(id);
265
+ lines.push(` if [ "$STEP_${varId}_STATUS" -eq 0 ]; then`);
266
+ lines.push(` merge_worktree "dna-wt-${safeId}" "${safeId}"`);
267
+ lines.push(" fi");
268
+ }
269
+ // Cleanup worktrees
270
+ lines.push("");
271
+ lines.push(" # Cleanup worktrees");
272
+ for (const id of group.step_ids) {
273
+ const safeId = sanitizeForShell(id);
274
+ lines.push(` git worktree remove ".dna/worktrees/${safeId}" 2>/dev/null || true`);
275
+ lines.push(` git branch -D "dna-wt-${safeId}" 2>/dev/null || true`);
276
+ }
277
+ }
278
+ else if (isParallel) {
279
+ // Standard parallel execution (no isolation)
218
280
  for (const id of group.step_ids) {
219
281
  const step = stepMap.get(id);
220
282
  const varId = sanitizeForBash(id);
@@ -248,6 +310,32 @@ function generateGroupExecution(group, stepMap, options, plan) {
248
310
  }
249
311
  return lines;
250
312
  }
313
+ /**
314
+ * Check if any parallel group in the plan uses worktree isolation.
315
+ */
316
+ function needsWorktreeSupport(plan) {
317
+ return plan.parallel_groups.some(g => g.isolation === "worktree" && g.step_ids.length > 1);
318
+ }
319
+ /**
320
+ * Generate the merge_worktree bash helper function.
321
+ * Only included in scripts that have worktree-isolated parallel groups.
322
+ */
323
+ function generateMergeWorktreeFn() {
324
+ return [
325
+ "# Merge a worktree branch back to main (escalate on conflict)",
326
+ "merge_worktree() {",
327
+ ' local branch="$1" name="$2"',
328
+ ' if ! git merge --no-commit --no-ff "$branch" 2>/dev/null; then',
329
+ " git merge --abort",
330
+ ' echo "[Intent DNA] CONFLICT: $name conflicts with current branch. Manual resolution required."',
331
+ ' echo "[Intent DNA] Branch preserved: $branch"',
332
+ " return 1",
333
+ " fi",
334
+ ' git commit -m "merge: $name" --no-edit 2>/dev/null || true',
335
+ "}",
336
+ "",
337
+ ];
338
+ }
251
339
  /**
252
340
  * Generate transition check code after all groups execute.
253
341
  */
@@ -316,6 +404,10 @@ export function compileWorkflowToShell(plan, options) {
316
404
  // run_agent function
317
405
  lines.push(...generateRunAgentFn(opts));
318
406
  lines.push("");
407
+ // merge_worktree function (only if needed)
408
+ if (needsWorktreeSupport(plan)) {
409
+ lines.push(...generateMergeWorktreeFn());
410
+ }
319
411
  // Main execution
320
412
  if (plan.retry.max_retries > 0) {
321
413
  // With retry loop
@@ -154,6 +154,16 @@ export interface WorkflowDef {
154
154
  default_isolation?: "none" | "worktree" | "auto";
155
155
  merge_strategy?: "escalate";
156
156
  }
157
+ /** Parallel block syntax sugar — expanded by compiler into steps with depends_on */
158
+ export interface ParallelBlockDef {
159
+ parallel: {
160
+ isolation?: "none" | "worktree" | "auto";
161
+ merge_strategy?: "escalate";
162
+ steps: WorkflowStepDef[];
163
+ };
164
+ }
165
+ /** Raw workflow step entry: either a regular step or a parallel block (syntax sugar) */
166
+ export type RawWorkflowStepEntry = WorkflowStepDef | ParallelBlockDef;
157
167
  export interface EpigeneticEffect {
158
168
  gene?: string;
159
169
  action?: ModifierAction;
@@ -0,0 +1,66 @@
1
+ # incident-response: 生产事故响应
2
+ # 适用: 线上故障排查、紧急修复、事后复盘
3
+ # 核心: 快速止血、根因分析、防止复发
4
+
5
+ version: "0.1.0"
6
+ id: template_incident_response
7
+ name: Incident Response
8
+ type: project
9
+ namespace: ir
10
+
11
+ cascade:
12
+ inherits: ["species:default"]
13
+ priority: 100
14
+
15
+ genes:
16
+ triage_first:
17
+ description: Assess severity and blast radius before fixing
18
+ codons:
19
+ - type: attract
20
+ target: severity_assessment
21
+ - type: attract
22
+ target: blast_radius_check
23
+ - type: repel
24
+ target: premature_fix
25
+ - type: sense
26
+ signal: production_alert
27
+ response: prioritize_investigation
28
+
29
+ minimal_fix:
30
+ description: Apply the smallest change that stops the bleeding
31
+ codons:
32
+ - type: attract
33
+ target: targeted_hotfix
34
+ - type: attract
35
+ target: revert_option
36
+ - type: repel
37
+ target: refactor_during_incident
38
+ - type: weight
39
+ a: speed
40
+ b: elegance
41
+ ratio: 0.9
42
+
43
+ evidence_driven:
44
+ description: Collect evidence before hypothesizing
45
+ codons:
46
+ - type: attract
47
+ target: logs_and_metrics
48
+ - type: attract
49
+ target: reproduction_steps
50
+ - type: repel
51
+ target: guessing_root_cause
52
+ - type: threshold
53
+ condition: "has_evidence == true"
54
+ action: escalate
55
+
56
+ prevent_recurrence:
57
+ description: Every fix must include prevention measures
58
+ codons:
59
+ - type: attract
60
+ target: regression_test
61
+ - type: attract
62
+ target: monitoring_alert
63
+ - type: repel
64
+ target: fix_without_test
65
+
66
+ contexts: {}
@@ -0,0 +1,67 @@
1
+ # migration-safety: 安全迁移
2
+ # 适用: 数据库迁移、依赖升级、框架版本迁移、API 版本切换
3
+ # 核心: 回滚优先、渐进式、验证每一步
4
+
5
+ version: "0.1.0"
6
+ id: template_migration_safety
7
+ name: Migration Safety
8
+ type: project
9
+ namespace: mig
10
+
11
+ cascade:
12
+ inherits: ["species:default"]
13
+ priority: 100
14
+
15
+ genes:
16
+ rollback_first:
17
+ description: Every migration step must have a rollback plan
18
+ codons:
19
+ - type: attract
20
+ target: reversible_changes
21
+ - type: attract
22
+ target: rollback_script
23
+ - type: repel
24
+ target: destructive_migration
25
+ - type: threshold
26
+ condition: "has_rollback_plan == true"
27
+ action: block
28
+
29
+ incremental_migration:
30
+ description: Migrate incrementally, verify at each stage
31
+ codons:
32
+ - type: attract
33
+ target: phased_migration
34
+ - type: attract
35
+ target: canary_first
36
+ - type: repel
37
+ target: big_bang_migration
38
+ - type: weight
39
+ a: safety
40
+ b: speed
41
+ ratio: 0.9
42
+
43
+ backward_compatible:
44
+ description: Maintain backward compatibility during transition
45
+ codons:
46
+ - type: attract
47
+ target: dual_write
48
+ - type: attract
49
+ target: feature_flags
50
+ - type: repel
51
+ target: breaking_change_without_migration
52
+ - type: sense
53
+ signal: deprecation_warning
54
+ response: plan_migration_path
55
+
56
+ verify_data_integrity:
57
+ description: Validate data consistency after each migration step
58
+ codons:
59
+ - type: attract
60
+ target: data_validation_checks
61
+ - type: attract
62
+ target: diff_before_after
63
+ - type: threshold
64
+ condition: "data_loss == false"
65
+ action: block
66
+
67
+ contexts: {}
@@ -0,0 +1,67 @@
1
+ # performance-audit: 性能优化
2
+ # 适用: 性能分析、基准测试、瓶颈排查、优化实施
3
+ # 核心: 先测量再优化、数据驱动、避免过早优化
4
+
5
+ version: "0.1.0"
6
+ id: template_performance_audit
7
+ name: Performance Audit
8
+ type: project
9
+ namespace: perf
10
+
11
+ cascade:
12
+ inherits: ["species:default"]
13
+ priority: 100
14
+
15
+ genes:
16
+ measure_first:
17
+ description: Profile and benchmark before making any change
18
+ codons:
19
+ - type: attract
20
+ target: profiling_data
21
+ - type: attract
22
+ target: baseline_benchmark
23
+ - type: repel
24
+ target: optimize_without_measurement
25
+ - type: threshold
26
+ condition: "has_baseline_metrics == true"
27
+ action: escalate
28
+
29
+ targeted_optimization:
30
+ description: Optimize the actual bottleneck, not hunches
31
+ codons:
32
+ - type: attract
33
+ target: hotspot_analysis
34
+ - type: attract
35
+ target: algorithmic_improvement
36
+ - type: repel
37
+ target: premature_optimization
38
+ - type: weight
39
+ a: correctness
40
+ b: performance
41
+ ratio: 0.7
42
+
43
+ regression_prevention:
44
+ description: Ensure optimizations don't break functionality
45
+ codons:
46
+ - type: attract
47
+ target: before_after_comparison
48
+ - type: attract
49
+ target: existing_tests_pass
50
+ - type: repel
51
+ target: optimization_breaks_tests
52
+ - type: threshold
53
+ condition: "test_regression == false"
54
+ action: block
55
+
56
+ document_results:
57
+ description: Record measurements and decisions for future reference
58
+ codons:
59
+ - type: attract
60
+ target: performance_report
61
+ - type: attract
62
+ target: optimization_rationale
63
+ - type: sense
64
+ signal: performance_improvement
65
+ response: record_technique
66
+
67
+ contexts: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",