intentdna 1.6.5 → 1.7.0
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/compile.js +2 -47
- package/dist/cli/commands/context.d.ts +8 -0
- package/dist/cli/commands/context.js +63 -0
- package/dist/cli/commands/feedback.d.ts +1 -0
- package/dist/cli/commands/feedback.js +11 -0
- package/dist/cli/commands/init.js +11 -63
- package/dist/cli/commands/run.js +5 -4
- package/dist/cli/commands/show.js +2 -38
- package/dist/cli/commands/sync.d.ts +9 -6
- package/dist/cli/commands/sync.js +186 -184
- package/dist/cli/commands/templates.d.ts +10 -1
- package/dist/cli/commands/templates.js +50 -1
- package/dist/cli/commands/validate.js +15 -9
- package/dist/cli/commands/verify.js +97 -25
- package/dist/cli/index.js +76 -11
- package/dist/compiler/cascade.d.ts +3 -1
- package/dist/compiler/cascade.js +51 -0
- package/dist/compiler/compile.js +37 -0
- package/dist/compiler/diagnostics.d.ts +17 -0
- package/dist/compiler/diagnostics.js +30 -0
- package/dist/compiler/index.d.ts +3 -0
- package/dist/compiler/index.js +8 -11
- package/dist/compiler/input-resolver.d.ts +25 -0
- package/dist/compiler/input-resolver.js +175 -0
- package/dist/hooks/cli.d.ts +10 -1
- package/dist/hooks/cli.js +37 -22
- package/dist/hooks/state.d.ts +2 -0
- package/dist/hooks/state.js +23 -2
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-compile.js +18 -49
- package/dist/mcp/tools-context.d.ts +2 -0
- package/dist/mcp/tools-context.js +85 -0
- package/dist/mcp/tools-enforce.d.ts +2 -2
- package/dist/mcp/tools-enforce.js +19 -49
- package/dist/mcp/tools-observability.js +24 -0
- package/dist/report/kernel-signals.js +3 -0
- package/dist/report/report-package.d.ts +56 -0
- package/dist/report/report-package.js +85 -0
- package/dist/runtime/agent-md.d.ts +1 -0
- package/dist/runtime/agent-md.js +21 -3
- package/dist/runtime/context-sources.d.ts +14 -0
- package/dist/runtime/context-sources.js +60 -0
- package/dist/runtime/skill-adapter.d.ts +32 -4
- package/dist/runtime/skill-adapter.js +184 -9
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/schema/types.d.ts +33 -0
- package/dist/schema/validate.js +156 -2
- package/dist/schema/validators/controllers.js +16 -0
- package/dist/signals/index.d.ts +10 -0
- package/dist/signals/index.js +90 -5
- package/dist/templates/catalog.d.ts +19 -0
- package/dist/templates/catalog.js +57 -0
- package/dist/templates/flutter-rewrite.dna.yaml +2 -2
- package/package.json +1 -1
- package/spec/foundation-hardening.md +2 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { compileFromFiles, detectDNAConfigs, expandDNAInputFiles } from "../compiler/index.js";
|
|
4
|
+
export async function loadContextSourceIndex(projectDir, files) {
|
|
5
|
+
const inputFiles = files && files.length > 0
|
|
6
|
+
? files.map((file) => resolve(projectDir, file))
|
|
7
|
+
: await detectDNAConfigs(projectDir);
|
|
8
|
+
if (inputFiles.length === 0)
|
|
9
|
+
return { sources: [], planning_instructions: [] };
|
|
10
|
+
const expandedFiles = await expandDNAInputFiles(inputFiles);
|
|
11
|
+
const ir = await compileFromFiles(expandedFiles);
|
|
12
|
+
return {
|
|
13
|
+
sources: ir.context_sources ?? [],
|
|
14
|
+
planning_instructions: ir.planning_context?.instructions ?? [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function searchContextSources(index, query) {
|
|
18
|
+
const normalized = query.trim().toLowerCase();
|
|
19
|
+
if (!normalized)
|
|
20
|
+
return index.sources;
|
|
21
|
+
return index.sources.filter((source) => {
|
|
22
|
+
const haystack = [
|
|
23
|
+
source.id,
|
|
24
|
+
source.type,
|
|
25
|
+
source.path,
|
|
26
|
+
source.url,
|
|
27
|
+
source.title,
|
|
28
|
+
source.description,
|
|
29
|
+
...(source.tags ?? []),
|
|
30
|
+
].filter(Boolean).join("\n").toLowerCase();
|
|
31
|
+
return haystack.includes(normalized);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function findContextSource(index, id) {
|
|
35
|
+
return index.sources.find((source) => source.id === id);
|
|
36
|
+
}
|
|
37
|
+
export async function readContextSourceDocument(projectDir, source) {
|
|
38
|
+
if (source.type !== "file" && source.type !== "doc") {
|
|
39
|
+
return { source };
|
|
40
|
+
}
|
|
41
|
+
if (!source.path)
|
|
42
|
+
return { source };
|
|
43
|
+
if (isAbsolute(source.path) || source.path.split(/[\\/]+/).includes("..")) {
|
|
44
|
+
throw new Error(`Context source '${source.id}' path must stay within the project`);
|
|
45
|
+
}
|
|
46
|
+
const projectRoot = await realpath(projectDir);
|
|
47
|
+
const filePath = await realpath(resolve(projectRoot, source.path));
|
|
48
|
+
const rel = relative(projectRoot, filePath);
|
|
49
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
50
|
+
throw new Error(`Context source '${source.id}' path must stay within the project`);
|
|
51
|
+
}
|
|
52
|
+
const content = await readFile(filePath, "utf-8");
|
|
53
|
+
return { source, content };
|
|
54
|
+
}
|
|
55
|
+
export function formatContextSource(source) {
|
|
56
|
+
const locator = source.path ?? source.url ?? "(no locator)";
|
|
57
|
+
const title = source.title ? ` — ${source.title}` : "";
|
|
58
|
+
const tags = source.tags?.length ? ` [${source.tags.join(", ")}]` : "";
|
|
59
|
+
return `${source.id}: ${source.type} ${locator}${title}${tags}`;
|
|
60
|
+
}
|
|
@@ -7,17 +7,45 @@
|
|
|
7
7
|
* Skills are compiled views of DNA's structured definitions — not stored content.
|
|
8
8
|
*/
|
|
9
9
|
import type { WorkflowPlan, WorkflowDef, RoleDef, ConstraintIR, ControllerDef } from "../schema/types.js";
|
|
10
|
+
export interface SkillFile {
|
|
11
|
+
relativePath: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SkillMapEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
dirName: string;
|
|
17
|
+
source: string;
|
|
18
|
+
kind: "workflow" | "controller";
|
|
19
|
+
bodyPath: string;
|
|
20
|
+
roles: string[];
|
|
21
|
+
consumes: Array<{
|
|
22
|
+
step_id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
path?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
artifact_id?: string;
|
|
27
|
+
description: string;
|
|
28
|
+
}>;
|
|
29
|
+
produces: Array<{
|
|
30
|
+
step_id: string;
|
|
31
|
+
type: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
artifact_id?: string;
|
|
35
|
+
description: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
10
38
|
export interface SkillResult {
|
|
11
39
|
name: string;
|
|
12
40
|
fileName: string;
|
|
13
41
|
dirName: string;
|
|
14
42
|
content: string;
|
|
43
|
+
launcherContent?: string;
|
|
44
|
+
bodyContent?: string;
|
|
45
|
+
bodyFileName?: string;
|
|
46
|
+
mapEntry?: SkillMapEntry;
|
|
15
47
|
}
|
|
16
48
|
export declare function compileControllerToSkill(controllerKey: string, controller: ControllerDef, variables?: Record<string, string>, workflows?: Record<string, WorkflowDef>): SkillResult;
|
|
17
|
-
/**
|
|
18
|
-
* Compile a WorkflowPlan + roles + IR into a SKILL.md file.
|
|
19
|
-
* Variables from DNA config are substituted into prompts and descriptions.
|
|
20
|
-
*/
|
|
21
49
|
export declare function compileWorkflowToSkill(plan: WorkflowPlan, roles: Record<string, RoleDef>, ir?: ConstraintIR, variables?: Record<string, string>): SkillResult;
|
|
22
50
|
export declare function writeSkillFiles(results: SkillResult[], outputDir: string): Promise<string[]>;
|
|
23
51
|
export declare function removeSkillFiles(outputDir: string): Promise<number>;
|
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
* Skills are compiled views of DNA's structured definitions — not stored content.
|
|
8
8
|
*/
|
|
9
9
|
import { mkdir, writeFile, readdir, readFile, rm } from "node:fs/promises";
|
|
10
|
-
import { join } from "node:path";
|
|
10
|
+
import { join, resolve, sep } from "node:path";
|
|
11
11
|
import { getControllerKindMetadata } from "../schema/controller-registry.js";
|
|
12
|
-
import { toKebabCase } from "./agent-md.js";
|
|
12
|
+
import { assertSafeGeneratedName, toKebabCase } from "./agent-md.js";
|
|
13
13
|
const SENTINEL = "<!-- intentdna:managed — do not edit manually -->";
|
|
14
|
+
function resolveContainedPath(baseDir, ...segments) {
|
|
15
|
+
const base = resolve(baseDir);
|
|
16
|
+
const target = resolve(baseDir, ...segments);
|
|
17
|
+
if (target !== base && !target.startsWith(`${base}${sep}`)) {
|
|
18
|
+
throw new Error(`generated path escapes output directory: ${target}`);
|
|
19
|
+
}
|
|
20
|
+
return target;
|
|
21
|
+
}
|
|
14
22
|
// ── Compile Controller → Skill ─────────────────────────────
|
|
15
23
|
export function compileControllerToSkill(controllerKey, controller, variables, workflows) {
|
|
16
24
|
const kindMetadata = getControllerKindMetadata(controller.kind);
|
|
@@ -18,6 +26,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
18
26
|
throw new Error(`Unsupported controller kind: ${controller.kind}`);
|
|
19
27
|
}
|
|
20
28
|
const skillName = `dna-${toKebabCase(controllerKey)}`;
|
|
29
|
+
assertSafeGeneratedName(skillName, "skill name");
|
|
21
30
|
const analyzerAgent = `dna-${toKebabCase(controller.roles.analyzer)}`;
|
|
22
31
|
const analysisReviewerAgent = `dna-${toKebabCase(controller.roles.analysis_reviewer)}`;
|
|
23
32
|
const surgeonAgent = `dna-${toKebabCase(controller.roles.surgeon)}`;
|
|
@@ -136,6 +145,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
136
145
|
lines.push("</Verdict_Mapping>");
|
|
137
146
|
lines.push("");
|
|
138
147
|
lines.push("<Stop_Report>");
|
|
148
|
+
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.");
|
|
139
149
|
for (const line of policy.stop_report) {
|
|
140
150
|
lines.push(line);
|
|
141
151
|
}
|
|
@@ -155,7 +165,28 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
155
165
|
}
|
|
156
166
|
const dirName = skillName;
|
|
157
167
|
const fileName = join(dirName, "SKILL.md");
|
|
158
|
-
return { name: skillName, fileName, dirName, content }
|
|
168
|
+
return attachSkillBody({ name: skillName, fileName, dirName, content }, {
|
|
169
|
+
description: controller.description || controller.name,
|
|
170
|
+
triggers: [toKebabCase(controllerKey), `run ${toKebabCase(controllerKey)}`],
|
|
171
|
+
kind: "controller",
|
|
172
|
+
mapEntry: {
|
|
173
|
+
name: skillName,
|
|
174
|
+
dirName,
|
|
175
|
+
source: controllerKey,
|
|
176
|
+
kind: "controller",
|
|
177
|
+
bodyPath: join(dirName, "skill-bodies", "body.md"),
|
|
178
|
+
roles: Object.values(controller.roles),
|
|
179
|
+
consumes: [
|
|
180
|
+
{ step_id: "controller", type: "file", path: controller.diagnosis_artifact_path, description: "Diagnosis artifact" },
|
|
181
|
+
{ step_id: "controller", type: "file", path: controller.diagnosis_review_artifact_path, description: "Diagnosis review artifact" },
|
|
182
|
+
{ step_id: "controller", type: "file", path: controller.review_artifact_path, description: "Fix review artifact" },
|
|
183
|
+
{ step_id: "controller", type: "file", path: controller.progress_artifact_path, description: "Progress JSON artifact" },
|
|
184
|
+
],
|
|
185
|
+
produces: [
|
|
186
|
+
{ step_id: "controller", type: "file", path: controller.progress_artifact_path, description: "Progress JSON artifact" },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
});
|
|
159
190
|
}
|
|
160
191
|
function requireStepByRole(workflow, role, workflowName) {
|
|
161
192
|
const step = workflow?.steps.find((candidate) => candidate.role === role);
|
|
@@ -164,6 +195,73 @@ function requireStepByRole(workflow, role, workflowName) {
|
|
|
164
195
|
}
|
|
165
196
|
return step;
|
|
166
197
|
}
|
|
198
|
+
function createSkillLauncher(params) {
|
|
199
|
+
const lines = [];
|
|
200
|
+
lines.push("---");
|
|
201
|
+
lines.push(`name: ${params.skillName}`);
|
|
202
|
+
lines.push(`description: "Use when user says /${params.skillName}. ${escapeYaml(params.description)}"`);
|
|
203
|
+
lines.push("user-invocable: true");
|
|
204
|
+
lines.push("triggers:");
|
|
205
|
+
for (const trigger of params.triggers)
|
|
206
|
+
lines.push(` - "${escapeYaml(trigger)}"`);
|
|
207
|
+
lines.push("---");
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(SENTINEL);
|
|
210
|
+
lines.push(`<!-- Compiled: ${new Date().toISOString()} -->`);
|
|
211
|
+
lines.push("");
|
|
212
|
+
lines.push(`# /${params.skillName}`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("<Purpose>");
|
|
215
|
+
lines.push(params.description);
|
|
216
|
+
lines.push("</Purpose>");
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push("<Skill_Body>");
|
|
219
|
+
lines.push(`Full ${params.kind} instructions live in \`skill-bodies/${params.bodyFileName}\`.`);
|
|
220
|
+
lines.push("Read that body before executing any step; the launcher is only an entrypoint.");
|
|
221
|
+
lines.push("</Skill_Body>");
|
|
222
|
+
lines.push("");
|
|
223
|
+
if (params.bodyContent.includes("<Required_Context>")) {
|
|
224
|
+
lines.push("<Required_Context>");
|
|
225
|
+
lines.push("The skill body contains required context files. Read them before any Edit, Write, or Bash action.");
|
|
226
|
+
lines.push("</Required_Context>");
|
|
227
|
+
lines.push("");
|
|
228
|
+
}
|
|
229
|
+
if (params.bodyContent.includes("<Advisory_Context>")) {
|
|
230
|
+
lines.push("<Advisory_Context>");
|
|
231
|
+
lines.push("The skill body contains planning-only advisory context sources. Quote and cite them when used; they never satisfy handoff or enforcement checks.");
|
|
232
|
+
lines.push("</Advisory_Context>");
|
|
233
|
+
lines.push("");
|
|
234
|
+
}
|
|
235
|
+
if (params.bodyContent.includes("<Handoff>")) {
|
|
236
|
+
lines.push("<Handoff>");
|
|
237
|
+
lines.push("Write human-readable handoff notes when requested, but do not treat handoff.md as a machine fact.");
|
|
238
|
+
lines.push("ArtifactManifest entries written by runtime hooks remain the machine source of truth for handoff satisfaction.");
|
|
239
|
+
lines.push("</Handoff>");
|
|
240
|
+
lines.push("");
|
|
241
|
+
}
|
|
242
|
+
lines.push("<Execution>");
|
|
243
|
+
lines.push(`Follow \`skill-bodies/${params.bodyFileName}\` exactly.`);
|
|
244
|
+
lines.push("</Execution>");
|
|
245
|
+
return lines.join("\n") + "\n";
|
|
246
|
+
}
|
|
247
|
+
function attachSkillBody(result, params) {
|
|
248
|
+
const bodyFileName = "body.md";
|
|
249
|
+
const launcherContent = createSkillLauncher({
|
|
250
|
+
skillName: result.name,
|
|
251
|
+
description: params.description,
|
|
252
|
+
triggers: params.triggers,
|
|
253
|
+
bodyFileName,
|
|
254
|
+
bodyContent: result.content,
|
|
255
|
+
kind: params.kind,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
...result,
|
|
259
|
+
launcherContent,
|
|
260
|
+
bodyContent: result.content,
|
|
261
|
+
bodyFileName,
|
|
262
|
+
mapEntry: params.mapEntry,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
167
265
|
function normalizeControllerPolicy(controller) {
|
|
168
266
|
const defaults = defaultControllerPolicy(controller);
|
|
169
267
|
const policy = {
|
|
@@ -234,7 +332,7 @@ function defaultControllerPolicy(controller) {
|
|
|
234
332
|
"- MAX_ROUNDS: stop and report max rounds reached",
|
|
235
333
|
],
|
|
236
334
|
stop_report: [
|
|
237
|
-
"On REQUEST_CHANGES, BLOCKED, or MAX_ROUNDS, explain the stop reason, the evidence,
|
|
335
|
+
"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.",
|
|
238
336
|
],
|
|
239
337
|
constraints: [
|
|
240
338
|
`The controller must schedule only from ${controller.progress_artifact_path}.`,
|
|
@@ -247,9 +345,46 @@ function defaultControllerPolicy(controller) {
|
|
|
247
345
|
* Compile a WorkflowPlan + roles + IR into a SKILL.md file.
|
|
248
346
|
* Variables from DNA config are substituted into prompts and descriptions.
|
|
249
347
|
*/
|
|
348
|
+
function workflowSkillMapEntry(skillName, dirName, plan) {
|
|
349
|
+
const consumes = [];
|
|
350
|
+
const produces = [];
|
|
351
|
+
for (const step of plan.steps) {
|
|
352
|
+
for (const artifact of step.handoff?.consumes ?? []) {
|
|
353
|
+
consumes.push({
|
|
354
|
+
step_id: step.id,
|
|
355
|
+
type: artifact.type,
|
|
356
|
+
path: artifact.path,
|
|
357
|
+
name: artifact.name,
|
|
358
|
+
artifact_id: artifact.artifact_id,
|
|
359
|
+
description: artifact.description,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
for (const artifact of step.handoff?.produces ?? []) {
|
|
363
|
+
produces.push({
|
|
364
|
+
step_id: step.id,
|
|
365
|
+
type: artifact.type,
|
|
366
|
+
path: artifact.path,
|
|
367
|
+
name: artifact.name,
|
|
368
|
+
artifact_id: artifact.artifact_id,
|
|
369
|
+
description: artifact.description,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
name: skillName,
|
|
375
|
+
dirName,
|
|
376
|
+
source: plan.workflow_key ?? plan.source_workflow,
|
|
377
|
+
kind: "workflow",
|
|
378
|
+
bodyPath: join(dirName, "skill-bodies", "body.md"),
|
|
379
|
+
roles: [...new Set(plan.steps.map((step) => step.role))],
|
|
380
|
+
consumes,
|
|
381
|
+
produces,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
250
384
|
export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
251
385
|
const lines = [];
|
|
252
386
|
const skillName = `dna-${toKebabCase(plan.name)}`;
|
|
387
|
+
assertSafeGeneratedName(skillName, "skill name");
|
|
253
388
|
// Frontmatter
|
|
254
389
|
lines.push("---");
|
|
255
390
|
lines.push(`name: ${skillName}`);
|
|
@@ -341,6 +476,22 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
341
476
|
lines.push("</Required_Context>");
|
|
342
477
|
lines.push("");
|
|
343
478
|
}
|
|
479
|
+
if (ir?.context_sources?.length) {
|
|
480
|
+
lines.push("<Advisory_Context>");
|
|
481
|
+
lines.push("These sources are planning-only references. They do not satisfy ArtifactManifest, handoff consumes, completion checks, or enforcement facts.");
|
|
482
|
+
lines.push("If you use them, quote the relevant passage and cite the source id.");
|
|
483
|
+
lines.push("");
|
|
484
|
+
for (const source of ir.context_sources) {
|
|
485
|
+
const locator = source.path ?? source.url ?? "(no locator)";
|
|
486
|
+
const title = source.title ? ` — ${source.title}` : "";
|
|
487
|
+
lines.push(`- ${source.id}: ${source.type} ${locator}${title}`);
|
|
488
|
+
}
|
|
489
|
+
for (const instruction of ir.planning_context?.instructions ?? []) {
|
|
490
|
+
lines.push(`- Planning instruction: ${instruction}`);
|
|
491
|
+
}
|
|
492
|
+
lines.push("</Advisory_Context>");
|
|
493
|
+
lines.push("");
|
|
494
|
+
}
|
|
344
495
|
// Steps — each step MUST be executed via Agent() tool call
|
|
345
496
|
const usedRoles = new Set(plan.steps.map((s) => s.role));
|
|
346
497
|
// Pre-compute handoff targets: steps that are pointed to by another step's handoff_to
|
|
@@ -489,6 +640,8 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
489
640
|
const stepsWithHandoff = plan.steps.filter((s) => s.handoff && (s.handoff.consumes?.length || s.handoff.produces?.length));
|
|
490
641
|
if (stepsWithHandoff.length > 0) {
|
|
491
642
|
lines.push("<Handoff>");
|
|
643
|
+
lines.push("Human-readable handoff notes are useful for review, but they do not satisfy machine handoff gates.");
|
|
644
|
+
lines.push("ArtifactManifest records written by runtime hooks remain the machine source of truth for produced/consumed artifacts.");
|
|
492
645
|
for (const step of stepsWithHandoff) {
|
|
493
646
|
const h = step.handoff;
|
|
494
647
|
if (h.produces && h.produces.length > 0) {
|
|
@@ -532,7 +685,12 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
532
685
|
}
|
|
533
686
|
const dirName = skillName;
|
|
534
687
|
const fileName = join(dirName, "SKILL.md");
|
|
535
|
-
return { name: skillName, fileName, dirName, content }
|
|
688
|
+
return attachSkillBody({ name: skillName, fileName, dirName, content }, {
|
|
689
|
+
description: plan.description || plan.name,
|
|
690
|
+
triggers: [plan.name, `run ${plan.name}`],
|
|
691
|
+
kind: "workflow",
|
|
692
|
+
mapEntry: workflowSkillMapEntry(skillName, dirName, plan),
|
|
693
|
+
});
|
|
536
694
|
}
|
|
537
695
|
/** Collect all {{var_name}} references from a workflow plan. */
|
|
538
696
|
function collectVariableRefs(plan) {
|
|
@@ -555,12 +713,29 @@ function collectVariableRefs(plan) {
|
|
|
555
713
|
// ── File Operations ────────────────────────────────────────
|
|
556
714
|
export async function writeSkillFiles(results, outputDir) {
|
|
557
715
|
const written = [];
|
|
716
|
+
const mapEntries = [];
|
|
558
717
|
for (const result of results) {
|
|
559
|
-
|
|
718
|
+
assertSafeGeneratedName(result.dirName, "skill directory");
|
|
719
|
+
const dir = resolveContainedPath(outputDir, result.dirName);
|
|
560
720
|
await mkdir(dir, { recursive: true });
|
|
561
|
-
const
|
|
562
|
-
await writeFile(
|
|
563
|
-
written.push(
|
|
721
|
+
const skillPath = resolveContainedPath(dir, "SKILL.md");
|
|
722
|
+
await writeFile(skillPath, result.launcherContent ?? result.content, "utf-8");
|
|
723
|
+
written.push(skillPath);
|
|
724
|
+
if (result.bodyContent && result.bodyFileName) {
|
|
725
|
+
assertSafeGeneratedName(result.bodyFileName.replace(/\.md$/, ""), "skill body file");
|
|
726
|
+
const bodyDir = resolveContainedPath(dir, "skill-bodies");
|
|
727
|
+
await mkdir(bodyDir, { recursive: true });
|
|
728
|
+
const bodyPath = resolveContainedPath(bodyDir, result.bodyFileName);
|
|
729
|
+
await writeFile(bodyPath, result.bodyContent, "utf-8");
|
|
730
|
+
written.push(bodyPath);
|
|
731
|
+
}
|
|
732
|
+
if (result.mapEntry)
|
|
733
|
+
mapEntries.push(result.mapEntry);
|
|
734
|
+
}
|
|
735
|
+
if (mapEntries.length > 0) {
|
|
736
|
+
const mapPath = resolveContainedPath(outputDir, ".intentdna-skill-map.json");
|
|
737
|
+
await writeFile(mapPath, JSON.stringify({ generated_at: new Date().toISOString(), skills: mapEntries }, null, 2) + "\n", "utf-8");
|
|
738
|
+
written.push(mapPath);
|
|
564
739
|
}
|
|
565
740
|
return written;
|
|
566
741
|
}
|
|
@@ -19,7 +19,7 @@ export interface WorkflowShellOptions {
|
|
|
19
19
|
maxBudgetPerAgent?: number;
|
|
20
20
|
/** Output format for agent results (default "json") */
|
|
21
21
|
outputFormat?: "json" | "text";
|
|
22
|
-
/** Permission mode (default "
|
|
22
|
+
/** Permission mode (default "default") */
|
|
23
23
|
permissionMode?: string;
|
|
24
24
|
/** Log directory relative to project root (default ".") */
|
|
25
25
|
logDir?: string;
|
|
@@ -466,7 +466,7 @@ export function compileWorkflowToShell(plan, options) {
|
|
|
466
466
|
agentPrefix: options?.agentPrefix ?? "dna-",
|
|
467
467
|
maxBudgetPerAgent: options?.maxBudgetPerAgent ?? 10,
|
|
468
468
|
outputFormat: options?.outputFormat ?? "json",
|
|
469
|
-
permissionMode: options?.permissionMode ?? "
|
|
469
|
+
permissionMode: options?.permissionMode ?? "default",
|
|
470
470
|
logDir: options?.logDir ?? ".",
|
|
471
471
|
pauseSupport: options?.pauseSupport ?? false,
|
|
472
472
|
};
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -295,6 +295,35 @@ export interface LegibilityAssetMap {
|
|
|
295
295
|
per_role?: Record<string, LegibilityAsset[]>;
|
|
296
296
|
per_workflow?: Record<string, LegibilityAsset[]>;
|
|
297
297
|
}
|
|
298
|
+
export type AdvisoryContextSourceType = "file" | "url" | "doc" | "note";
|
|
299
|
+
export interface AdvisoryContextSource {
|
|
300
|
+
id: string;
|
|
301
|
+
type: AdvisoryContextSourceType;
|
|
302
|
+
path?: string;
|
|
303
|
+
url?: string;
|
|
304
|
+
title?: string;
|
|
305
|
+
description?: string;
|
|
306
|
+
tags?: string[];
|
|
307
|
+
}
|
|
308
|
+
export interface PlanningContext {
|
|
309
|
+
sources: string[];
|
|
310
|
+
instructions?: string[];
|
|
311
|
+
require_quotes?: boolean;
|
|
312
|
+
require_attribution?: boolean;
|
|
313
|
+
}
|
|
314
|
+
export interface CompiledAdvisoryContextSource extends AdvisoryContextSource {
|
|
315
|
+
usage: "planning_only";
|
|
316
|
+
non_enforcing: true;
|
|
317
|
+
requires_quotes: boolean;
|
|
318
|
+
requires_attribution: boolean;
|
|
319
|
+
}
|
|
320
|
+
export interface CompiledPlanningContext {
|
|
321
|
+
sources: string[];
|
|
322
|
+
instructions?: string[];
|
|
323
|
+
require_quotes: boolean;
|
|
324
|
+
require_attribution: boolean;
|
|
325
|
+
non_enforcing: true;
|
|
326
|
+
}
|
|
298
327
|
export interface IntentDNA {
|
|
299
328
|
$schema?: string;
|
|
300
329
|
version: string;
|
|
@@ -311,6 +340,8 @@ export interface IntentDNA {
|
|
|
311
340
|
variables?: Record<string, string | VariableDef>;
|
|
312
341
|
mcp?: Record<string, MCPServerDef>;
|
|
313
342
|
legibility_assets?: LegibilityAssetMap;
|
|
343
|
+
context_sources?: AdvisoryContextSource[];
|
|
344
|
+
planning_context?: PlanningContext;
|
|
314
345
|
context_files?: {
|
|
315
346
|
mandatory?: string[];
|
|
316
347
|
per_role?: Record<string, string[]>;
|
|
@@ -439,6 +470,8 @@ export interface ConstraintIR {
|
|
|
439
470
|
post_execution_validators: PostExecutionValidator[];
|
|
440
471
|
verifier_specs?: VerifierSpec[];
|
|
441
472
|
context_injections: ContextInjection[];
|
|
473
|
+
context_sources?: CompiledAdvisoryContextSource[];
|
|
474
|
+
planning_context?: CompiledPlanningContext;
|
|
442
475
|
source_dna_ids: string[];
|
|
443
476
|
compiled_at: string;
|
|
444
477
|
active_context?: string;
|
package/dist/schema/validate.js
CHANGED
|
@@ -7,7 +7,25 @@
|
|
|
7
7
|
* - Codon values in valid ranges
|
|
8
8
|
* - No circular inheritance
|
|
9
9
|
*/
|
|
10
|
+
import { isAbsolute } from "node:path";
|
|
10
11
|
import { validateController } from "./validators/controllers.js";
|
|
12
|
+
const SAFE_GENERATED_IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
const SAFE_GENERATED_DISPLAY_NAME = /^[A-Za-z0-9][A-Za-z0-9 _-]*$/;
|
|
14
|
+
function isSafeGeneratedIdentifier(value) {
|
|
15
|
+
return SAFE_GENERATED_IDENTIFIER.test(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\");
|
|
16
|
+
}
|
|
17
|
+
function isSafeGeneratedDisplayName(value) {
|
|
18
|
+
return SAFE_GENERATED_DISPLAY_NAME.test(value) && !value.includes("..");
|
|
19
|
+
}
|
|
20
|
+
function isSafeProjectTemplatePath(value) {
|
|
21
|
+
const withoutPlaceholders = value.replace(/\{\{[A-Za-z_][A-Za-z0-9_]*\}\}/g, "placeholder");
|
|
22
|
+
return value.trim().length > 0 &&
|
|
23
|
+
!isAbsolute(value) &&
|
|
24
|
+
!value.split(/[\\/]+/).includes("..") &&
|
|
25
|
+
!/[*?[\]{}]/.test(withoutPlaceholders) &&
|
|
26
|
+
!value.includes("${") &&
|
|
27
|
+
!value.includes("$(`");
|
|
28
|
+
}
|
|
11
29
|
function isLegibilityAssetArray(value) {
|
|
12
30
|
return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null &&
|
|
13
31
|
typeof item.type === "string" &&
|
|
@@ -72,6 +90,126 @@ function validateLegibilityAssets(dna, roleNamesSet, errors, warnings) {
|
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
}
|
|
93
|
+
function validateContextSources(dna, errors) {
|
|
94
|
+
const sources = dna.context_sources;
|
|
95
|
+
if (sources !== undefined) {
|
|
96
|
+
if (!Array.isArray(sources)) {
|
|
97
|
+
errors.push({ path: "context_sources", message: "must be an array" });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (let i = 0; i < sources.length; i++) {
|
|
102
|
+
const path = `context_sources[${i}]`;
|
|
103
|
+
if (typeof sources[i] !== "object" || sources[i] === null || Array.isArray(sources[i])) {
|
|
104
|
+
errors.push({ path, message: "context source must be an object" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const source = sources[i];
|
|
108
|
+
if (!source.id || typeof source.id !== "string") {
|
|
109
|
+
errors.push({ path: `${path}.id`, message: "context source requires non-empty id" });
|
|
110
|
+
}
|
|
111
|
+
else if (seen.has(source.id)) {
|
|
112
|
+
errors.push({ path: `${path}.id`, message: `duplicate context source id '${source.id}'` });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
seen.add(source.id);
|
|
116
|
+
}
|
|
117
|
+
if (!["file", "url", "doc", "note"].includes(source.type)) {
|
|
118
|
+
errors.push({ path: `${path}.type`, message: "must be one of: file, url, doc, note" });
|
|
119
|
+
}
|
|
120
|
+
if (source.type === "file" || source.type === "doc") {
|
|
121
|
+
if (typeof source.path !== "string" || !source.path) {
|
|
122
|
+
errors.push({ path: `${path}.path`, message: `${source.type} context source requires non-empty path` });
|
|
123
|
+
}
|
|
124
|
+
else if (isAbsolute(source.path) || source.path.split(/[\\/]+/).includes("..")) {
|
|
125
|
+
errors.push({ path: `${path}.path`, message: "context source path must stay within the project" });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (source.type === "url" && (typeof source.url !== "string" || !source.url)) {
|
|
129
|
+
errors.push({ path: `${path}.url`, message: "url context source requires non-empty url" });
|
|
130
|
+
}
|
|
131
|
+
if (source.tags !== undefined && (!Array.isArray(source.tags) || source.tags.some((tag) => typeof tag !== "string" || !tag))) {
|
|
132
|
+
errors.push({ path: `${path}.tags`, message: "tags must be an array of non-empty strings" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (dna.planning_context) {
|
|
138
|
+
const pc = dna.planning_context;
|
|
139
|
+
if (!Array.isArray(pc.sources) || pc.sources.some((sourceId) => typeof sourceId !== "string" || !sourceId)) {
|
|
140
|
+
errors.push({ path: "planning_context.sources", message: "must be an array of non-empty context source ids" });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const sourceList = Array.isArray(sources) ? sources : [];
|
|
144
|
+
const declared = new Set(sourceList.map((source) => source.id));
|
|
145
|
+
for (const sourceId of pc.sources) {
|
|
146
|
+
if (!declared.has(sourceId)) {
|
|
147
|
+
errors.push({ path: "planning_context.sources", message: `references unknown context source '${sourceId}'` });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (pc.instructions !== undefined && (!Array.isArray(pc.instructions) || pc.instructions.some((instruction) => typeof instruction !== "string" || !instruction))) {
|
|
152
|
+
errors.push({ path: "planning_context.instructions", message: "must be an array of non-empty strings" });
|
|
153
|
+
}
|
|
154
|
+
if (pc.require_quotes !== undefined && typeof pc.require_quotes !== "boolean") {
|
|
155
|
+
errors.push({ path: "planning_context.require_quotes", message: "must be a boolean" });
|
|
156
|
+
}
|
|
157
|
+
if (pc.require_attribution !== undefined && typeof pc.require_attribution !== "boolean") {
|
|
158
|
+
errors.push({ path: "planning_context.require_attribution", message: "must be a boolean" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function contextSourceIdentities(source) {
|
|
163
|
+
const identities = [source.id];
|
|
164
|
+
if (source.type === "file" || source.type === "doc")
|
|
165
|
+
identities.push(source.path);
|
|
166
|
+
if (source.type === "url")
|
|
167
|
+
identities.push(source.url);
|
|
168
|
+
return identities.filter((identity) => typeof identity === "string" && identity.length > 0);
|
|
169
|
+
}
|
|
170
|
+
function handoffArtifactIdentities(artifact) {
|
|
171
|
+
return [artifact.artifact_id, artifact.name, artifact.path, artifact.type === "git_commit" ? "git_commit" : undefined]
|
|
172
|
+
.filter((identity) => typeof identity === "string" && identity.length > 0);
|
|
173
|
+
}
|
|
174
|
+
function validateContextSourceHandoffOverlap(dna, errors) {
|
|
175
|
+
const contextSources = Array.isArray(dna.context_sources) ? dna.context_sources : [];
|
|
176
|
+
if (contextSources.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
const advisoryIdentities = new Set(contextSources.flatMap(contextSourceIdentities));
|
|
179
|
+
if (advisoryIdentities.size === 0)
|
|
180
|
+
return;
|
|
181
|
+
const checkArtifact = (artifact, path) => {
|
|
182
|
+
if (artifact.required === false)
|
|
183
|
+
return;
|
|
184
|
+
for (const identity of handoffArtifactIdentities(artifact)) {
|
|
185
|
+
if (!advisoryIdentities.has(identity))
|
|
186
|
+
continue;
|
|
187
|
+
errors.push({
|
|
188
|
+
path,
|
|
189
|
+
message: `planning-only context source '${identity}' cannot be used as a required handoff artifact`,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const checkWorkflow = (workflow, path) => {
|
|
195
|
+
for (let i = 0; i < (workflow.steps ?? []).length; i++) {
|
|
196
|
+
const step = workflow.steps[i];
|
|
197
|
+
for (const direction of ["consumes", "produces"]) {
|
|
198
|
+
const artifacts = step.handoff?.[direction] ?? [];
|
|
199
|
+
for (let j = 0; j < artifacts.length; j++) {
|
|
200
|
+
checkArtifact(artifacts[j], `${path}.steps[${i}].handoff.${direction}[${j}]`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (dna.workflow)
|
|
206
|
+
checkWorkflow(dna.workflow, "workflow");
|
|
207
|
+
if (dna.workflows) {
|
|
208
|
+
for (const [wfName, wfDef] of Object.entries(dna.workflows)) {
|
|
209
|
+
checkWorkflow(wfDef, `workflows.${wfName}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
75
213
|
function validateCodon(codon, path) {
|
|
76
214
|
const errors = [];
|
|
77
215
|
switch (codon.type) {
|
|
@@ -150,6 +288,9 @@ function validateContext(name, ctx, geneNames) {
|
|
|
150
288
|
function validateRole(name, role, geneNames) {
|
|
151
289
|
const errors = [];
|
|
152
290
|
const path = `roles.${name}`;
|
|
291
|
+
if (!isSafeGeneratedIdentifier(name)) {
|
|
292
|
+
errors.push({ path, message: "role name must contain only letters, digits, underscores, and hyphens" });
|
|
293
|
+
}
|
|
153
294
|
// description required
|
|
154
295
|
if (!role.description) {
|
|
155
296
|
errors.push({ path: `${path}.description`, message: "role requires 'description'" });
|
|
@@ -245,8 +386,10 @@ function validateRole(name, role, geneNames) {
|
|
|
245
386
|
if (os.converter !== undefined && (typeof os.converter !== "string" || !os.converter)) {
|
|
246
387
|
errors.push({ path: `${osPath}.converter`, message: "must be a non-empty string" });
|
|
247
388
|
}
|
|
248
|
-
if (os.path !== undefined
|
|
249
|
-
|
|
389
|
+
if (os.path !== undefined) {
|
|
390
|
+
if (typeof os.path !== "string" || !isSafeProjectTemplatePath(os.path)) {
|
|
391
|
+
errors.push({ path: `${osPath}.path`, message: "must be a safe project-relative path template" });
|
|
392
|
+
}
|
|
250
393
|
}
|
|
251
394
|
}
|
|
252
395
|
return errors;
|
|
@@ -307,6 +450,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
307
450
|
if (!workflow.name) {
|
|
308
451
|
errors.push({ path: `${path}.name`, message: "workflow requires 'name'" });
|
|
309
452
|
}
|
|
453
|
+
else if (!isSafeGeneratedDisplayName(workflow.name)) {
|
|
454
|
+
errors.push({ path: `${path}.name`, message: "workflow name must contain only letters, digits, spaces, underscores, and hyphens" });
|
|
455
|
+
}
|
|
310
456
|
// steps required and non-empty
|
|
311
457
|
if (!workflow.steps || workflow.steps.length === 0) {
|
|
312
458
|
errors.push({ path: `${path}.steps`, message: "workflow requires at least one step" });
|
|
@@ -322,6 +468,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
322
468
|
if (!step.id) {
|
|
323
469
|
errors.push({ path: `${stepPath}.id`, message: "step requires 'id'" });
|
|
324
470
|
}
|
|
471
|
+
else if (!isSafeGeneratedIdentifier(step.id)) {
|
|
472
|
+
errors.push({ path: `${stepPath}.id`, message: "step id must contain only letters, digits, underscores, and hyphens" });
|
|
473
|
+
}
|
|
325
474
|
else if (seenStepIds.has(step.id)) {
|
|
326
475
|
errors.push({ path: `${stepPath}.id`, message: `duplicate step id '${step.id}'` });
|
|
327
476
|
}
|
|
@@ -333,6 +482,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
333
482
|
if (!step.role) {
|
|
334
483
|
errors.push({ path: `${stepPath}.role`, message: "step requires 'role'" });
|
|
335
484
|
}
|
|
485
|
+
else if (!isSafeGeneratedIdentifier(step.role)) {
|
|
486
|
+
errors.push({ path: `${stepPath}.role`, message: "step role must contain only letters, digits, underscores, and hyphens" });
|
|
487
|
+
}
|
|
336
488
|
else if (roleNames.size > 0 && !roleNames.has(step.role)) {
|
|
337
489
|
errors.push({
|
|
338
490
|
path: `${stepPath}.role`,
|
|
@@ -583,6 +735,7 @@ export function validateDNA(dna) {
|
|
|
583
735
|
errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
|
|
584
736
|
}
|
|
585
737
|
}
|
|
738
|
+
validateContextSourceHandoffOverlap(dna, errors);
|
|
586
739
|
if (dna.controllers) {
|
|
587
740
|
const workflowsForControllers = { ...(dna.workflows ?? {}) };
|
|
588
741
|
if (dna.workflow)
|
|
@@ -621,6 +774,7 @@ export function validateDNA(dna) {
|
|
|
621
774
|
}
|
|
622
775
|
}
|
|
623
776
|
validateLegibilityAssets(dna, roleNamesSet, errors, warnings);
|
|
777
|
+
validateContextSources(dna, errors);
|
|
624
778
|
// Validate context_files
|
|
625
779
|
if (dna.context_files) {
|
|
626
780
|
const cf = dna.context_files;
|