intentdna 1.4.4 → 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.
|
@@ -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(
|
|
398
|
+
const resolvedSteps = inferImplicitDeps(expandedSteps);
|
|
337
399
|
// 3. Build dependency graph
|
|
338
400
|
const graph = buildDependencyGraph(resolvedSteps);
|
|
339
401
|
// 4. Topological sort + cycle detection
|
|
@@ -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
|
@@ -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;
|