intentdna 1.9.0 → 1.9.3

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.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +227 -87
  4. package/dist/cli/commands/run-lifecycle.d.ts +61 -11
  5. package/dist/cli/commands/run-lifecycle.js +184 -50
  6. package/dist/cli/commands/run-observer.d.ts +15 -0
  7. package/dist/cli/commands/run-observer.js +210 -0
  8. package/dist/cli/commands/run.d.ts +16 -20
  9. package/dist/cli/commands/run.js +461 -659
  10. package/dist/cli/commands/sync.js +16 -12
  11. package/dist/cli/index.js +14 -5
  12. package/dist/compiler/compile.d.ts +1 -0
  13. package/dist/compiler/compile.js +1 -1
  14. package/dist/compiler/controller.d.ts +16 -0
  15. package/dist/compiler/controller.js +693 -0
  16. package/dist/compiler/input-resolver.d.ts +2 -0
  17. package/dist/compiler/input-resolver.js +22 -6
  18. package/dist/hooks/cli.d.ts +45 -0
  19. package/dist/hooks/cli.js +263 -31
  20. package/dist/hooks/host-event.d.ts +57 -0
  21. package/dist/hooks/host-event.js +187 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/index.js +1 -0
  24. package/dist/hooks/protocol.d.ts +7 -0
  25. package/dist/hooks/schema.d.ts +1 -0
  26. package/dist/hooks/schema.js +89 -1
  27. package/dist/hooks/state.d.ts +23 -1
  28. package/dist/hooks/state.js +9 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/mcp/index.js +2 -0
  32. package/dist/mcp/tools-run.d.ts +4 -0
  33. package/dist/mcp/tools-run.js +419 -0
  34. package/dist/mcp/tools-state.d.ts +1 -1
  35. package/dist/mcp/tools-state.js +13 -241
  36. package/dist/runtime/artifact-store.d.ts +146 -0
  37. package/dist/runtime/artifact-store.js +1436 -0
  38. package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
  39. package/dist/runtime/canonical-attempt-outcome.js +195 -0
  40. package/dist/runtime/canonical-json.d.ts +10 -0
  41. package/dist/runtime/canonical-json.js +105 -0
  42. package/dist/runtime/canonical-run-application.d.ts +93 -0
  43. package/dist/runtime/canonical-run-application.js +229 -0
  44. package/dist/runtime/canonical-run-service.d.ts +454 -0
  45. package/dist/runtime/canonical-run-service.js +4760 -0
  46. package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
  47. package/dist/runtime/canonical-runtime-composition.js +34 -0
  48. package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
  49. package/dist/runtime/canonical-runtime-projection.js +4 -0
  50. package/dist/runtime/canonical-target-compiler.d.ts +46 -0
  51. package/dist/runtime/canonical-target-compiler.js +514 -0
  52. package/dist/runtime/claude-agent-identity.d.ts +2 -0
  53. package/dist/runtime/claude-agent-identity.js +21 -0
  54. package/dist/runtime/executable-run-plan.d.ts +328 -0
  55. package/dist/runtime/executable-run-plan.js +1485 -0
  56. package/dist/runtime/execution-authority.d.ts +83 -0
  57. package/dist/runtime/execution-authority.js +98 -0
  58. package/dist/runtime/handoff-resolver.js +4 -1
  59. package/dist/runtime/harness-pull-adapter.d.ts +146 -0
  60. package/dist/runtime/harness-pull-adapter.js +315 -0
  61. package/dist/runtime/index.d.ts +39 -14
  62. package/dist/runtime/index.js +29 -7
  63. package/dist/runtime/local-execution-authority.d.ts +194 -0
  64. package/dist/runtime/local-execution-authority.js +2897 -0
  65. package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
  66. package/dist/runtime/local-execution-reconciliation.js +107 -0
  67. package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
  68. package/dist/runtime/local-execution-supervisor-script.js +793 -0
  69. package/dist/runtime/local-provider-sandbox.d.ts +42 -0
  70. package/dist/runtime/local-provider-sandbox.js +154 -0
  71. package/dist/runtime/local-verifier-execution.d.ts +24 -0
  72. package/dist/runtime/local-verifier-execution.js +290 -0
  73. package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
  74. package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
  75. package/dist/runtime/plan-store.d.ts +30 -0
  76. package/dist/runtime/plan-store.js +231 -0
  77. package/dist/runtime/process-tree.d.ts +9 -0
  78. package/dist/runtime/process-tree.js +97 -24
  79. package/dist/runtime/providers/codex.js +2 -2
  80. package/dist/runtime/push-driver.d.ts +184 -0
  81. package/dist/runtime/push-driver.js +1002 -0
  82. package/dist/runtime/result-store.d.ts +34 -3
  83. package/dist/runtime/result-store.js +1073 -2
  84. package/dist/runtime/run-binding.d.ts +68 -0
  85. package/dist/runtime/run-binding.js +278 -0
  86. package/dist/runtime/run-contracts.d.ts +575 -0
  87. package/dist/runtime/run-contracts.js +58 -7
  88. package/dist/runtime/run-controller.d.ts +1 -6
  89. package/dist/runtime/run-controller.js +0 -41
  90. package/dist/runtime/run-store.d.ts +85 -7
  91. package/dist/runtime/run-store.js +2528 -186
  92. package/dist/runtime/skill-adapter.d.ts +18 -7
  93. package/dist/runtime/skill-adapter.js +100 -742
  94. package/dist/runtime/structured-output-validator.d.ts +44 -0
  95. package/dist/runtime/structured-output-validator.js +171 -0
  96. package/dist/runtime/verifier-command-binding.d.ts +22 -0
  97. package/dist/runtime/verifier-command-binding.js +162 -0
  98. package/dist/runtime/verifier.d.ts +46 -6
  99. package/dist/runtime/verifier.js +355 -53
  100. package/dist/runtime/windows-job-keeper.d.ts +47 -0
  101. package/dist/runtime/windows-job-keeper.js +231 -0
  102. package/dist/runtime/worker-executor.d.ts +17 -1
  103. package/dist/runtime/worker-executor.js +26 -5
  104. package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
  105. package/dist/runtime/workflow-plan-adapter.js +502 -6
  106. package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
  107. package/dist/runtime/workflow-runtime-manifest.js +5 -0
  108. package/dist/runtime/workspace-isolation.d.ts +29 -2
  109. package/dist/runtime/workspace-isolation.js +488 -11
  110. package/dist/runtime/workspace-observation.d.ts +44 -0
  111. package/dist/runtime/workspace-observation.js +216 -0
  112. package/dist/schema/controller-registry.d.ts +0 -7
  113. package/dist/schema/controller-registry.js +0 -8
  114. package/dist/schema/types.d.ts +84 -10
  115. package/dist/schema/types.js +2 -0
  116. package/dist/schema/validate.js +151 -1
  117. package/dist/schema/validators/controllers.d.ts +1 -1
  118. package/dist/schema/validators/controllers.js +126 -41
  119. package/dist/schema/workflow-authoring-contract.js +2 -0
  120. package/dist/schema/yaml-parser.js +1 -1
  121. package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
  122. package/dist/templates/flutter-rewrite.dna.yaml +87 -60
  123. package/dist/templates/safe-refactoring.dna.yaml +1 -0
  124. package/native/windows-job-keeper/README.md +80 -0
  125. package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
  126. package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
  127. package/native/windows-job-keeper/keeper.c +979 -0
  128. package/native/windows-job-keeper/manifest.json +33 -0
  129. package/package.json +6 -2
  130. package/scripts/build-windows-job-keeper.mjs +172 -0
  131. package/scripts/verify-windows-job-keeper.mjs +184 -0
@@ -1,14 +1,11 @@
1
1
  /**
2
- * Intent DNA Skill Adapter
2
+ * Intent DNA - Skill Adapter
3
3
  *
4
- * Compiles DNA roles + workflow + genes into Claude Code skill files
5
- * (.claude/skills/<name>/SKILL.md).
6
- *
7
- * Skills are compiled views of DNA's structured definitions — not stored content.
4
+ * Generated Skills are transport launchers. Execution semantics live only in
5
+ * the canonical executable plan and durable run service.
8
6
  */
9
- import { mkdir, writeFile, readdir, readFile, rm } from "node:fs/promises";
7
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
10
8
  import { join, resolve, sep } from "node:path";
11
- import { getControllerKindMetadata } from "../schema/controller-registry.js";
12
9
  import { assertSafeGeneratedName, toKebabCase } from "./agent-md.js";
13
10
  import { resolveWorkflowVariableReferences, valueUsesArguments, workflowUsesArguments, } from "./workflow-runtime-manifest.js";
14
11
  const SENTINEL = "<!-- intentdna:managed — do not edit manually -->";
@@ -20,249 +17,53 @@ function resolveContainedPath(baseDir, ...segments) {
20
17
  }
21
18
  return target;
22
19
  }
23
- function isHumanRole(role) {
24
- return role?.model?.trim().toLowerCase() === "human";
25
- }
26
- function isInteractiveRole(role) {
27
- return role?.model?.trim().toLowerCase() === "interactive";
20
+ export const CANONICAL_SKILL_RUN_REQUEST_SCHEMA_VERSION = "intentdna.skill_run_request.v1";
21
+ function canonicalSkillRequest(kind, key, acceptsArgument, options) {
22
+ return {
23
+ schema_version: CANONICAL_SKILL_RUN_REQUEST_SCHEMA_VERSION,
24
+ dna_sources: options?.dnaSources?.length
25
+ ? [...options.dnaSources]
26
+ : [".dna/configs"],
27
+ target: { kind, key },
28
+ inputs: acceptsArgument
29
+ ? {
30
+ ARGUMENTS: "$ARGUMENTS",
31
+ arguments: "$ARGUMENTS",
32
+ task_id: "$ARGUMENTS",
33
+ workflow: key,
34
+ }
35
+ : { workflow: key },
36
+ input_policy: "declared_only",
37
+ max_concurrency: 1,
38
+ };
28
39
  }
29
- // ── Compile Controller → Skill ─────────────────────────────
30
- export function compileControllerToSkill(controllerKey, controller, variables, workflows) {
31
- const controllerVariables = variables ? resolveWorkflowVariableReferences(variables) : undefined;
32
- const kindMetadata = getControllerKindMetadata(controller.kind);
33
- if (!kindMetadata?.supportsSkillGeneration) {
34
- throw new Error(`Unsupported controller kind: ${controller.kind}`);
35
- }
36
- const diagnosisWorkflowName = controller.diagnosis_workflow;
37
- const fixWorkflowName = controller.fix_workflow;
38
- const diagnosisArtifactPath = controller.diagnosis_artifact_path;
39
- const diagnosisReviewArtifactPath = controller.diagnosis_review_artifact_path;
40
- const reviewArtifactPath = controller.review_artifact_path;
41
- const analyzerRole = controller.roles.analyzer;
42
- const analysisReviewerRole = controller.roles.analysis_reviewer;
43
- const surgeonRole = controller.roles.surgeon;
44
- const fixReviewerRole = controller.roles.fix_reviewer;
45
- const testRunnerRole = controller.roles.test_runner;
46
- if (!diagnosisWorkflowName ||
47
- !fixWorkflowName ||
48
- !diagnosisArtifactPath ||
49
- !diagnosisReviewArtifactPath ||
50
- !reviewArtifactPath ||
51
- !analyzerRole ||
52
- !analysisReviewerRole ||
53
- !surgeonRole ||
54
- !fixReviewerRole ||
55
- !testRunnerRole) {
56
- throw new Error(`Controller '${controllerKey}' is missing fix-loop skill generation fields`);
57
- }
58
- const skillName = `dna-${toKebabCase(controllerKey)}`;
59
- assertSafeGeneratedName(skillName, "skill name");
60
- const analyzerAgent = `dna-${toKebabCase(analyzerRole)}`;
61
- const analysisReviewerAgent = `dna-${toKebabCase(analysisReviewerRole)}`;
62
- const surgeonAgent = `dna-${toKebabCase(surgeonRole)}`;
63
- const fixReviewerAgent = `dna-${toKebabCase(fixReviewerRole)}`;
64
- const testRunnerAgent = `dna-${toKebabCase(testRunnerRole)}`;
65
- const diagnosisWorkflow = workflows?.[diagnosisWorkflowName];
66
- const fixWorkflow = workflows?.[fixWorkflowName];
67
- const diagnosisAnalyzeStep = requireStepByRole(diagnosisWorkflow, analyzerRole, diagnosisWorkflowName);
68
- const diagnosisReviewStep = requireStepByRole(diagnosisWorkflow, analysisReviewerRole, diagnosisWorkflowName);
69
- const fixBugsStep = requireStepByRole(fixWorkflow, surgeonRole, fixWorkflowName);
70
- const reviewChangesStep = requireStepByRole(fixWorkflow, fixReviewerRole, fixWorkflowName);
71
- const verifyReportStep = requireStepByRole(fixWorkflow, testRunnerRole, fixWorkflowName);
72
- const stepPrompt = (step) => step.prompt || step.description || "";
73
- const policy = normalizeControllerPolicy(controller);
40
+ function canonicalSkillBody(params) {
74
41
  const lines = [];
75
- lines.push("---");
76
- lines.push(`name: ${skillName}`);
77
- lines.push(`description: "Use when user says /${skillName}. ${escapeYaml(controller.description || controller.name)}"`);
78
- lines.push("user-invocable: true");
79
- lines.push("triggers:");
80
- lines.push(` - "${toKebabCase(controllerKey)}"`);
81
- lines.push(` - "run ${toKebabCase(controllerKey)}"`);
82
- lines.push("---");
83
- lines.push("");
84
42
  lines.push(SENTINEL);
85
43
  lines.push(`<!-- Compiled: ${new Date().toISOString()} -->`);
86
44
  lines.push("");
87
- lines.push(`# /${skillName} <module>`);
45
+ lines.push(`# /${params.skillName}${params.acceptsArgument ? " <argument>" : ""}`);
88
46
  lines.push("");
89
47
  lines.push("<Purpose>");
90
- lines.push(controller.description || controller.name);
48
+ lines.push(params.description);
91
49
  lines.push("</Purpose>");
92
50
  lines.push("");
93
- lines.push("<Controller>");
94
- lines.push(`kind: ${controller.kind}`);
95
- lines.push(`max_rounds: ${controller.max_rounds}`);
96
- lines.push(`diagnosis_workflow: ${diagnosisWorkflowName}`);
97
- lines.push(`fix_workflow: ${fixWorkflowName}`);
98
- lines.push(`progress_artifact_path: ${controller.progress_artifact_path}`);
99
- lines.push("</Controller>");
100
- lines.push("");
101
- lines.push("<Steps>");
102
- lines.push("0. Validate the module argument before using it in any artifact path.");
103
- lines.push(" - The module argument must match /^[A-Za-z0-9_-]+$/.");
104
- lines.push(" - If it contains '/', '.', '..', spaces, braces, glob characters, or path separators, stop with the configured stop report.");
105
- lines.push(" - Do not read or write controller artifacts until the module argument is safe.");
106
- lines.push("1. Check diagnosis artifact freshness before any fix round.");
107
- lines.push(` - Diagnosis artifact: ${diagnosisArtifactPath}`);
108
- lines.push(` - Diagnosis review artifact: ${diagnosisReviewArtifactPath}`);
109
- lines.push("2. Create a stable controller_run_id once for this invocation before using any artifact path.");
110
- lines.push(" - Reuse the same controller_run_id in every child Agent prompt and in every progress validation.");
111
- lines.push(" - Do not regenerate controller_run_id inside fix rounds.");
112
- lines.push("3. Reuse the diagnosis artifact only when all diagnosis reuse criteria pass:");
113
- for (const criterion of policy.diagnosis_reuse_criteria) {
114
- lines.push(` - ${criterion}`);
115
- }
116
- lines.push(" Reuse skips fresh analyzer and analysis_reviewer agents; it never skips the declared diagnosis contract completion gates or the persisted diagnosis review JSON approval gate before any surgeon step.");
117
- lines.push(`4. If diagnosis is missing, invalid, stale, or scope-changed, run the analyzer once with fresh context:`);
118
- lines.push(" a. Spawn fresh analyzer agent with controller_run_id, then wait for completion.");
119
- lines.push(" ```");
120
- lines.push(" Agent(");
121
- lines.push(` subagent_type="${analyzerAgent}",`);
122
- lines.push(` prompt="controller_run_id=<controller_run_id>. ${escapePrompt(stepPrompt(diagnosisAnalyzeStep))}"`);
123
- lines.push(" )");
124
- lines.push(" ```");
125
- pushWorkflowStepVerifierLines(lines, diagnosisAnalyzeStep, " ");
126
- lines.push(` b. Diagnosis artifact gate: read ${controller.diagnosis_artifact_path}; validate it exists, is non-empty, names the safe module argument, and is current for this module before continuing.`);
127
- lines.push(" c. Spawn fresh analysis_reviewer agent with controller_run_id, then wait for completion.");
128
- lines.push(" ```");
129
- lines.push(" Agent(");
130
- lines.push(` subagent_type="${analysisReviewerAgent}",`);
131
- lines.push(` prompt="controller_run_id=<controller_run_id>; expected_review_artifact=${controller.diagnosis_review_artifact_path}. ${escapePrompt(stepPrompt(diagnosisReviewStep))}"`);
132
- lines.push(" )");
133
- lines.push(" ```");
134
- pushWorkflowStepVerifierLines(lines, diagnosisReviewStep, " ");
135
- lines.push("5. If the diagnosis artifact was reused, do not spawn analyzer or analysis_reviewer; rerun the declared diagnosis contract completion gates against the existing artifacts:");
136
- pushWorkflowStepVerifierLines(lines, diagnosisReviewStep, " ");
137
- lines.push(`6. Diagnosis review JSON gate: read ${controller.diagnosis_review_artifact_path}; require existing valid JSON with verdict APPROVE before fix rounds.`);
138
- lines.push(" If the review JSON is missing, invalid, or verdict is not APPROVE, stop immediately with the configured stop report; do NOT enter fix rounds.");
139
- lines.push(`7. For round 1..${controller.max_rounds}, set current_round to the loop number and run one fresh context fix round with sequential gates:`);
140
- lines.push(" a. Spawn fresh surgeon agent with controller_run_id and current_round, then wait for completion.");
141
- lines.push(" ```");
142
- lines.push(" Agent(");
143
- lines.push(` subagent_type="${surgeonAgent}",`);
144
- lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context round for $ARGUMENTS. ${escapePrompt(stepPrompt(fixBugsStep))}"`);
145
- lines.push(" )");
146
- lines.push(" ```");
147
- pushWorkflowStepVerifierLines(lines, fixBugsStep, " ");
148
- lines.push(" b. Fix handoff gate: validate the current round handoff and fix commit, or an explicit no-commit handoff for current_round, before review.");
149
- lines.push(" c. Spawn fresh fix_reviewer agent with controller_run_id, current_round, and expected review artifact path, then wait for completion.");
150
- lines.push(" ```");
151
- lines.push(" Agent(");
152
- lines.push(` subagent_type="${fixReviewerAgent}",`);
153
- lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>; expected_review_artifact=${controller.review_artifact_path}. Fresh context review for $ARGUMENTS. ${escapePrompt(stepPrompt(reviewChangesStep))}"`);
154
- lines.push(" )");
155
- lines.push(" ```");
156
- pushWorkflowStepVerifierLines(lines, reviewChangesStep, " ");
157
- lines.push(` d. Review JSON gate: read ${controller.review_artifact_path}; validate it exists, is valid JSON, module equals the safe module argument, round equals the provided current_round, run_id equals the provided controller_run_id, updated_at is present, and verdict is APPROVE or REQUEST_CHANGES before verification.`);
158
- lines.push(" e. Spawn fresh test_runner agent only after the review gate passes; include controller_run_id and current_round, then wait for completion.");
159
- lines.push(" ```");
160
- lines.push(" Agent(");
161
- lines.push(` subagent_type="${testRunnerAgent}",`);
162
- lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context verification for $ARGUMENTS. The progress JSON run_id must equal the provided controller_run_id and round must equal the provided current_round; do not use manual-run during controller execution. ${escapePrompt(stepPrompt(verifyReportStep))}"`);
163
- lines.push(" )");
164
- lines.push(" ```");
165
- pushWorkflowStepVerifierLines(lines, verifyReportStep, " ");
166
- lines.push(` f. Progress JSON read/provenance validation: read ${controller.progress_artifact_path}. The controller must read this JSON sidecar, not Markdown prose.`);
167
- lines.push(" g. Verify progress JSON provenance before branching:");
168
- for (const check of policy.progress_provenance_checks) {
169
- lines.push(` - ${check}`);
170
- }
171
- lines.push(` - ${policy.progress_provenance_failure}`);
172
- lines.push(" h. Branch only from the JSON verdict:");
173
- for (const branch of policy.verdict_branches) {
174
- lines.push(` - ${branch}`);
175
- }
176
- lines.push(`8. If max ${controller.max_rounds} rounds is reached, stop with the configured stop report.`);
177
- lines.push("</Steps>");
178
- lines.push("");
179
- lines.push("<Verdict_Mapping>");
180
- for (const line of policy.verdict_mapping) {
181
- lines.push(line);
182
- }
183
- lines.push("</Verdict_Mapping>");
184
- lines.push("");
185
- lines.push("<Stop_Report>");
186
- lines.push("Every critical decision in the stop report must cite at least one verifier result_id, trace_id, or ArtifactManifest artifact id; prose-only claims are not sufficient.");
187
- for (const line of policy.stop_report) {
188
- lines.push(line);
189
- }
190
- lines.push("</Stop_Report>");
51
+ lines.push("<Canonical_Run_Request>");
52
+ lines.push(JSON.stringify(params.request, null, 2));
53
+ lines.push("</Canonical_Run_Request>");
191
54
  lines.push("");
192
- lines.push("<Constraints>");
193
- for (const constraint of policy.constraints) {
194
- lines.push(`- ${constraint}`);
195
- }
196
- lines.push("</Constraints>");
55
+ lines.push("<Execution>");
56
+ lines.push("Execution authority belongs only to the canonical runtime plan and durable ledger.");
57
+ lines.push(params.acceptsArgument
58
+ ? "Replace the literal $ARGUMENTS values in the request with the invocation argument, then call `dna_run_start` exactly once unless this invocation already has a durable run_id."
59
+ : "Call `dna_run_start` with the request exactly once unless this invocation already has a durable run_id.");
60
+ lines.push("Keep the returned run_id. Use `dna_run_next` to obtain the next opaque runtime action; never infer execution order or domain transitions from this Skill text.");
61
+ lines.push("When the runtime returns an attempt, execute only its returned packet through the harness worker surface and use `dna_attempt_attach`, `dna_attempt_heartbeat`, and `dna_attempt_submit` with the returned durable identities.");
62
+ lines.push("When the runtime returns wait, preserve the run_id and report that the run is resumable. When it returns a terminal state, report the durable terminal reason and result references.");
63
+ lines.push("If a required harness capability is unavailable, preserve the typed unsupported result. Do not emulate the missing capability or fall back to prose scheduling.");
64
+ lines.push("</Execution>");
197
65
  lines.push("");
198
- let content = lines.join("\n") + "\n";
199
- if (controllerVariables) {
200
- for (const [key, value] of Object.entries(controllerVariables)) {
201
- content = content.replaceAll(`{{${key}}}`, value);
202
- }
203
- }
204
- const dirName = skillName;
205
- const fileName = join(dirName, "SKILL.md");
206
- return attachSkillBody({ name: skillName, fileName, dirName, content }, {
207
- description: controller.description || controller.name,
208
- triggers: [toKebabCase(controllerKey), `run ${toKebabCase(controllerKey)}`],
209
- kind: "controller",
210
- mapEntry: {
211
- name: skillName,
212
- dirName,
213
- source: controllerKey,
214
- kind: "controller",
215
- bodyPath: join(dirName, "skill-bodies", "body.md"),
216
- roles: Object.values(controller.roles),
217
- consumes: [
218
- { step_id: "controller", type: "file", path: controller.diagnosis_artifact_path, description: "Diagnosis artifact" },
219
- { step_id: "controller", type: "file", path: controller.diagnosis_review_artifact_path, description: "Diagnosis review artifact" },
220
- { step_id: "controller", type: "file", path: controller.review_artifact_path, description: "Fix review artifact" },
221
- { step_id: "controller", type: "file", path: controller.progress_artifact_path, description: "Progress JSON artifact" },
222
- ],
223
- produces: [
224
- { step_id: "controller", type: "file", path: controller.progress_artifact_path, description: "Progress JSON artifact" },
225
- ],
226
- },
227
- });
228
- }
229
- function requireStepByRole(workflow, role, workflowName) {
230
- const step = workflow?.steps.find((candidate) => candidate.role === role);
231
- if (!step) {
232
- throw new Error(`Controller workflow '${workflowName}' must include a step for role '${role}'`);
233
- }
234
- return step;
235
- }
236
- function pushWorkflowStepVerifierLines(lines, step, indent) {
237
- if (step.completion?.length) {
238
- lines.push(`${indent}Completion gates for workflow step '${step.id}' must pass in declaration order before treating the step as complete:`);
239
- for (let i = 0; i < step.completion.length; i++) {
240
- lines.push(`${indent} ${i + 1}. ${describeCompletionCheck(step.completion[i])}`);
241
- }
242
- }
243
- if (step.checkpoints?.length) {
244
- lines.push(`${indent}Checkpoint gates for workflow step '${step.id}' must pass in declaration order before transitioning:`);
245
- for (let i = 0; i < step.checkpoints.length; i++) {
246
- const checkpoint = step.checkpoints[i];
247
- const action = checkpoint.action ?? "block";
248
- const command = checkpoint.command ? `; command: ${checkpoint.command}` : "";
249
- lines.push(`${indent} ${i + 1}. ${checkpoint.assert}: ${checkpoint.message} (${action}${command})`);
250
- }
251
- }
252
- if (step.completion?.some((check) => check.command_success) || step.checkpoints?.some((check) => check.command)) {
253
- lines.push(`${indent}Command-backed completion/checkpoint gates must use runtime verifier_policy semantics: first authorize the declared raw command against exact allow_commands or allow_command_prefixes, then resolve templates safely, execute the exact resolved command, require exit code 0 for pass, treat policy denial as exit_code=126, and treat any non-zero exit code as blocking failure unless the checkpoint action is warn.`);
254
- }
255
- }
256
- function describeCompletionCheck(check) {
257
- if (check.file_exists)
258
- return `file_exists: ${check.file_exists}`;
259
- if (check.file_not_empty)
260
- return `file_not_empty: ${check.file_not_empty}`;
261
- if (check.file_contains)
262
- return `file_contains: ${check.file_contains.path} matches ${check.file_contains.pattern}`;
263
- if (check.command_success)
264
- return `command_success: execute exact command "${check.command_success}", require exit code 0, and block on any non-zero exit code`;
265
- return "invalid completion check: exactly one completion check field is required";
66
+ return lines.join("\n");
266
67
  }
267
68
  function createSkillLauncher(params) {
268
69
  const lines = [];
@@ -271,8 +72,9 @@ function createSkillLauncher(params) {
271
72
  lines.push(`description: "Use when user says /${params.skillName}. ${escapeYaml(params.description)}"`);
272
73
  lines.push("user-invocable: true");
273
74
  lines.push("triggers:");
274
- for (const trigger of params.triggers)
75
+ for (const trigger of params.triggers) {
275
76
  lines.push(` - "${escapeYaml(trigger)}"`);
77
+ }
276
78
  lines.push("---");
277
79
  lines.push("");
278
80
  lines.push(SENTINEL);
@@ -285,135 +87,40 @@ function createSkillLauncher(params) {
285
87
  lines.push("</Purpose>");
286
88
  lines.push("");
287
89
  lines.push("<Skill_Body>");
288
- lines.push(`Full ${params.kind} instructions live in \`skill-bodies/${params.bodyFileName}\`.`);
289
- lines.push("Read that body before executing any step; the launcher is only an entrypoint.");
90
+ lines.push(`Full ${params.kind} launcher request lives in \`skill-bodies/${params.bodyFileName}\`.`);
91
+ lines.push("Read that body before invoking the canonical runtime adapter.");
290
92
  lines.push("</Skill_Body>");
291
93
  lines.push("");
292
- if (params.bodyContent.includes("<Required_Context>")) {
293
- lines.push("<Required_Context>");
294
- lines.push("The skill body contains required context files. Read them before any Edit, Write, or Bash action.");
295
- lines.push("</Required_Context>");
296
- lines.push("");
297
- }
298
- if (params.bodyContent.includes("<Advisory_Context>")) {
299
- lines.push("<Advisory_Context>");
300
- lines.push("The skill body contains planning-only advisory context sources. Quote and cite them when used; they never satisfy handoff or enforcement checks.");
301
- lines.push("</Advisory_Context>");
302
- lines.push("");
303
- }
304
- if (params.bodyContent.includes("<Handoff>")) {
305
- lines.push("<Handoff>");
306
- lines.push("Write human-readable handoff notes when requested, but do not treat handoff.md as a machine fact.");
307
- lines.push("ArtifactManifest entries written by runtime hooks remain the machine source of truth for handoff satisfaction.");
308
- lines.push("</Handoff>");
309
- lines.push("");
310
- }
311
- lines.push("<Execution>");
312
- lines.push(`Follow \`skill-bodies/${params.bodyFileName}\` exactly.`);
313
- lines.push("</Execution>");
314
94
  return lines.join("\n") + "\n";
315
95
  }
316
96
  function attachSkillBody(result, params) {
317
97
  const bodyFileName = "body.md";
318
- const launcherContent = createSkillLauncher({
319
- skillName: result.name,
320
- description: params.description,
321
- triggers: params.triggers,
322
- bodyFileName,
323
- bodyContent: result.content,
324
- kind: params.kind,
325
- });
326
98
  return {
327
99
  ...result,
328
- launcherContent,
100
+ launcherContent: createSkillLauncher({
101
+ skillName: result.name,
102
+ description: params.description,
103
+ triggers: params.triggers,
104
+ bodyFileName,
105
+ kind: params.kind,
106
+ }),
329
107
  bodyContent: result.content,
330
108
  bodyFileName,
331
109
  mapEntry: params.mapEntry,
332
110
  };
333
111
  }
334
- function normalizeControllerPolicy(controller) {
335
- const defaults = defaultControllerPolicy(controller);
336
- const policy = {
337
- diagnosis_reuse_criteria: controller.policy?.diagnosis_reuse_criteria ?? defaults.diagnosis_reuse_criteria,
338
- progress_provenance_checks: controller.policy?.progress_provenance_checks ?? defaults.progress_provenance_checks,
339
- progress_provenance_failure: controller.policy?.progress_provenance_failure ?? defaults.progress_provenance_failure,
340
- verdict_branches: controller.policy?.verdict_branches ?? defaults.verdict_branches,
341
- verdict_mapping: controller.policy?.verdict_mapping ?? defaults.verdict_mapping,
342
- stop_report: controller.policy?.stop_report ?? defaults.stop_report,
343
- constraints: controller.policy?.constraints ?? defaults.constraints,
344
- };
112
+ function controllerSkillMapEntry(skillName, dirName, controllerKey, controller) {
345
113
  return {
346
- diagnosis_reuse_criteria: renderControllerPolicyLines(policy.diagnosis_reuse_criteria, controller),
347
- progress_provenance_checks: renderControllerPolicyLines(policy.progress_provenance_checks, controller),
348
- progress_provenance_failure: renderControllerPolicyText(policy.progress_provenance_failure, controller),
349
- verdict_branches: renderControllerPolicyLines(policy.verdict_branches, controller),
350
- verdict_mapping: renderControllerPolicyLines(policy.verdict_mapping, controller),
351
- stop_report: renderControllerPolicyLines(policy.stop_report, controller),
352
- constraints: renderControllerPolicyLines(policy.constraints, controller),
353
- };
354
- }
355
- function renderControllerPolicyLines(lines, controller) {
356
- return lines.map((line) => renderControllerPolicyText(line, controller));
357
- }
358
- function renderControllerPolicyText(text, controller) {
359
- const replacements = {
360
- "$progress_artifact_path": controller.progress_artifact_path,
361
- "$diagnosis_artifact_path": controller.diagnosis_artifact_path ?? "",
362
- "$diagnosis_review_artifact_path": controller.diagnosis_review_artifact_path ?? "",
363
- "$review_artifact_path": controller.review_artifact_path ?? "",
364
- };
365
- let rendered = text;
366
- for (const [token, value] of Object.entries(replacements)) {
367
- rendered = rendered.replaceAll(token, value);
368
- }
369
- return rendered;
370
- }
371
- function defaultControllerPolicy(controller) {
372
- return {
373
- diagnosis_reuse_criteria: [
374
- `${controller.diagnosis_artifact_path} exists`,
375
- `${controller.diagnosis_review_artifact_path} exists and verdict is APPROVE`,
376
- "the user did not explicitly request reanalysis",
377
- "progress JSON does not invalidate the diagnosis",
378
- ],
379
- progress_provenance_checks: [
380
- "module equals the safe module argument",
381
- "round equals the provided current_round",
382
- "run_id equals the provided controller_run_id",
383
- `diagnosis_artifact equals ${controller.diagnosis_artifact_path}`,
384
- `review_artifact equals ${controller.review_artifact_path}`,
385
- "git_head or last_fix_commit references the current round's fix commit when available",
386
- "updated_at is not earlier than the current round start time",
387
- ],
388
- progress_provenance_failure: "if provenance is missing, stale, or mismatched, stop with Verdict BLOCKED and failure_reason PROGRESS_PROVENANCE_MISMATCH",
389
- verdict_branches: [
390
- "PASS -> success stop",
391
- "CONTINUE -> next fresh context fix round",
392
- "REQUEST_CHANGES -> stop with the configured stop report",
393
- "BLOCKED -> stop with the configured stop report",
394
- ],
395
- verdict_mapping: [
396
- "PASS / CONTINUE / REQUEST_CHANGES / BLOCKED mapping:",
397
- "- PASS: success stop",
398
- "- CONTINUE: start the next fresh context round",
399
- "- REQUEST_CHANGES: stop and report the requested changes",
400
- "- BLOCKED: stop and report the blocker",
401
- "- MAX_ROUNDS: stop and report max rounds reached",
402
- ],
403
- stop_report: [
404
- "On REQUEST_CHANGES, BLOCKED, or MAX_ROUNDS, explain the stop reason, the evidence, the next action, and cite at least one verifier result_id, trace_id, or ArtifactManifest artifact id.",
405
- ],
406
- constraints: [
407
- `The controller must schedule only from ${controller.progress_artifact_path}.`,
408
- "Branch only from the progress JSON verdict.",
409
- ],
114
+ name: skillName,
115
+ dirName,
116
+ source: controllerKey,
117
+ kind: "controller",
118
+ bodyPath: `${dirName}/skill-bodies/body.md`,
119
+ roles: Object.values(controller.roles),
120
+ consumes: [],
121
+ produces: [],
410
122
  };
411
123
  }
412
- // ── Compile Workflow → Skill ───────────────────────────────
413
- /**
414
- * Compile a WorkflowPlan + roles + IR into a SKILL.md file.
415
- * Variables from DNA config are substituted into prompts and descriptions.
416
- */
417
124
  function workflowSkillMapEntry(skillName, dirName, plan) {
418
125
  const consumes = [];
419
126
  const produces = [];
@@ -444,399 +151,58 @@ function workflowSkillMapEntry(skillName, dirName, plan) {
444
151
  dirName,
445
152
  source: plan.workflow_key ?? plan.source_workflow,
446
153
  kind: "workflow",
447
- bodyPath: join(dirName, "skill-bodies", "body.md"),
154
+ bodyPath: `${dirName}/skill-bodies/body.md`,
448
155
  roles: [...new Set(plan.steps.map((step) => step.role))],
449
156
  consumes,
450
157
  produces,
451
158
  };
452
159
  }
160
+ export function compileControllerToSkill(controllerKey, controller, _variables, _workflows, options) {
161
+ const skillName = `dna-${toKebabCase(controllerKey)}`;
162
+ assertSafeGeneratedName(skillName, "skill name");
163
+ const description = controller.description || controller.name;
164
+ const dirName = skillName;
165
+ const content = canonicalSkillBody({
166
+ skillName,
167
+ description,
168
+ request: canonicalSkillRequest("controller", controllerKey, true, options),
169
+ acceptsArgument: true,
170
+ });
171
+ return attachSkillBody({ name: skillName, fileName: join(dirName, "SKILL.md"), dirName, content }, {
172
+ description,
173
+ triggers: [toKebabCase(controllerKey), `run ${toKebabCase(controllerKey)}`],
174
+ kind: "controller",
175
+ mapEntry: controllerSkillMapEntry(skillName, dirName, controllerKey, controller),
176
+ });
177
+ }
453
178
  export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
454
- const lines = [];
455
179
  const skillName = `dna-${toKebabCase(plan.name)}`;
456
- const surface = options?.surface ?? "claude_code";
457
- const workflowId = plan.workflow_key ?? plan.source_workflow;
458
- const skillVariables = variables ? resolveWorkflowVariableReferences(variables) : undefined;
459
- const workflowVerifierSpecs = ir?.verifier_specs?.filter((spec) => spec.workflow_name === workflowId) ?? [];
180
+ assertSafeGeneratedName(skillName, "skill name");
181
+ const description = plan.description || plan.name;
182
+ const workflowKey = plan.workflow_key ?? plan.source_workflow;
183
+ const skillVariables = variables
184
+ ? resolveWorkflowVariableReferences(variables)
185
+ : undefined;
186
+ const workflowVerifierSpecs = ir?.verifier_specs?.filter((spec) => spec.workflow_name === workflowKey) ?? [];
460
187
  const rolePostChecks = [...new Set(plan.steps.map((step) => step.role))]
461
188
  .flatMap((roleName) => roles[roleName]?.post_checks ?? []);
462
- const usesArguments = workflowUsesArguments(plan, skillVariables) ||
463
- valueUsesArguments(workflowVerifierSpecs, skillVariables) ||
464
- valueUsesArguments(rolePostChecks, skillVariables);
465
- assertSafeGeneratedName(skillName, "skill name");
466
- // Frontmatter
467
- lines.push("---");
468
- lines.push(`name: ${skillName}`);
469
- lines.push(`description: "Use when user says /${skillName}. ${escapeYaml(plan.description || plan.name)}"`);
470
- lines.push("user-invocable: true");
471
- lines.push("triggers:");
472
- lines.push(` - "${plan.name}"`);
473
- lines.push(` - "run ${plan.name}"`);
474
- lines.push("---");
475
- lines.push("");
476
- // Sentinel
477
- lines.push(SENTINEL);
478
- lines.push(`<!-- Compiled: ${new Date().toISOString()} -->`);
479
- lines.push("");
480
- // Title
481
- lines.push(`# /${skillName}${usesArguments ? " <argument>" : ""}`);
482
- lines.push("");
483
- // Purpose
484
- if (plan.description) {
485
- lines.push("<Purpose>");
486
- lines.push(plan.description);
487
- lines.push("</Purpose>");
488
- lines.push("");
489
- }
490
- lines.push("<Workflow_State>");
491
- lines.push("At workflow start, call the MCP tool `dna_workflow_write` before any step or hook-triggering action so hook verifier evidence is attributed to this workflow instead of `*:direct`.");
492
- lines.push(`Use workflow id \`${workflowId}\`.`);
493
- lines.push("The MCP server initializes workflow_asset and resolved_variables from the compiled workflow runtime manifest. Do not submit either field from Skill text.");
494
- const firstStep = plan.steps[0];
495
- lines.push(`Initial state write: workflow=${workflowId}, current_step=${firstStep.id}, current_role=${firstStep.role}, iteration=1, active=true.`);
496
- if (usesArguments) {
497
- lines.push("Record the invocation argument as inputs.ARGUMENTS so handoff artifact paths like $ARGUMENTS can be resolved deterministically.");
498
- }
499
- else {
500
- lines.push("This workflow has no invocation argument; omit inputs.ARGUMENTS from workflow state.");
501
- }
502
- lines.push(`Before each workflow step, call \`dna_workflow_write\` again with workflow, current_step, current_role, iteration, active=true${usesArguments ? ", and inputs.ARGUMENTS" : ""}.`);
503
- lines.push("At workflow completion or terminal failure, call `dna_workflow_write` with active=false using the same workflow id.");
504
- lines.push("</Workflow_State>");
505
- lines.push("");
506
- // Required Context — context files that must be read before any work
507
- const workflowKeys = [plan.workflow_key, plan.source_workflow, plan.name].filter((key, index, keys) => Boolean(key) && keys.indexOf(key) === index);
508
- const requiredReadAssets = [];
509
- if (ir?.legibility_assets) {
510
- const assets = ir.legibility_assets;
511
- for (const asset of assets.mandatory ?? []) {
512
- if (asset.type === "required_read" && !requiredReadAssets.includes(asset.path))
513
- requiredReadAssets.push(asset.path);
514
- }
515
- for (const workflowKey of workflowKeys) {
516
- for (const asset of assets.per_workflow?.[workflowKey] ?? []) {
517
- if (asset.type === "required_read" && !requiredReadAssets.includes(asset.path))
518
- requiredReadAssets.push(asset.path);
519
- }
520
- }
521
- for (const step of plan.steps) {
522
- for (const asset of assets.per_role?.[step.role] ?? []) {
523
- if (asset.type === "required_read" && !requiredReadAssets.includes(asset.path))
524
- requiredReadAssets.push(asset.path);
525
- }
526
- }
527
- }
528
- const contextFiles = requiredReadAssets.length > 0 ? requiredReadAssets : (() => {
529
- if (!ir?.context_files)
530
- return [];
531
- const files = [];
532
- if (ir.context_files.mandatory)
533
- files.push(...ir.context_files.mandatory);
534
- for (const workflowKey of workflowKeys) {
535
- const workflowFiles = ir.context_files.per_workflow?.[workflowKey] ?? [];
536
- for (const file of workflowFiles) {
537
- if (!files.includes(file))
538
- files.push(file);
539
- }
540
- }
541
- if (ir.context_files.per_role) {
542
- for (const step of plan.steps) {
543
- const roleFiles = ir.context_files.per_role[step.role];
544
- if (roleFiles) {
545
- for (const f of roleFiles) {
546
- if (!files.includes(f))
547
- files.push(f);
548
- }
549
- }
550
- }
551
- }
552
- return files;
553
- })();
554
- if (contextFiles.length > 0) {
555
- lines.push("<Required_Context>");
556
- lines.push("Your context is EMPTY at startup. You MUST read these files first:");
557
- lines.push("");
558
- for (let i = 0; i < contextFiles.length; i++) {
559
- lines.push(`${i + 1}. ${contextFiles[i]}`);
560
- }
561
- lines.push("");
562
- lines.push("Without reading these, you cannot perform your role.");
563
- lines.push("Hook will warn on first Edit/Bash/Write until these are read.");
564
- lines.push("</Required_Context>");
565
- lines.push("");
566
- }
567
- if (ir?.context_sources?.length) {
568
- lines.push("<Advisory_Context>");
569
- lines.push("These sources are planning-only references. They do not satisfy ArtifactManifest, handoff consumes, completion checks, or enforcement facts.");
570
- lines.push("If you use them, quote the relevant passage and cite the source id.");
571
- lines.push("");
572
- for (const source of ir.context_sources) {
573
- const locator = source.path ?? source.url ?? "(no locator)";
574
- const title = source.title ? ` — ${source.title}` : "";
575
- lines.push(`- ${source.id}: ${source.type} ${locator}${title}`);
576
- }
577
- for (const instruction of ir.planning_context?.instructions ?? []) {
578
- lines.push(`- Planning instruction: ${instruction}`);
579
- }
580
- lines.push("</Advisory_Context>");
581
- lines.push("");
582
- }
583
- // Steps — agent-owned steps are delegated; human-owned steps stay in the main session.
584
- const usedRoles = new Set(plan.steps.map((s) => s.role));
585
- const hasHumanSteps = plan.steps.some((step) => isHumanRole(roles[step.role]));
586
- const hasInteractiveSteps = plan.steps.some((step) => isInteractiveRole(roles[step.role]));
587
- // Pre-compute handoff targets: steps that are pointed to by another step's handoff_to
588
- const handoffTargets = new Set();
589
- for (const step of plan.steps) {
590
- if (step.handoff_to)
591
- handoffTargets.add(step.handoff_to);
592
- }
593
- lines.push("<Steps>");
594
- let stepNumber = 0;
595
- for (let i = 0; i < plan.parallel_groups.length; i++) {
596
- const group = plan.parallel_groups[i];
597
- for (const stepId of group.step_ids) {
598
- stepNumber++;
599
- const step = plan.steps.find((s) => s.id === stepId);
600
- const agentType = `dna-${toKebabCase(step.role)}`;
601
- const humanOwned = isHumanRole(roles[step.role]);
602
- const interactiveOwned = isInteractiveRole(roles[step.role]);
603
- const runIf = step.run_if ? `\n **Condition**: ${step.run_if}` : "";
604
- const optional = step.optional ? " (optional)" : "";
605
- // Build agent prompt: anti-recursion + role identity + task
606
- const promptParts = [
607
- "Do NOT spawn sub-agents.",
608
- `You are the ${agentType} agent.`,
609
- ];
610
- // Reflection gate: inject Previous_Attempts for handoff target steps
611
- if (handoffTargets.has(step.id)) {
612
- promptParts.push("<Previous_Attempts>\\n" +
613
- "If this is not the first round, the workflow state contains an experience_chain " +
614
- "with previous analysis, what surgeon tried, results, and lessons learned. " +
615
- "Read the experience chain from workflow state before analyzing. " +
616
- "DO NOT repeat approaches that already failed.\\n" +
617
- "</Previous_Attempts>");
618
- }
619
- // Reflection gate: inject Failed_Approaches for steps with max_attempts + handoff_to
620
- if (step.max_attempts && step.handoff_to) {
621
- promptParts.push("<Failed_Approaches>\\n" +
622
- "If previous attempts exist in the experience chain, review what was tried " +
623
- "and why it failed. You MUST use a DIFFERENT approach.\\n" +
624
- "</Failed_Approaches>");
625
- }
626
- promptParts.push(step.prompt || step.description);
627
- const agentPrompt = escapePrompt(promptParts.join(" "));
628
- lines.push(`${stepNumber}. **${step.id}**${optional}`);
629
- lines.push(` ${step.description}${runIf}`);
630
- lines.push(` Before this step, call \`dna_workflow_write\` with workflow=${workflowId}, current_step=${step.id}, current_role=${step.role}, iteration=<current iteration>, active=true${usesArguments ? ", and inputs.ARGUMENTS" : ""}.`);
631
- lines.push("");
632
- if (humanOwned) {
633
- lines.push(" **Human-owned gate (`model: human`)**");
634
- lines.push(` Ask the user in the main session: ${step.prompt || step.description}`);
635
- lines.push(" Do not delegate this decision, infer approval, or continue from silence. Continue only after an explicit human response.");
636
- }
637
- else if (interactiveOwned) {
638
- lines.push(" **Main-session external-agent step (`model: interactive`)**");
639
- lines.push(` Perform directly in the main session: ${step.prompt || step.description}`);
640
- lines.push(" This step may interview the user or edit the candidate YAML inside its declared scope. Do not delegate away the conversation context.");
641
- }
642
- else if (surface === "codex_cli") {
643
- lines.push(" Delegate to a Codex native subagent with this role contract; do not discard the role boundaries:");
644
- lines.push(" ```text");
645
- lines.push(` role: ${agentType}`);
646
- lines.push(` prompt: ${agentPrompt}`);
647
- lines.push(" ```");
648
- lines.push(" Wait for the subagent to complete and read its output before proceeding.");
649
- }
650
- else {
651
- lines.push(` Execute with Agent tool — DO NOT perform this work yourself:`);
652
- lines.push(` \`\`\``);
653
- lines.push(` Agent(`);
654
- lines.push(` subagent_type="${agentType}",`);
655
- lines.push(` prompt="${agentPrompt}"`);
656
- lines.push(` )`);
657
- lines.push(` \`\`\``);
658
- lines.push(` Wait for agent to complete. Read the output before proceeding.`);
659
- }
660
- if (step.handoff?.produces && step.handoff.produces.length > 0) {
661
- const artifacts = step.handoff.produces.map(p => p.description).join(", ");
662
- lines.push(` Verify produced artifacts: ${artifacts}`);
663
- }
664
- pushWorkflowStepVerifierLines(lines, step, " ");
665
- if (step.completion?.length || step.checkpoints?.length) {
666
- lines.push(" If any completion/checkpoint gate fails, preserve the exact verifier diagnostics verbatim: id, check/assert or command, message, evidence, target/artifact, and exit_code when available.");
667
- lines.push(` Feed those exact diagnostics to ${step.on_fail === "retry_with_feedback" ? "this step's on_fail retry" : "the retry_from/on_fail step"} before any retry, reanalysis, or downstream transition.`);
668
- }
669
- lines.push("");
670
- // Reflection gate: inject Reflection_Gate after steps with max_attempts
671
- if (step.max_attempts) {
672
- const handoffStep = step.handoff_to ?? "previous step";
673
- const maxH = step.max_handoffs ?? 3;
674
- const blockedPath = step.blocked_items_path ?? "blocked_items.md";
675
- lines.push(`<Reflection_Gate>`);
676
- lines.push(`max_attempts=${step.max_attempts}, handoff_to=${handoffStep}, max_handoffs=${maxH}`);
677
- lines.push(`No declared completion progress after ${step.max_attempts} attempts → handoff to ${handoffStep} for re-analysis.`);
678
- lines.push(`When the declared gates are test or no_test_regression gates, preserve the existing test-progress meaning as part of declared completion progress.`);
679
- lines.push(`After ${maxH} handoffs with no declared completion progress → SKIP and record to ${blockedPath}.`);
680
- lines.push(`</Reflection_Gate>`);
681
- lines.push("");
682
- }
683
- }
684
- }
685
- lines.push("</Steps>");
686
- lines.push("");
687
- if (plan.retry.max_retries > 0) {
688
- lines.push("<Retry_Policy>");
689
- lines.push(`- max_retries: ${plan.retry.max_retries}`);
690
- lines.push(`- retry_from: ${plan.retry.retry_from ?? "first_failed_step"}`);
691
- lines.push(`- backoff: ${plan.retry.backoff}`);
692
- lines.push("- On a blocking completion, checkpoint, or validator failure, preserve the exact diagnostics and return them verbatim to the retry_from/on_fail step.");
693
- lines.push("- Exact diagnostics means every available id/result_id, check/assert or command, message, evidence, target/artifact, and exit_code. Do not paraphrase away failed paths, command output, or policy-denied exit codes.");
694
- lines.push("- Retry prompts must include the prior failed step id, the declared gates that failed, and the exact verifier diagnostics before asking for another attempt.");
695
- lines.push("- Repeat only the retry slice and its dependent steps; do not continue to save, adoption, or sync while validation is red.");
696
- lines.push("- Stop and report the remaining diagnostics when the retry budget is exhausted.");
697
- lines.push("</Retry_Policy>");
698
- lines.push("");
699
- }
700
- // Execution Policy — force delegation while preserving explicit human gates.
701
- lines.push("<Execution_Policy>");
702
- if (surface === "codex_cli") {
703
- lines.push("- **CRITICAL**: Delegate each delegated-agent step to a Codex native subagent using the specified role contract. Do not collapse distinct roles into an unreviewed single pass.");
704
- }
705
- else {
706
- lines.push(hasHumanSteps || hasInteractiveSteps
707
- ? "- **CRITICAL**: Each delegated-agent step MUST be executed by spawning an Agent using the Agent tool with the specified subagent_type. DO NOT perform delegated-agent work directly in the main session."
708
- : "- **CRITICAL**: Each step MUST be executed by spawning an Agent using the Agent tool with the specified subagent_type. DO NOT perform any step's work directly in the main session.");
709
- }
710
- lines.push("- Use the task tracker to track each step only as pending/in_progress/completed; use deleted only for skipped dependent tasks.");
711
- lines.push(hasHumanSteps
712
- ? "- Proceed automatically between non-human steps, but stop at every `model: human` step and require an explicit user response before continuing."
713
- : "- After completing each step, immediately proceed to the next — do not stop, summarize, or wait for confirmation.");
714
- if (hasInteractiveSteps) {
715
- lines.push("- Execute `model: interactive` steps in the main session so interview answers and candidate edits remain in the user-visible conversation.");
716
- }
717
- lines.push("- If a step cannot complete, do not use a failed status. Keep the current task in_progress or record the blocker in the task description/output, delete skipped dependent tasks, and continue only to non-dependent steps.");
718
- lines.push(hasHumanSteps
719
- ? "- Never auto-approve, delegate, or synthesize a human-owned decision."
720
- : "- Do not ask the user for permission between steps — the workflow is pre-approved.");
721
- lines.push("- Wait for each delegated agent to complete and read its output before proceeding to the next step.");
722
- lines.push("</Execution_Policy>");
723
- lines.push("");
724
- // Tool_Usage — only if roles have permissions or scope
725
- const toolUsageLines = [];
726
- for (const roleName of usedRoles) {
727
- const role = roles[roleName];
728
- if (!role)
729
- continue;
730
- const allowed = role.tool_permissions?.allow?.join(", ") || "all";
731
- const denied = role.tool_permissions?.deny?.join(", ") || "none";
732
- const writeScope = role.scope?.write?.join(", ") || "none";
733
- toolUsageLines.push(`### ${roleName}`);
734
- toolUsageLines.push(`- Owner: ${isHumanRole(role) ? "human" : isInteractiveRole(role) ? "main_session_external_agent" : "delegated_agent"}`);
735
- toolUsageLines.push(`- Description: ${role.description}`);
736
- toolUsageLines.push(`- Allowed: ${allowed}`);
737
- toolUsageLines.push(`- Denied: ${denied}`);
738
- toolUsageLines.push(`- Write scope: ${writeScope}`);
739
- for (const instruction of role.instructions ?? [])
740
- toolUsageLines.push(`- Instruction: ${instruction}`);
741
- for (const criterion of role.success_criteria ?? [])
742
- toolUsageLines.push(`- Success criterion: ${criterion}`);
743
- }
744
- if (toolUsageLines.length > 0) {
745
- lines.push("<Tool_Usage>");
746
- lines.push(...toolUsageLines);
747
- lines.push("</Tool_Usage>");
748
- lines.push("");
749
- }
750
- // Constraints — IR prompt_directives grouped by priority
751
- if (ir && ir.prompt_directives.length > 0) {
752
- lines.push("<Constraints>");
753
- const high = ir.prompt_directives.filter((d) => d.priority === "high");
754
- const medium = ir.prompt_directives.filter((d) => d.priority === "medium");
755
- const low = ir.prompt_directives.filter((d) => d.priority !== "high" && d.priority !== "medium");
756
- if (high.length > 0) {
757
- lines.push("## Critical");
758
- for (const d of high) {
759
- lines.push(`- **${d.text}**`);
760
- }
761
- }
762
- if (medium.length > 0) {
763
- lines.push("## Standard");
764
- for (const d of medium) {
765
- lines.push(`- ${d.text}`);
766
- }
767
- }
768
- if (low.length > 0) {
769
- lines.push("## Other");
770
- for (const d of low) {
771
- lines.push(`- ${d.text}`);
772
- }
773
- }
774
- lines.push("</Constraints>");
775
- lines.push("");
776
- }
777
- // Checkpoints — steps that have checkpoint conditions
778
- const stepsWithCheckpoints = plan.steps.filter((s) => s.checkpoints && s.checkpoints.length > 0);
779
- if (stepsWithCheckpoints.length > 0) {
780
- lines.push("<Checkpoints>");
781
- for (const step of stepsWithCheckpoints) {
782
- lines.push(`### ${step.id}`);
783
- for (const cp of step.checkpoints) {
784
- const action = cp.action || "block";
785
- lines.push(`- ${cp.assert}: ${cp.message} (${action})`);
786
- if (cp.command)
787
- lines.push(` command: ${cp.command}`);
788
- }
789
- }
790
- lines.push("</Checkpoints>");
791
- lines.push("");
792
- }
793
- // Handoff — artifact flow between steps
794
- const stepsWithHandoff = plan.steps.filter((s) => s.handoff && (s.handoff.consumes?.length || s.handoff.produces?.length));
795
- if (stepsWithHandoff.length > 0) {
796
- lines.push("<Handoff>");
797
- lines.push("Human-readable handoff notes are useful for review, but they do not satisfy machine handoff gates.");
798
- lines.push("ArtifactManifest records written by runtime hooks remain the machine source of truth for produced/consumed artifacts.");
799
- for (const step of stepsWithHandoff) {
800
- const h = step.handoff;
801
- if (h.produces && h.produces.length > 0) {
802
- for (const p of h.produces) {
803
- lines.push(` Step ${step.id} produces: ${p.description} (${p.type}${p.path ? `, ${p.path}` : ""})`);
804
- }
805
- }
806
- if (h.consumes && h.consumes.length > 0) {
807
- for (const c of h.consumes) {
808
- const fromNote = c.from ? ` from ${c.from}` : "";
809
- lines.push(` Step ${step.id} consumes: ${c.description}${fromNote} (${c.type}${c.path ? `, ${c.path}` : ""})`);
810
- }
811
- }
812
- }
813
- lines.push("</Handoff>");
814
- lines.push("");
815
- }
816
- // Workflow Boundary — prevent cross-workflow execution
817
- lines.push("<Workflow_Boundary>");
818
- lines.push(`Before reporting final success, terminal failure, or exhaustion, call \`dna_workflow_write\` with workflow=${workflowId}, current_step=workflow_complete, current_role=workflow, active=false.`);
819
- lines.push("This workflow is COMPLETE. Do NOT proceed to any other workflow.");
820
- lines.push("Report your results and STOP. The user will decide the next step.");
821
- lines.push("</Workflow_Boundary>");
822
- lines.push("");
823
- let content = lines.join("\n") + "\n";
824
- // Substitute project variables: {{var_name}} → value
825
- if (skillVariables) {
826
- for (const [key, value] of Object.entries(skillVariables)) {
827
- content = content.replaceAll(`{{${key}}}`, value);
828
- }
829
- }
189
+ const acceptsArgument = workflowUsesArguments(plan, skillVariables)
190
+ || valueUsesArguments(workflowVerifierSpecs, skillVariables)
191
+ || valueUsesArguments(rolePostChecks, skillVariables);
830
192
  const dirName = skillName;
831
- const fileName = join(dirName, "SKILL.md");
832
- return attachSkillBody({ name: skillName, fileName, dirName, content }, {
833
- description: plan.description || plan.name,
193
+ const content = canonicalSkillBody({
194
+ skillName,
195
+ description,
196
+ request: canonicalSkillRequest("workflow", workflowKey, acceptsArgument, options),
197
+ acceptsArgument,
198
+ });
199
+ return attachSkillBody({ name: skillName, fileName: join(dirName, "SKILL.md"), dirName, content }, {
200
+ description,
834
201
  triggers: [plan.name, `run ${plan.name}`],
835
202
  kind: "workflow",
836
203
  mapEntry: workflowSkillMapEntry(skillName, dirName, plan),
837
204
  });
838
205
  }
839
- // ── File Operations ────────────────────────────────────────
840
206
  export async function writeSkillFiles(results, outputDir) {
841
207
  const written = [];
842
208
  const mapEntries = [];
@@ -884,19 +250,11 @@ export async function removeSkillFiles(outputDir) {
884
250
  }
885
251
  }
886
252
  catch {
887
- // Skip
253
+ // Ignore unmanaged or incomplete entries.
888
254
  }
889
255
  }
890
256
  return removed;
891
257
  }
892
- function escapeYaml(s) {
893
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
894
- }
895
- /** Escape prompt text for Agent() call template inside markdown code block. */
896
- function escapePrompt(s) {
897
- return s
898
- .replace(/\\/g, "\\\\")
899
- .replace(/"/g, '\\"')
900
- .replace(/`{3,}/g, "` ` `")
901
- .replace(/\n/g, "\\n");
258
+ function escapeYaml(value) {
259
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
902
260
  }