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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/generate.d.ts +16 -0
- package/dist/cli/commands/generate.js +151 -0
- package/dist/cli/commands/verify.js +51 -5
- package/dist/cli/index.js +22 -0
- package/dist/compiler/workflow.d.ts +25 -1
- package/dist/compiler/workflow.js +170 -6
- package/dist/hooks/cli.js +34 -1
- package/dist/hooks/enforce.d.ts +3 -0
- package/dist/hooks/enforce.js +7 -2
- package/dist/runtime/workflow-runner.js +95 -3
- package/dist/schema/types.d.ts +16 -0
- package/package.json +2 -1
- package/spec/parallel-isolation.md +205 -0
- package/spec/schema-spec.md +676 -0
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -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
|
+
"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
|
+
- [ ] 测试覆盖所有新增代码
|