intentdna 1.8.3 → 1.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/dist/cli/commands/assets.d.ts +1 -0
- package/dist/cli/commands/assets.js +51 -17
- package/dist/cli/commands/sync.js +10 -8
- package/dist/cli/index.js +0 -0
- package/dist/hooks/cli.d.ts +3 -0
- package/dist/hooks/cli.js +108 -15
- package/dist/hooks/event-registry.js +1 -1
- package/dist/hooks/state.d.ts +2 -0
- package/dist/hooks/state.js +4 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-artifacts.d.ts +2 -0
- package/dist/mcp/tools-artifacts.js +137 -0
- package/dist/runtime/claude-sync-target.d.ts +3 -1
- package/dist/runtime/claude-sync-target.js +16 -4
- package/dist/runtime/settings-adapter.d.ts +12 -0
- package/dist/runtime/settings-adapter.js +121 -3
- package/dist/runtime/skill-adapter.js +34 -4
- package/dist/runtime/verifier.d.ts +3 -1
- package/dist/runtime/verifier.js +82 -27
- package/dist/templates/flutter-rewrite.dna.yaml +19 -9
- package/hooks/hooks.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { errorResult, textResult } from "./server.js";
|
|
5
|
+
function sha256(value) {
|
|
6
|
+
return createHash("sha256").update(value).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
function isContained(root, target) {
|
|
9
|
+
return target === root || target.startsWith(`${root}${sep}`);
|
|
10
|
+
}
|
|
11
|
+
function canonicalJson(value) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
const record = value;
|
|
16
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(value) ?? "null";
|
|
19
|
+
}
|
|
20
|
+
function decodeJsonPointer(pointer) {
|
|
21
|
+
if (!pointer.startsWith("/") || /~(?:[^01]|$)/.test(pointer)) {
|
|
22
|
+
throw new Error("exclude_json_pointers must contain valid non-root JSON pointers");
|
|
23
|
+
}
|
|
24
|
+
return pointer.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
25
|
+
}
|
|
26
|
+
function omitJsonPointer(value, pointer) {
|
|
27
|
+
const segments = decodeJsonPointer(pointer);
|
|
28
|
+
let cursor = value;
|
|
29
|
+
for (const segment of segments.slice(0, -1)) {
|
|
30
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor) || !Object.hasOwn(cursor, segment)) {
|
|
31
|
+
throw new Error(`JSON pointer does not identify an object member: ${pointer}`);
|
|
32
|
+
}
|
|
33
|
+
cursor = cursor[segment];
|
|
34
|
+
}
|
|
35
|
+
const leaf = segments.at(-1);
|
|
36
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor) || !Object.hasOwn(cursor, leaf)) {
|
|
37
|
+
throw new Error(`JSON pointer does not identify an object member: ${pointer}`);
|
|
38
|
+
}
|
|
39
|
+
delete cursor[leaf];
|
|
40
|
+
}
|
|
41
|
+
async function resolveArtifact(projectDir, requestedPath) {
|
|
42
|
+
if (!requestedPath || requestedPath.includes("\0") || isAbsolute(requestedPath)) {
|
|
43
|
+
throw new Error("path must be a non-empty project-relative path");
|
|
44
|
+
}
|
|
45
|
+
const root = await realpath(projectDir);
|
|
46
|
+
const lexicalTarget = resolve(root, requestedPath);
|
|
47
|
+
if (!isContained(root, lexicalTarget))
|
|
48
|
+
throw new Error("path escapes project directory");
|
|
49
|
+
let absolutePath;
|
|
50
|
+
try {
|
|
51
|
+
absolutePath = await realpath(lexicalTarget);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error("artifact is not a readable regular file");
|
|
55
|
+
}
|
|
56
|
+
if (!isContained(root, absolutePath))
|
|
57
|
+
throw new Error("path resolves outside project directory");
|
|
58
|
+
const artifactStat = await stat(absolutePath);
|
|
59
|
+
if (!artifactStat.isFile())
|
|
60
|
+
throw new Error("artifact is not a readable regular file");
|
|
61
|
+
return {
|
|
62
|
+
absolutePath,
|
|
63
|
+
projectRelativePath: relative(root, lexicalTarget).split(sep).join("/"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function createArtifactTools(projectDir) {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
name: "dna_artifact_digest",
|
|
70
|
+
description: "Read-only SHA-256 digest for one project-relative regular file. Supports raw bytes, whitespace-normalized text, and canonical JSON with explicit object-member exclusions; refuses project escapes and writes nothing.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
path: { type: "string", description: "Project-relative path to a regular file" },
|
|
75
|
+
mode: {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["bytes", "whitespace_normalized_text", "canonical_json"],
|
|
78
|
+
description: "Digest representation (default: bytes)",
|
|
79
|
+
},
|
|
80
|
+
exclude_json_pointers: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: { type: "string" },
|
|
83
|
+
description: "For canonical_json only, object members to omit using RFC 6901 JSON pointers",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["path"],
|
|
87
|
+
},
|
|
88
|
+
handler: async (args) => {
|
|
89
|
+
try {
|
|
90
|
+
const requestedPath = typeof args.path === "string" ? args.path : "";
|
|
91
|
+
const mode = (args.mode ?? "bytes");
|
|
92
|
+
if (!["bytes", "whitespace_normalized_text", "canonical_json"].includes(mode)) {
|
|
93
|
+
return errorResult("mode must be bytes, whitespace_normalized_text, or canonical_json");
|
|
94
|
+
}
|
|
95
|
+
const excluded = args.exclude_json_pointers === undefined
|
|
96
|
+
? []
|
|
97
|
+
: Array.isArray(args.exclude_json_pointers) && args.exclude_json_pointers.every((item) => typeof item === "string")
|
|
98
|
+
? args.exclude_json_pointers
|
|
99
|
+
: undefined;
|
|
100
|
+
if (!excluded)
|
|
101
|
+
return errorResult("exclude_json_pointers must be an array of strings");
|
|
102
|
+
if (mode !== "canonical_json" && excluded.length > 0) {
|
|
103
|
+
return errorResult("exclude_json_pointers is only valid with canonical_json mode");
|
|
104
|
+
}
|
|
105
|
+
const artifact = await resolveArtifact(projectDir, requestedPath);
|
|
106
|
+
const content = await readFile(artifact.absolutePath);
|
|
107
|
+
const artifactStat = await stat(artifact.absolutePath);
|
|
108
|
+
const contentSha256 = sha256(content);
|
|
109
|
+
let digest = contentSha256;
|
|
110
|
+
if (mode === "whitespace_normalized_text") {
|
|
111
|
+
digest = sha256(content.toString("utf-8").replace(/\s+/g, " ").trim());
|
|
112
|
+
}
|
|
113
|
+
else if (mode === "canonical_json") {
|
|
114
|
+
const parsed = JSON.parse(content.toString("utf-8"));
|
|
115
|
+
for (const pointer of excluded)
|
|
116
|
+
omitJsonPointer(parsed, pointer);
|
|
117
|
+
digest = sha256(canonicalJson(parsed));
|
|
118
|
+
}
|
|
119
|
+
return textResult(JSON.stringify({
|
|
120
|
+
schema_version: "intentdna.artifact_digest.v1",
|
|
121
|
+
path: artifact.projectRelativePath,
|
|
122
|
+
mode,
|
|
123
|
+
sha256: digest,
|
|
124
|
+
content_sha256: contentSha256,
|
|
125
|
+
size_bytes: content.byteLength,
|
|
126
|
+
modified_at: artifactStat.mtime.toISOString(),
|
|
127
|
+
excluded_json_pointers: excluded,
|
|
128
|
+
read_only: true,
|
|
129
|
+
}, null, 2));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return errorResult(error instanceof Error ? error.message : "artifact digest failed");
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConstraintIR, RoleDef } from "../schema/types.js";
|
|
2
2
|
import type { SyncOutputArtifactId } from "./output-artifact-registry.js";
|
|
3
3
|
import { type CompiledIRFile } from "./plugin-adapter.js";
|
|
4
|
-
import { type DNASettings, type HookEventKey } from "./settings-adapter.js";
|
|
4
|
+
import { type DNASettings, type HookEventKey, type IntentDNASettingsHookCleanupResult } from "./settings-adapter.js";
|
|
5
5
|
import type { AdapterCapabilityReport, CapabilityReportEntry, PlannedArtifact, SyncTargetPlan } from "./sync-target-plan.js";
|
|
6
6
|
export type ClaudeSyncMode = "plugin" | "bin";
|
|
7
7
|
export interface ClaudeCorePlannedArtifact extends PlannedArtifact {
|
|
@@ -31,6 +31,7 @@ export interface ClaudeCoreSyncTargetPlan extends SyncTargetPlan {
|
|
|
31
31
|
legacyHooksDir: string;
|
|
32
32
|
mcpConfigPath: string;
|
|
33
33
|
settingsHookRegistration?: ClaudeSettingsHookRegistration;
|
|
34
|
+
pluginSettingsPath?: string;
|
|
34
35
|
pluginSkipsSettings: boolean;
|
|
35
36
|
plannedArtifacts: ClaudeCorePlannedArtifact[];
|
|
36
37
|
capabilityReport: ClaudeCoreAdapterCapabilityReport;
|
|
@@ -38,6 +39,7 @@ export interface ClaudeCoreSyncTargetPlan extends SyncTargetPlan {
|
|
|
38
39
|
export interface ExecuteClaudeCoreSyncTargetPlanResult {
|
|
39
40
|
messages: string[];
|
|
40
41
|
removedLegacyHooks: string[];
|
|
42
|
+
settingsHookCleanup?: IntentDNASettingsHookCleanupResult;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Remove legacy bash hook scripts written by previous Intent DNA versions.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { createCompiledIR, writeCompiledIR } from "./plugin-adapter.js";
|
|
4
|
-
import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "./settings-adapter.js";
|
|
4
|
+
import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts, removeIntentDNASettingsHooks, } from "./settings-adapter.js";
|
|
5
5
|
import { formatSyncTargetPlanDiagnostics } from "./sync-target-plan.js";
|
|
6
6
|
const LEGACY_BASH_HOOKS = [
|
|
7
7
|
"dna-pre-tool-use.sh",
|
|
@@ -83,6 +83,7 @@ export function createClaudeCoreSyncTargetPlan(options) {
|
|
|
83
83
|
legacyHooksDir,
|
|
84
84
|
mcpConfigPath,
|
|
85
85
|
settingsHookRegistration,
|
|
86
|
+
pluginSettingsPath: options.mode === "plugin" ? options.settingsPath : undefined,
|
|
86
87
|
pluginSkipsSettings: options.mode === "plugin" && !!options.settingsPath,
|
|
87
88
|
plannedArtifacts,
|
|
88
89
|
capabilityReport: createClaudeCapabilityReport(plannedArtifacts, options.mode, settingsHookRegistration),
|
|
@@ -238,6 +239,7 @@ async function registerDNAMCPServer(projectDir) {
|
|
|
238
239
|
}
|
|
239
240
|
export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
|
|
240
241
|
const messages = [];
|
|
242
|
+
let settingsHookCleanup;
|
|
241
243
|
const writtenIrPath = await writeCompiledIR(projectDir, plan.compiledIR);
|
|
242
244
|
messages.push(`Wrote compiled IR: ${writtenIrPath}`);
|
|
243
245
|
const removedLegacyHooks = await removeLegacyBashHooks(plan.legacyHooksDir);
|
|
@@ -253,9 +255,19 @@ export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
|
|
|
253
255
|
messages.push(`Bin mode: registered ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
|
|
254
256
|
}
|
|
255
257
|
else if (plan.pluginSkipsSettings) {
|
|
256
|
-
|
|
258
|
+
settingsHookCleanup = await removeIntentDNASettingsHooks(plan.pluginSettingsPath);
|
|
259
|
+
if (settingsHookCleanup.removedCommands > 0) {
|
|
260
|
+
const action = settingsHookCleanup.removedFile ? "removed" : "cleaned";
|
|
261
|
+
messages.push(`Plugin mode: ${action} ${settingsHookCleanup.removedCommands} stale bin-mode dna-hook command(s) from ${plan.pluginSettingsPath}; hooks are managed by plugin framework`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
messages.push("Plugin mode: hooks managed by plugin framework (settings.json not written)");
|
|
265
|
+
}
|
|
266
|
+
if (settingsHookCleanup.warning) {
|
|
267
|
+
messages.push(`Plugin mode: preserved settings.json without cleanup (${settingsHookCleanup.warning})`);
|
|
268
|
+
}
|
|
257
269
|
}
|
|
258
|
-
return { messages, removedLegacyHooks };
|
|
270
|
+
return { messages, removedLegacyHooks, settingsHookCleanup };
|
|
259
271
|
}
|
|
260
272
|
export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
|
|
261
273
|
const lines = [
|
|
@@ -267,7 +279,7 @@ export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
|
|
|
267
279
|
lines.push(`Dry-run: would register ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
|
|
268
280
|
}
|
|
269
281
|
else if (plan.pluginSkipsSettings) {
|
|
270
|
-
lines.push("Plugin mode: hooks managed by plugin framework
|
|
282
|
+
lines.push("Plugin mode: hooks managed by plugin framework; would remove stale bin-mode dna-hook commands if present and would not register settings hooks");
|
|
271
283
|
}
|
|
272
284
|
const capabilityEntries = Object.entries(plan.capabilityReport.capabilities)
|
|
273
285
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
@@ -18,6 +18,13 @@ export interface SettingsHookEntry {
|
|
|
18
18
|
export interface DNASettings {
|
|
19
19
|
hooks: Record<string, SettingsHookEntry[]>;
|
|
20
20
|
}
|
|
21
|
+
export interface IntentDNASettingsHookCleanupResult {
|
|
22
|
+
changed: boolean;
|
|
23
|
+
removedCommands: number;
|
|
24
|
+
removedEvents: string[];
|
|
25
|
+
removedFile: boolean;
|
|
26
|
+
warning?: string;
|
|
27
|
+
}
|
|
21
28
|
/**
|
|
22
29
|
* Compile settings.json hook configuration for dna-hook binary.
|
|
23
30
|
* Uses `dna-hook <event>` commands.
|
|
@@ -34,3 +41,8 @@ export declare function detectEnabledEvents(ir: import("../schema/types.js").Con
|
|
|
34
41
|
* Replaces DNA hooks (identified by "dna-" prefix), keeps user hooks.
|
|
35
42
|
*/
|
|
36
43
|
export declare function mergeSettingsFile(dnaSettings: DNASettings, settingsPath: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Remove bin-mode IntentDNA hook commands when Claude plugin hooks own dispatch.
|
|
46
|
+
* Invalid or user-owned settings are preserved without rewriting the file.
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeIntentDNASettingsHooks(settingsPath: string): Promise<IntentDNASettingsHookCleanupResult>;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* See docs/archive/2026H1/references/cc-source-analysis.md §2 for the full hook event catalog.
|
|
8
8
|
*/
|
|
9
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
10
10
|
import { getHookEventBySettingsKey, settingsHookEventKeys } from "../hooks/event-registry.js";
|
|
11
|
-
// ──
|
|
11
|
+
// ── Bin Mode (project settings via dna-hook binary) ───────
|
|
12
12
|
/**
|
|
13
13
|
* Compile settings.json hook configuration for dna-hook binary.
|
|
14
14
|
* Uses `dna-hook <event>` commands.
|
|
@@ -46,6 +46,7 @@ export function recommendHookTimeouts(ir, baseTimeout = 10) {
|
|
|
46
46
|
spec.checkpoint?.assert === "no_test_regression")) ?? false;
|
|
47
47
|
if (hasStopCommandVerifiers) {
|
|
48
48
|
timeouts.stop = Math.max(baseTimeout, 45);
|
|
49
|
+
timeouts.subagentStop = Math.max(baseTimeout, 45);
|
|
49
50
|
}
|
|
50
51
|
return timeouts;
|
|
51
52
|
}
|
|
@@ -70,7 +71,7 @@ export function detectEnabledEvents(ir) {
|
|
|
70
71
|
if (ir.prompt_directives.some(d => d.priority === "high")) {
|
|
71
72
|
events.push("userPromptSubmit");
|
|
72
73
|
}
|
|
73
|
-
if (hasRoleScope) {
|
|
74
|
+
if (hasRoleScope || hasStopVerifiers) {
|
|
74
75
|
events.push("subagentStop");
|
|
75
76
|
}
|
|
76
77
|
if (ir.prompt_directives.length > 0) {
|
|
@@ -130,3 +131,120 @@ export async function mergeSettingsFile(dnaSettings, settingsPath) {
|
|
|
130
131
|
existing.hooks = mergedHooks;
|
|
131
132
|
await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
132
133
|
}
|
|
134
|
+
const INTENTDNA_SETTINGS_HOOK_COMMANDS = new Set(settingsHookEventKeys().map((key) => `dna-hook ${getHookEventBySettingsKey(key).event}`));
|
|
135
|
+
function isIntentDNASettingsHookCommand(value, eventName) {
|
|
136
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
137
|
+
return false;
|
|
138
|
+
const hook = value;
|
|
139
|
+
const expectedCommand = `dna-hook ${eventName}`;
|
|
140
|
+
return hook.type === "command"
|
|
141
|
+
&& typeof hook.command === "string"
|
|
142
|
+
&& hook.command === expectedCommand
|
|
143
|
+
&& INTENTDNA_SETTINGS_HOOK_COMMANDS.has(expectedCommand);
|
|
144
|
+
}
|
|
145
|
+
function isGeneratedIntentDNASettingsHookEntry(entry) {
|
|
146
|
+
return entry.matcher === ""
|
|
147
|
+
&& Object.keys(entry).every((key) => key === "matcher" || key === "hooks");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Remove bin-mode IntentDNA hook commands when Claude plugin hooks own dispatch.
|
|
151
|
+
* Invalid or user-owned settings are preserved without rewriting the file.
|
|
152
|
+
*/
|
|
153
|
+
export async function removeIntentDNASettingsHooks(settingsPath) {
|
|
154
|
+
const unchanged = (warning) => ({
|
|
155
|
+
changed: false,
|
|
156
|
+
removedCommands: 0,
|
|
157
|
+
removedEvents: [],
|
|
158
|
+
removedFile: false,
|
|
159
|
+
...(warning ? { warning } : {}),
|
|
160
|
+
});
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await readFile(settingsPath, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
167
|
+
return code === "ENOENT" ? unchanged() : unchanged("could not read settings.json");
|
|
168
|
+
}
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(raw);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return unchanged("settings.json is not valid JSON");
|
|
175
|
+
}
|
|
176
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
177
|
+
return unchanged("settings.json root is not an object");
|
|
178
|
+
}
|
|
179
|
+
const settings = parsed;
|
|
180
|
+
const rawHooks = settings.hooks;
|
|
181
|
+
if (!rawHooks || typeof rawHooks !== "object" || Array.isArray(rawHooks)) {
|
|
182
|
+
return unchanged();
|
|
183
|
+
}
|
|
184
|
+
const cleanedHooks = {};
|
|
185
|
+
const removedEvents = [];
|
|
186
|
+
let removedCommands = 0;
|
|
187
|
+
for (const [eventName, rawEntries] of Object.entries(rawHooks)) {
|
|
188
|
+
if (!Array.isArray(rawEntries)) {
|
|
189
|
+
cleanedHooks[eventName] = rawEntries;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let eventRemoved = false;
|
|
193
|
+
const cleanedEntries = [];
|
|
194
|
+
for (const rawEntry of rawEntries) {
|
|
195
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
196
|
+
cleanedEntries.push(rawEntry);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const entry = rawEntry;
|
|
200
|
+
if (!Array.isArray(entry.hooks)) {
|
|
201
|
+
cleanedEntries.push(rawEntry);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let entryRemovedCommands = 0;
|
|
205
|
+
const retainedCommands = entry.hooks.filter((hook) => {
|
|
206
|
+
const owned = isIntentDNASettingsHookCommand(hook, eventName);
|
|
207
|
+
if (owned) {
|
|
208
|
+
removedCommands += 1;
|
|
209
|
+
entryRemovedCommands += 1;
|
|
210
|
+
eventRemoved = true;
|
|
211
|
+
}
|
|
212
|
+
return !owned;
|
|
213
|
+
});
|
|
214
|
+
if (entryRemovedCommands === 0) {
|
|
215
|
+
cleanedEntries.push(rawEntry);
|
|
216
|
+
}
|
|
217
|
+
else if (retainedCommands.length > 0 || !isGeneratedIntentDNASettingsHookEntry(entry)) {
|
|
218
|
+
cleanedEntries.push({ ...entry, hooks: retainedCommands });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (eventRemoved)
|
|
222
|
+
removedEvents.push(eventName);
|
|
223
|
+
if (!eventRemoved || cleanedEntries.length > 0)
|
|
224
|
+
cleanedHooks[eventName] = cleanedEntries;
|
|
225
|
+
}
|
|
226
|
+
if (removedCommands === 0)
|
|
227
|
+
return unchanged();
|
|
228
|
+
if (Object.keys(cleanedHooks).length > 0) {
|
|
229
|
+
settings.hooks = cleanedHooks;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
delete settings.hooks;
|
|
233
|
+
}
|
|
234
|
+
if (Object.keys(settings).length === 0) {
|
|
235
|
+
await unlink(settingsPath);
|
|
236
|
+
return {
|
|
237
|
+
changed: true,
|
|
238
|
+
removedCommands,
|
|
239
|
+
removedEvents,
|
|
240
|
+
removedFile: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
244
|
+
return {
|
|
245
|
+
changed: true,
|
|
246
|
+
removedCommands,
|
|
247
|
+
removedEvents,
|
|
248
|
+
removedFile: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -448,10 +448,19 @@ function workflowSkillMapEntry(skillName, dirName, plan) {
|
|
|
448
448
|
produces,
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
|
+
function inferredWorkflowAsset(ir) {
|
|
452
|
+
const sourceIds = [...new Set(ir?.source_dna_ids ?? [])]
|
|
453
|
+
.filter((sourceId) => sourceId.length > 0)
|
|
454
|
+
.filter((sourceId) => !sourceId.startsWith("species:"))
|
|
455
|
+
.filter((sourceId) => !sourceId.startsWith("enterprise:"));
|
|
456
|
+
return sourceIds.length === 1 ? sourceIds[0] : undefined;
|
|
457
|
+
}
|
|
451
458
|
export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
452
459
|
const lines = [];
|
|
453
460
|
const skillName = `dna-${toKebabCase(plan.name)}`;
|
|
454
461
|
const surface = options?.surface ?? "claude_code";
|
|
462
|
+
const workflowId = plan.workflow_key ?? plan.source_workflow;
|
|
463
|
+
const workflowAsset = inferredWorkflowAsset(ir);
|
|
455
464
|
assertSafeGeneratedName(skillName, "skill name");
|
|
456
465
|
// Frontmatter
|
|
457
466
|
lines.push("---");
|
|
@@ -478,9 +487,20 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
478
487
|
lines.push("");
|
|
479
488
|
}
|
|
480
489
|
lines.push("<Workflow_State>");
|
|
481
|
-
lines.push("At workflow start,
|
|
490
|
+
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`.");
|
|
491
|
+
lines.push(`Use workflow id \`${workflowId}\`.`);
|
|
492
|
+
if (workflowAsset) {
|
|
493
|
+
lines.push(`Use workflow_asset \`${workflowAsset}\`; it is the single non-species/non-enterprise IR source id.`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
lines.push("Do not set workflow_asset: IR does not provide exactly one non-species/non-enterprise source id, so the asset cannot be inferred safely.");
|
|
497
|
+
}
|
|
498
|
+
const firstStep = plan.steps[0];
|
|
499
|
+
lines.push(`Initial state write: workflow=${workflowId}, current_step=${firstStep.id}, current_role=${firstStep.role}, iteration=1, active=true.`);
|
|
482
500
|
lines.push("Record the invocation argument as inputs.ARGUMENTS so handoff artifact paths like $ARGUMENTS can be resolved deterministically.");
|
|
483
501
|
lines.push("If template variables are shown below, persist their resolved values as resolved_variables.");
|
|
502
|
+
lines.push("Before each workflow step, call `dna_workflow_write` again with workflow, workflow_asset only when safely inferred above, current_step, current_role, iteration, active=true, inputs.ARGUMENTS, and resolved_variables.");
|
|
503
|
+
lines.push("At workflow completion or terminal failure, call `dna_workflow_write` with active=false using the same workflow id and safe workflow_asset value.");
|
|
484
504
|
lines.push("</Workflow_State>");
|
|
485
505
|
lines.push("");
|
|
486
506
|
// Required Context — context files that must be read before any work
|
|
@@ -607,6 +627,7 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
607
627
|
const agentPrompt = escapePrompt(promptParts.join(" "));
|
|
608
628
|
lines.push(`${stepNumber}. **${step.id}**${optional}`);
|
|
609
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, inputs.ARGUMENTS, resolved_variables, and ${workflowAsset ? `workflow_asset=${workflowAsset}` : "no workflow_asset unless one has been explicitly supplied by trusted workflow state"}.`);
|
|
610
631
|
lines.push("");
|
|
611
632
|
if (humanOwned) {
|
|
612
633
|
lines.push(" **Human-owned gate (`model: human`)**");
|
|
@@ -640,6 +661,11 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
640
661
|
const artifacts = step.handoff.produces.map(p => p.description).join(", ");
|
|
641
662
|
lines.push(` Verify produced artifacts: ${artifacts}`);
|
|
642
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
|
+
}
|
|
643
669
|
lines.push("");
|
|
644
670
|
// Reflection gate: inject Reflection_Gate after steps with max_attempts
|
|
645
671
|
if (step.max_attempts) {
|
|
@@ -648,8 +674,9 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
648
674
|
const blockedPath = step.blocked_items_path ?? "blocked_items.md";
|
|
649
675
|
lines.push(`<Reflection_Gate>`);
|
|
650
676
|
lines.push(`max_attempts=${step.max_attempts}, handoff_to=${handoffStep}, max_handoffs=${maxH}`);
|
|
651
|
-
lines.push(`No
|
|
652
|
-
lines.push(`
|
|
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}.`);
|
|
653
680
|
lines.push(`</Reflection_Gate>`);
|
|
654
681
|
lines.push("");
|
|
655
682
|
}
|
|
@@ -662,7 +689,9 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
662
689
|
lines.push(`- max_retries: ${plan.retry.max_retries}`);
|
|
663
690
|
lines.push(`- retry_from: ${plan.retry.retry_from ?? "first_failed_step"}`);
|
|
664
691
|
lines.push(`- backoff: ${plan.retry.backoff}`);
|
|
665
|
-
lines.push("- On a blocking checkpoint or validator failure, preserve the exact diagnostics and return them to the retry_from step.");
|
|
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.");
|
|
666
695
|
lines.push("- Repeat only the retry slice and its dependent steps; do not continue to save, adoption, or sync while validation is red.");
|
|
667
696
|
lines.push("- Stop and report the remaining diagnostics when the retry budget is exhausted.");
|
|
668
697
|
lines.push("</Retry_Policy>");
|
|
@@ -797,6 +826,7 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
797
826
|
}
|
|
798
827
|
// Workflow Boundary — prevent cross-workflow execution
|
|
799
828
|
lines.push("<Workflow_Boundary>");
|
|
829
|
+
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, and ${workflowAsset ? `workflow_asset=${workflowAsset}` : "no workflow_asset unless one has been explicitly supplied by trusted workflow state"}.`);
|
|
800
830
|
lines.push("This workflow is COMPLETE. Do NOT proceed to any other workflow.");
|
|
801
831
|
lines.push("Report your results and STOP. The user will decide the next step.");
|
|
802
832
|
lines.push("</Workflow_Boundary>");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CompletionCheck, ConstraintIR, StepCheckpoint, VerifierCommandPolicy } from "../schema/types.js";
|
|
2
2
|
export declare const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30000;
|
|
3
3
|
export declare const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
|
|
4
|
+
export declare const VERIFIER_DIAGNOSTIC_PREFIX = "INTENTDNA_DIAGNOSTIC:";
|
|
5
|
+
export declare const MAX_VERIFIER_DIAGNOSTIC_BYTES = 2048;
|
|
4
6
|
export interface VerifierRuntimeContext {
|
|
5
7
|
projectDir: string;
|
|
6
8
|
variables?: Record<string, string>;
|
|
@@ -29,7 +31,7 @@ export declare function hasUnresolvedVerifierTemplate(value: string): boolean;
|
|
|
29
31
|
export declare function unresolvedVerifierTemplateMessage(value: string): string;
|
|
30
32
|
export declare function hasUnsafeShellControl(command: string): boolean;
|
|
31
33
|
export declare function isVerifierCommandAllowed(policy: VerifierCommandPolicy | undefined, command: string): boolean;
|
|
32
|
-
export declare function verifierCommandPolicyMessage(
|
|
34
|
+
export declare function verifierCommandPolicyMessage(_command: string): string;
|
|
33
35
|
export declare function isBuiltinAssertAllowed(policy: VerifierCommandPolicy | undefined, assertName: string): boolean;
|
|
34
36
|
export declare function verifierAssertPolicyMessage(assertName: string): string;
|
|
35
37
|
export declare function trimVerifierEvidence(raw: string | undefined): string | undefined;
|