intentdna 1.4.3 → 1.4.5

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.
@@ -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
@@ -139,6 +139,7 @@ export interface WorkflowStepDef {
139
139
  completion?: CompletionCheck[];
140
140
  checkpoints?: StepCheckpoint[];
141
141
  handoff?: StepHandoff;
142
+ isolation?: "none" | "worktree" | "auto";
142
143
  }
143
144
  /** Top-level workflow definition */
144
145
  export interface WorkflowDef {
@@ -150,7 +151,19 @@ export interface WorkflowDef {
150
151
  max_rounds?: number;
151
152
  produces?: HandoffArtifact[];
152
153
  consumes?: HandoffArtifact[];
154
+ default_isolation?: "none" | "worktree" | "auto";
155
+ merge_strategy?: "escalate";
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
+ };
153
164
  }
165
+ /** Raw workflow step entry: either a regular step or a parallel block (syntax sugar) */
166
+ export type RawWorkflowStepEntry = WorkflowStepDef | ParallelBlockDef;
154
167
  export interface EpigeneticEffect {
155
168
  gene?: string;
156
169
  action?: ModifierAction;
@@ -320,6 +333,9 @@ export interface WorkflowStep {
320
333
  export interface ParallelGroup {
321
334
  group_index: number;
322
335
  step_ids: string[];
336
+ isolation: "none" | "worktree";
337
+ merge_strategy: "escalate";
338
+ scope_overlap: boolean;
323
339
  }
324
340
  /** Compiled transition rule */
325
341
  export interface TransitionRule {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,7 @@
33
33
  ],
34
34
  "files": [
35
35
  "dist",
36
+ "spec",
36
37
  ".claude-plugin",
37
38
  "README.md",
38
39
  "LICENSE"
@@ -0,0 +1,205 @@
1
+ # Spec: DNA 并行执行 + Worktree 隔离
2
+
3
+ ## Metadata
4
+ - Interview: 8 rounds, final ambiguity 20%
5
+ - Type: brownfield
6
+ - Date: 2026-04-14
7
+ - Phase: 6 (基础体验)
8
+
9
+ ## Goal
10
+
11
+ DNA 模板可声明并行执行策略和文件隔离策略,编译到 workflow-runner 生成的 bash 脚本中实际执行。支持强制 worktree、编译时自动判断(scope overlap)、和无隔离三种模式。
12
+
13
+ ## 核心设计决策
14
+
15
+ | 决策 | 结论 | 理由 |
16
+ |------|------|------|
17
+ | 执行方式 | 自管 worktree (bash) | 不依赖 CC 平台特性,完全自控 |
18
+ | isolation 语义 | 纯文件隔离 | context 隔离已由独立进程解决,context 保护已由 Handoff 解决 |
19
+ | Schema 声明 | step 属性 + parallel block 双支持 | 灵活性 + 简洁性兼顾 |
20
+ | auto 模式 | 编译时 scope overlap 检测 | DNA 独有能力——Role scope 编译时已知 |
21
+ | merge 策略 | escalate only (MVP) | 冲突报告给用户,不自动解决 |
22
+ | 模型差异 | 暂不考虑 | 后续扩展 |
23
+
24
+ ## Schema 设计
25
+
26
+ ### isolation 取值
27
+
28
+ | 值 | 含义 |
29
+ |---|---|
30
+ | `none` | 共享目录(默认,当前行为) |
31
+ | `worktree` | 强制每个并行 step 开 worktree |
32
+ | `auto` | 编译时分析 scope overlap,自动决定 none 或 worktree |
33
+
34
+ ### 两种声明方式
35
+
36
+ **方式 A: step 属性 + workflow 默认值(隐式并行)**
37
+
38
+ ```yaml
39
+ workflows:
40
+ rescue:
41
+ default_isolation: auto # workflow 级默认
42
+ merge_strategy: escalate
43
+ steps:
44
+ - id: investigate
45
+ role: investigator
46
+ - id: fix-auth
47
+ role: surgeon
48
+ isolation: worktree # step 级覆盖
49
+ depends_on: [investigate]
50
+ - id: fix-api
51
+ role: surgeon
52
+ # 继承 workflow 默认 auto
53
+ depends_on: [investigate]
54
+ - id: report
55
+ depends_on: [fix-auth, fix-api]
56
+ ```
57
+
58
+ 编译器通过 `depends_on` + Kahn 拓扑排序自动检测并行组(已实现)。
59
+
60
+ **方式 B: 显式 parallel block(语法糖)**
61
+
62
+ ```yaml
63
+ workflows:
64
+ rescue:
65
+ steps:
66
+ - id: investigate
67
+ role: investigator
68
+
69
+ - parallel:
70
+ isolation: worktree
71
+ merge_strategy: escalate
72
+ steps:
73
+ - id: fix-auth
74
+ role: surgeon
75
+ - id: fix-api
76
+ role: surgeon
77
+
78
+ - id: report
79
+ depends_on: [fix-auth, fix-api]
80
+ ```
81
+
82
+ parallel block 编译时展开为带 `depends_on` 的独立 step + 统一 isolation 设置。
83
+
84
+ ### isolation 粒度
85
+
86
+ | 粒度 | 声明位置 | 含义 |
87
+ |------|----------|------|
88
+ | step 级 | step.isolation | 该 step 的并行执行隔离 |
89
+ | workflow 级 | workflow.default_isolation | 所有并行组的默认隔离策略 |
90
+
91
+ step 级 > workflow 级(覆盖优先级)。
92
+
93
+ ## auto 模式编译逻辑
94
+
95
+ ```
96
+ 对每个 ParallelGroup:
97
+ 1. 收集组内所有 step 对应 role 的 write scope
98
+ 2. 做 glob 交叉检测(minimatch 或等价)
99
+ 3. 无重叠 → 编译为 none
100
+ 4. 有重叠 → 编译为 worktree
101
+ 5. 无法判断("**/*")→ 保守编译为 worktree
102
+ ```
103
+
104
+ ## workflow-runner 生成
105
+
106
+ ### isolation: none(当前行为不变)
107
+
108
+ ```bash
109
+ # Group 1: fix-auth, fix-api (parallel, no isolation)
110
+ run_agent "dna-surgeon" 'prompt' "fix-auth" &
111
+ PID_fix_auth=$!
112
+ run_agent "dna-surgeon" 'prompt' "fix-api" &
113
+ PID_fix_api=$!
114
+ wait $PID_fix_auth || STEP_fix_auth_STATUS=1
115
+ wait $PID_fix_api || STEP_fix_api_STATUS=1
116
+ ```
117
+
118
+ ### isolation: worktree
119
+
120
+ ```bash
121
+ # Group 1: fix-auth, fix-api (parallel, worktree isolation)
122
+ MAIN_BRANCH=$(git rev-parse --abbrev-ref HEAD)
123
+
124
+ git worktree add .dna/worktrees/fix-auth -b dna-wt-fix-auth HEAD 2>/dev/null
125
+ git worktree add .dna/worktrees/fix-api -b dna-wt-fix-api HEAD 2>/dev/null
126
+
127
+ (cd .dna/worktrees/fix-auth && run_agent "dna-surgeon" 'prompt' "fix-auth") &
128
+ PID_fix_auth=$!
129
+ (cd .dna/worktrees/fix-api && run_agent "dna-surgeon" 'prompt' "fix-api") &
130
+ PID_fix_api=$!
131
+
132
+ wait $PID_fix_auth || STEP_fix_auth_STATUS=1
133
+ wait $PID_fix_api || STEP_fix_api_STATUS=1
134
+
135
+ # Merge (escalate on conflict)
136
+ merge_worktree() {
137
+ local branch=$1 name=$2
138
+ if ! git merge --no-commit --no-ff "$branch" 2>/dev/null; then
139
+ git merge --abort
140
+ echo "[Intent DNA] CONFLICT: $name conflicts with $MAIN_BRANCH. Manual resolution required."
141
+ echo "[Intent DNA] Branch preserved: $branch"
142
+ return 1
143
+ fi
144
+ git commit -m "merge: $name" --no-edit
145
+ }
146
+
147
+ merge_worktree "dna-wt-fix-auth" "fix-auth"
148
+ merge_worktree "dna-wt-fix-api" "fix-api"
149
+
150
+ # Cleanup (only if merge succeeded)
151
+ git worktree remove .dna/worktrees/fix-auth 2>/dev/null
152
+ git worktree remove .dna/worktrees/fix-api 2>/dev/null
153
+ ```
154
+
155
+ ## Constraint IR 扩展
156
+
157
+ ```typescript
158
+ // ParallelGroup 扩展
159
+ interface ParallelGroup {
160
+ group_index: number;
161
+ step_ids: string[];
162
+ isolation: 'none' | 'worktree'; // auto 已被编译期解析
163
+ merge_strategy: 'escalate'; // MVP: escalate only
164
+ scope_overlap: boolean; // 编译期分析结果
165
+ }
166
+
167
+ // WorkflowIR 扩展
168
+ interface WorkflowIR {
169
+ // ... 现有字段
170
+ parallel_groups?: ParallelGroupIR[]; // 新增
171
+ }
172
+ ```
173
+
174
+ ## 与 DNA 现有概念的关系
175
+
176
+ | DNA 概念 | 在并行隔离中的角色 |
177
+ |---|---|
178
+ | **Role scope** | auto 模式的编译时 overlap 分析输入 |
179
+ | **Handoff** | 并行 step 的 artifact 传递(已实现) |
180
+ | **Threshold codon** | 可声明 `parallel_without_isolation == false` 硬约束 |
181
+ | **Attract codon** | `attract: worktree_per_task` 软提示(保留兼容) |
182
+ | **Hooks enforce** | worktree 内 scope enforce 仍生效 |
183
+
184
+ ## 不做
185
+
186
+ - 自动冲突解决(MVP 只 escalate)
187
+ - LLM 模型差异适配(暂不考虑)
188
+ - context budget 管理(已由独立进程 + Handoff 解决)
189
+ - container 级隔离(过度设计)
190
+
191
+ ## Acceptance Criteria
192
+
193
+ - [ ] Schema: WorkflowStepDef 支持 `isolation` 字段
194
+ - [ ] Schema: WorkflowDef 支持 `default_isolation` + `merge_strategy`
195
+ - [ ] Schema: `parallel:` block 语法糖可解析
196
+ - [ ] Compiler: auto 模式 scope overlap 检测正确
197
+ - [ ] Compiler: parallel block 展开为 step + depends_on
198
+ - [ ] workflow-runner: isolation=none 行为不变(向后兼容)
199
+ - [ ] workflow-runner: isolation=worktree 生成 git worktree 生命周期脚本
200
+ - [ ] workflow-runner: merge 冲突时 escalate(报告 + 保留分支)
201
+ - [ ] E2E: 两个并行 step 写不同目录,auto → none,正确执行
202
+ - [ ] E2E: 两个并行 step 写相同目录,auto → worktree,隔离执行 + merge
203
+ - [ ] Dogfood: flutter-rewrite rescue workflow 可配置 isolation
204
+ - [ ] IR: ParallelGroup 包含 isolation + scope_overlap 信息
205
+ - [ ] 测试覆盖所有新增代码