intentdna 1.6.4 → 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/generate.js +5 -33
- 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 +209 -190
- 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 +39 -26
- package/dist/hooks/event-registry.d.ts +17 -0
- package/dist/hooks/event-registry.js +89 -0
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/schema.js +5 -1
- 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 +21 -53
- 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/markdown-target-registry.d.ts +17 -0
- package/dist/runtime/markdown-target-registry.js +37 -0
- package/dist/runtime/markdown.d.ts +2 -1
- package/dist/runtime/markdown.js +9 -12
- package/dist/runtime/output-artifact-registry.d.ts +11 -0
- package/dist/runtime/output-artifact-registry.js +30 -0
- package/dist/runtime/settings-adapter.d.ts +2 -1
- package/dist/runtime/settings-adapter.js +4 -12
- package/dist/runtime/skill-adapter.d.ts +32 -4
- package/dist/runtime/skill-adapter.js +187 -10
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/schema/controller-registry.d.ts +29 -0
- package/dist/schema/controller-registry.js +35 -0
- package/dist/schema/types.d.ts +33 -0
- package/dist/schema/validate.js +157 -138
- package/dist/schema/validators/controllers.d.ts +3 -0
- package/dist/schema/validators/controllers.js +156 -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/dist/templates/metadata.d.ts +6 -0
- package/dist/templates/metadata.js +32 -0
- package/package.json +1 -1
- package/spec/foundation-hardening.md +2 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CompiledAdvisoryContextSource } from "../schema/types.js";
|
|
2
|
+
export interface ContextSourceDocument {
|
|
3
|
+
source: CompiledAdvisoryContextSource;
|
|
4
|
+
content?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ContextSourceIndex {
|
|
7
|
+
sources: CompiledAdvisoryContextSource[];
|
|
8
|
+
planning_instructions: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function loadContextSourceIndex(projectDir: string, files?: string[]): Promise<ContextSourceIndex>;
|
|
11
|
+
export declare function searchContextSources(index: ContextSourceIndex, query: string): CompiledAdvisoryContextSource[];
|
|
12
|
+
export declare function findContextSource(index: ContextSourceIndex, id: string): CompiledAdvisoryContextSource | undefined;
|
|
13
|
+
export declare function readContextSourceDocument(projectDir: string, source: CompiledAdvisoryContextSource): Promise<ContextSourceDocument>;
|
|
14
|
+
export declare function formatContextSource(source: CompiledAdvisoryContextSource): string;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const MARKDOWN_TARGET_IDS: readonly ["claude-md", "soul-md", "cursorrules", "system-prompt", "agents-md"];
|
|
2
|
+
export type MarkdownTarget = typeof MARKDOWN_TARGET_IDS[number];
|
|
3
|
+
export type MarkdownRenderer<TIR = unknown> = (ir: TIR) => string;
|
|
4
|
+
export interface MarkdownTargetMetadata {
|
|
5
|
+
id: MarkdownTarget;
|
|
6
|
+
label: string;
|
|
7
|
+
supportsSyncInject: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface MarkdownTargetRuntime<TIR = unknown> extends MarkdownTargetMetadata {
|
|
10
|
+
render: MarkdownRenderer<TIR>;
|
|
11
|
+
}
|
|
12
|
+
export declare const MARKDOWN_TARGET_REGISTRY: readonly MarkdownTargetMetadata[];
|
|
13
|
+
export declare function getMarkdownTargetMetadata(target: unknown): MarkdownTargetMetadata | undefined;
|
|
14
|
+
export declare function isMarkdownTarget(target: unknown): target is MarkdownTarget;
|
|
15
|
+
export declare function syncInjectMarkdownTargets(): MarkdownTarget[];
|
|
16
|
+
export declare function bindMarkdownTargetRenderers<TIR>(renderers: Record<MarkdownTarget, MarkdownRenderer<TIR>>): readonly MarkdownTargetRuntime<TIR>[];
|
|
17
|
+
export declare function getMarkdownTargetRuntime<TIR>(target: unknown, renderers: Record<MarkdownTarget, MarkdownRenderer<TIR>>): MarkdownTargetRuntime<TIR> | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const MARKDOWN_TARGET_IDS = [
|
|
2
|
+
"claude-md",
|
|
3
|
+
"soul-md",
|
|
4
|
+
"cursorrules",
|
|
5
|
+
"system-prompt",
|
|
6
|
+
"agents-md",
|
|
7
|
+
];
|
|
8
|
+
export const MARKDOWN_TARGET_REGISTRY = [
|
|
9
|
+
{ id: "claude-md", label: "CLAUDE.md", supportsSyncInject: true },
|
|
10
|
+
{ id: "soul-md", label: "SOUL.md", supportsSyncInject: true },
|
|
11
|
+
{ id: "cursorrules", label: ".cursorrules", supportsSyncInject: true },
|
|
12
|
+
{ id: "system-prompt", label: "System prompt", supportsSyncInject: true },
|
|
13
|
+
{ id: "agents-md", label: "AGENTS.md", supportsSyncInject: false },
|
|
14
|
+
];
|
|
15
|
+
const TARGET_BY_ID = new Map(MARKDOWN_TARGET_REGISTRY.map((entry) => [entry.id, entry]));
|
|
16
|
+
export function getMarkdownTargetMetadata(target) {
|
|
17
|
+
return typeof target === "string" ? TARGET_BY_ID.get(target) : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function isMarkdownTarget(target) {
|
|
20
|
+
return getMarkdownTargetMetadata(target) !== undefined;
|
|
21
|
+
}
|
|
22
|
+
export function syncInjectMarkdownTargets() {
|
|
23
|
+
return MARKDOWN_TARGET_REGISTRY
|
|
24
|
+
.filter((entry) => entry.supportsSyncInject)
|
|
25
|
+
.map((entry) => entry.id);
|
|
26
|
+
}
|
|
27
|
+
export function bindMarkdownTargetRenderers(renderers) {
|
|
28
|
+
return MARKDOWN_TARGET_REGISTRY.map((entry) => ({
|
|
29
|
+
...entry,
|
|
30
|
+
render: renderers[entry.id],
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function getMarkdownTargetRuntime(target, renderers) {
|
|
34
|
+
if (typeof target !== "string")
|
|
35
|
+
return undefined;
|
|
36
|
+
return bindMarkdownTargetRenderers(renderers).find((entry) => entry.id === target);
|
|
37
|
+
}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* <!-- intentdna:end -->
|
|
14
14
|
*/
|
|
15
15
|
import type { ConstraintIR } from "../schema/types.js";
|
|
16
|
-
|
|
16
|
+
import { type MarkdownTarget } from "./markdown-target-registry.js";
|
|
17
|
+
export type { MarkdownTarget } from "./markdown-target-registry.js";
|
|
17
18
|
/**
|
|
18
19
|
* Compile Constraint IR to a markdown block suitable for the target format.
|
|
19
20
|
*/
|
package/dist/runtime/markdown.js
CHANGED
|
@@ -13,24 +13,21 @@
|
|
|
13
13
|
* <!-- intentdna:end -->
|
|
14
14
|
*/
|
|
15
15
|
import { readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { getMarkdownTargetRuntime } from "./markdown-target-registry.js";
|
|
16
17
|
const SENTINEL_START = "<!-- intentdna:start -->";
|
|
17
18
|
const SENTINEL_END = "<!-- intentdna:end -->";
|
|
19
|
+
const MARKDOWN_RENDERERS = {
|
|
20
|
+
"claude-md": renderClaudeMd,
|
|
21
|
+
"soul-md": renderSoulMd,
|
|
22
|
+
cursorrules: renderCursorrules,
|
|
23
|
+
"system-prompt": renderSystemPrompt,
|
|
24
|
+
"agents-md": renderAgentsMd,
|
|
25
|
+
};
|
|
18
26
|
/**
|
|
19
27
|
* Compile Constraint IR to a markdown block suitable for the target format.
|
|
20
28
|
*/
|
|
21
29
|
export function compileToMarkdown(ir, target) {
|
|
22
|
-
|
|
23
|
-
case "claude-md":
|
|
24
|
-
return renderClaudeMd(ir);
|
|
25
|
-
case "soul-md":
|
|
26
|
-
return renderSoulMd(ir);
|
|
27
|
-
case "cursorrules":
|
|
28
|
-
return renderCursorrules(ir);
|
|
29
|
-
case "system-prompt":
|
|
30
|
-
return renderSystemPrompt(ir);
|
|
31
|
-
case "agents-md":
|
|
32
|
-
return renderAgentsMd(ir);
|
|
33
|
-
}
|
|
30
|
+
return getMarkdownTargetRuntime(target, MARKDOWN_RENDERERS).render(ir);
|
|
34
31
|
}
|
|
35
32
|
/**
|
|
36
33
|
* Wrap content in sentinel comments for idempotent injection.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const SYNC_OUTPUT_ARTIFACT_IDS: readonly ["markdown-injection", "compiled-ir", "settings-hooks", "agents", "workflows", "skills-controllers", "mcp-config", "lock-file"];
|
|
2
|
+
export type SyncOutputArtifactId = typeof SYNC_OUTPUT_ARTIFACT_IDS[number];
|
|
3
|
+
export interface SyncOutputArtifactMetadata {
|
|
4
|
+
id: SyncOutputArtifactId;
|
|
5
|
+
order: number;
|
|
6
|
+
label: string;
|
|
7
|
+
trackedByLock: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const SYNC_OUTPUT_ARTIFACT_REGISTRY: readonly SyncOutputArtifactMetadata[];
|
|
10
|
+
export declare function getSyncOutputArtifactMetadata(id: unknown): SyncOutputArtifactMetadata | undefined;
|
|
11
|
+
export declare function syncOutputArtifactIds(): SyncOutputArtifactId[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const SYNC_OUTPUT_ARTIFACT_IDS = [
|
|
2
|
+
"markdown-injection",
|
|
3
|
+
"compiled-ir",
|
|
4
|
+
"settings-hooks",
|
|
5
|
+
"agents",
|
|
6
|
+
"workflows",
|
|
7
|
+
"skills-controllers",
|
|
8
|
+
"mcp-config",
|
|
9
|
+
"lock-file",
|
|
10
|
+
];
|
|
11
|
+
export const SYNC_OUTPUT_ARTIFACT_REGISTRY = [
|
|
12
|
+
{ id: "markdown-injection", order: 30, label: "Markdown injection", trackedByLock: true },
|
|
13
|
+
{ id: "compiled-ir", order: 35, label: "Compiled IR", trackedByLock: true },
|
|
14
|
+
{ id: "settings-hooks", order: 36, label: "Settings hooks", trackedByLock: true },
|
|
15
|
+
{ id: "agents", order: 50, label: "Agents", trackedByLock: true },
|
|
16
|
+
{ id: "workflows", order: 60, label: "Workflows", trackedByLock: true },
|
|
17
|
+
{ id: "skills-controllers", order: 70, label: "Skills and controllers", trackedByLock: true },
|
|
18
|
+
{ id: "mcp-config", order: 80, label: "MCP config", trackedByLock: false },
|
|
19
|
+
{ id: "lock-file", order: 90, label: "Lock file", trackedByLock: false },
|
|
20
|
+
];
|
|
21
|
+
const ARTIFACT_BY_ID = new Map(SYNC_OUTPUT_ARTIFACT_REGISTRY.map((entry) => [entry.id, entry]));
|
|
22
|
+
export function getSyncOutputArtifactMetadata(id) {
|
|
23
|
+
return typeof id === "string" ? ARTIFACT_BY_ID.get(id) : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function syncOutputArtifactIds() {
|
|
26
|
+
return SYNC_OUTPUT_ARTIFACT_REGISTRY
|
|
27
|
+
.slice()
|
|
28
|
+
.sort((a, b) => a.order - b.order)
|
|
29
|
+
.map((entry) => entry.id);
|
|
30
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* See docs/references/cc-source-analysis.md §2 for the full hook event catalog.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import { type SettingsHookEventKey } from "../hooks/event-registry.js";
|
|
10
|
+
export type HookEventKey = SettingsHookEventKey;
|
|
10
11
|
export interface SettingsHookEntry {
|
|
11
12
|
matcher: string;
|
|
12
13
|
hooks: Array<{
|
|
@@ -7,17 +7,8 @@
|
|
|
7
7
|
* See docs/references/cc-source-analysis.md §2 for the full hook event catalog.
|
|
8
8
|
*/
|
|
9
9
|
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { getHookEventBySettingsKey, settingsHookEventKeys } from "../hooks/event-registry.js";
|
|
10
11
|
// ── Plugin Mode (Node.js hooks via dna-hook binary) ───────
|
|
11
|
-
/** Hook event key → Claude Code settings event name */
|
|
12
|
-
const HOOK_EVENT_MAP = {
|
|
13
|
-
preToolUse: "PreToolUse",
|
|
14
|
-
postToolUse: "PostToolUse",
|
|
15
|
-
userPromptSubmit: "UserPromptSubmit",
|
|
16
|
-
subagentStop: "SubagentStop",
|
|
17
|
-
preCompact: "PreCompact",
|
|
18
|
-
notification: "Notification",
|
|
19
|
-
stop: "Stop",
|
|
20
|
-
};
|
|
21
12
|
/**
|
|
22
13
|
* Compile settings.json hook configuration for dna-hook binary.
|
|
23
14
|
* Uses `dna-hook <event>` commands.
|
|
@@ -25,7 +16,7 @@ const HOOK_EVENT_MAP = {
|
|
|
25
16
|
export function compilePluginSettings(enabledEvents, timeout = 10, timeouts) {
|
|
26
17
|
const settings = { hooks: {} };
|
|
27
18
|
for (const key of enabledEvents) {
|
|
28
|
-
const eventName =
|
|
19
|
+
const eventName = getHookEventBySettingsKey(key)?.event;
|
|
29
20
|
if (!eventName)
|
|
30
21
|
continue;
|
|
31
22
|
const eventTimeout = timeouts?.[key] ?? timeout;
|
|
@@ -120,7 +111,8 @@ export async function mergeSettingsFile(dnaSettings, settingsPath) {
|
|
|
120
111
|
mergedHooks[eventName] = [...nonDNA, ...entries];
|
|
121
112
|
}
|
|
122
113
|
// Remove DNA hook events that are no longer generated
|
|
123
|
-
for (const
|
|
114
|
+
for (const key of settingsHookEventKeys()) {
|
|
115
|
+
const eventName = getHookEventBySettingsKey(key).event;
|
|
124
116
|
if (!dnaSettings.hooks[eventName] && mergedHooks[eventName]) {
|
|
125
117
|
const entries = mergedHooks[eventName];
|
|
126
118
|
const nonDNA = entries.filter((h) => {
|
|
@@ -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,15 +7,26 @@
|
|
|
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";
|
|
11
|
-
import {
|
|
10
|
+
import { join, resolve, sep } from "node:path";
|
|
11
|
+
import { getControllerKindMetadata } from "../schema/controller-registry.js";
|
|
12
|
+
import { assertSafeGeneratedName, toKebabCase } from "./agent-md.js";
|
|
12
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
|
+
}
|
|
13
22
|
// ── Compile Controller → Skill ─────────────────────────────
|
|
14
23
|
export function compileControllerToSkill(controllerKey, controller, variables, workflows) {
|
|
15
|
-
|
|
24
|
+
const kindMetadata = getControllerKindMetadata(controller.kind);
|
|
25
|
+
if (!kindMetadata?.supportsSkillGeneration) {
|
|
16
26
|
throw new Error(`Unsupported controller kind: ${controller.kind}`);
|
|
17
27
|
}
|
|
18
28
|
const skillName = `dna-${toKebabCase(controllerKey)}`;
|
|
29
|
+
assertSafeGeneratedName(skillName, "skill name");
|
|
19
30
|
const analyzerAgent = `dna-${toKebabCase(controller.roles.analyzer)}`;
|
|
20
31
|
const analysisReviewerAgent = `dna-${toKebabCase(controller.roles.analysis_reviewer)}`;
|
|
21
32
|
const surgeonAgent = `dna-${toKebabCase(controller.roles.surgeon)}`;
|
|
@@ -134,6 +145,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
134
145
|
lines.push("</Verdict_Mapping>");
|
|
135
146
|
lines.push("");
|
|
136
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.");
|
|
137
149
|
for (const line of policy.stop_report) {
|
|
138
150
|
lines.push(line);
|
|
139
151
|
}
|
|
@@ -153,7 +165,28 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
153
165
|
}
|
|
154
166
|
const dirName = skillName;
|
|
155
167
|
const fileName = join(dirName, "SKILL.md");
|
|
156
|
-
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
|
+
});
|
|
157
190
|
}
|
|
158
191
|
function requireStepByRole(workflow, role, workflowName) {
|
|
159
192
|
const step = workflow?.steps.find((candidate) => candidate.role === role);
|
|
@@ -162,6 +195,73 @@ function requireStepByRole(workflow, role, workflowName) {
|
|
|
162
195
|
}
|
|
163
196
|
return step;
|
|
164
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
|
+
}
|
|
165
265
|
function normalizeControllerPolicy(controller) {
|
|
166
266
|
const defaults = defaultControllerPolicy(controller);
|
|
167
267
|
const policy = {
|
|
@@ -232,7 +332,7 @@ function defaultControllerPolicy(controller) {
|
|
|
232
332
|
"- MAX_ROUNDS: stop and report max rounds reached",
|
|
233
333
|
],
|
|
234
334
|
stop_report: [
|
|
235
|
-
"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.",
|
|
236
336
|
],
|
|
237
337
|
constraints: [
|
|
238
338
|
`The controller must schedule only from ${controller.progress_artifact_path}.`,
|
|
@@ -245,9 +345,46 @@ function defaultControllerPolicy(controller) {
|
|
|
245
345
|
* Compile a WorkflowPlan + roles + IR into a SKILL.md file.
|
|
246
346
|
* Variables from DNA config are substituted into prompts and descriptions.
|
|
247
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
|
+
}
|
|
248
384
|
export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
249
385
|
const lines = [];
|
|
250
386
|
const skillName = `dna-${toKebabCase(plan.name)}`;
|
|
387
|
+
assertSafeGeneratedName(skillName, "skill name");
|
|
251
388
|
// Frontmatter
|
|
252
389
|
lines.push("---");
|
|
253
390
|
lines.push(`name: ${skillName}`);
|
|
@@ -339,6 +476,22 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
339
476
|
lines.push("</Required_Context>");
|
|
340
477
|
lines.push("");
|
|
341
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
|
+
}
|
|
342
495
|
// Steps — each step MUST be executed via Agent() tool call
|
|
343
496
|
const usedRoles = new Set(plan.steps.map((s) => s.role));
|
|
344
497
|
// Pre-compute handoff targets: steps that are pointed to by another step's handoff_to
|
|
@@ -487,6 +640,8 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
487
640
|
const stepsWithHandoff = plan.steps.filter((s) => s.handoff && (s.handoff.consumes?.length || s.handoff.produces?.length));
|
|
488
641
|
if (stepsWithHandoff.length > 0) {
|
|
489
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.");
|
|
490
645
|
for (const step of stepsWithHandoff) {
|
|
491
646
|
const h = step.handoff;
|
|
492
647
|
if (h.produces && h.produces.length > 0) {
|
|
@@ -530,7 +685,12 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
530
685
|
}
|
|
531
686
|
const dirName = skillName;
|
|
532
687
|
const fileName = join(dirName, "SKILL.md");
|
|
533
|
-
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
|
+
});
|
|
534
694
|
}
|
|
535
695
|
/** Collect all {{var_name}} references from a workflow plan. */
|
|
536
696
|
function collectVariableRefs(plan) {
|
|
@@ -553,12 +713,29 @@ function collectVariableRefs(plan) {
|
|
|
553
713
|
// ── File Operations ────────────────────────────────────────
|
|
554
714
|
export async function writeSkillFiles(results, outputDir) {
|
|
555
715
|
const written = [];
|
|
716
|
+
const mapEntries = [];
|
|
556
717
|
for (const result of results) {
|
|
557
|
-
|
|
718
|
+
assertSafeGeneratedName(result.dirName, "skill directory");
|
|
719
|
+
const dir = resolveContainedPath(outputDir, result.dirName);
|
|
558
720
|
await mkdir(dir, { recursive: true });
|
|
559
|
-
const
|
|
560
|
-
await writeFile(
|
|
561
|
-
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);
|
|
562
739
|
}
|
|
563
740
|
return written;
|
|
564
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
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ControllerKind } from "./types.js";
|
|
2
|
+
export declare const CONTROLLER_ROLE_KEYS: readonly ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
3
|
+
export type ControllerRoleKey = typeof CONTROLLER_ROLE_KEYS[number];
|
|
4
|
+
export interface ControllerKindMetadata {
|
|
5
|
+
kind: ControllerKind;
|
|
6
|
+
label: string;
|
|
7
|
+
supportsSkillGeneration: boolean;
|
|
8
|
+
requiredRoleKeys: readonly ControllerRoleKey[];
|
|
9
|
+
diagnosisRoleKeys: readonly ControllerRoleKey[];
|
|
10
|
+
fixRoleKeys: readonly ControllerRoleKey[];
|
|
11
|
+
}
|
|
12
|
+
export declare const CONTROLLER_KIND_REGISTRY: readonly [{
|
|
13
|
+
readonly kind: "fix_loop";
|
|
14
|
+
readonly label: "Fix loop";
|
|
15
|
+
readonly supportsSkillGeneration: true;
|
|
16
|
+
readonly requiredRoleKeys: readonly ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
17
|
+
readonly diagnosisRoleKeys: readonly ["analyzer", "analysis_reviewer"];
|
|
18
|
+
readonly fixRoleKeys: readonly ["surgeon", "fix_reviewer", "test_runner"];
|
|
19
|
+
}, {
|
|
20
|
+
readonly kind: "flutter_rewrite_fix_loop";
|
|
21
|
+
readonly label: "Flutter rewrite fix loop";
|
|
22
|
+
readonly supportsSkillGeneration: true;
|
|
23
|
+
readonly requiredRoleKeys: readonly ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
24
|
+
readonly diagnosisRoleKeys: readonly ["analyzer", "analysis_reviewer"];
|
|
25
|
+
readonly fixRoleKeys: readonly ["surgeon", "fix_reviewer", "test_runner"];
|
|
26
|
+
}];
|
|
27
|
+
export declare function getControllerKindMetadata(kind: unknown): ControllerKindMetadata | undefined;
|
|
28
|
+
export declare function isSupportedControllerKind(kind: unknown): kind is ControllerKind;
|
|
29
|
+
export declare function supportedControllerKinds(): ControllerKind[];
|