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.
@@ -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.5",
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.5",
4
4
  "description": "Declarative policy layer for AI agent governance"
5
5
  }
@@ -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
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.4.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",