intentdna 1.8.7 → 1.9.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/cli/commands/candidate-promotion.d.ts +15 -0
  4. package/dist/cli/commands/candidate-promotion.js +197 -0
  5. package/dist/cli/commands/run-lifecycle.js +1 -0
  6. package/dist/cli/commands/run.js +76 -42
  7. package/dist/cli/commands/setup.d.ts +10 -0
  8. package/dist/cli/commands/setup.js +129 -36
  9. package/dist/cli/commands/source-assets.d.ts +32 -0
  10. package/dist/cli/commands/source-assets.js +154 -0
  11. package/dist/cli/commands/sync.js +52 -12
  12. package/dist/cli/commands/templates.js +59 -37
  13. package/dist/cli/index.js +161 -5
  14. package/dist/compiler/cascade.js +1 -1
  15. package/dist/hooks/cli.d.ts +6 -0
  16. package/dist/hooks/cli.js +118 -10
  17. package/dist/hooks/enforcement-boundary.d.ts +1 -1
  18. package/dist/hooks/protocol.d.ts +1 -1
  19. package/dist/hooks/state.d.ts +5 -0
  20. package/dist/hooks/state.js +21 -6
  21. package/dist/mcp/tools-state.js +251 -15
  22. package/dist/runtime/claude-sync-target.d.ts +2 -0
  23. package/dist/runtime/claude-sync-target.js +1 -1
  24. package/dist/runtime/evaluator.d.ts +13 -0
  25. package/dist/runtime/evaluator.js +46 -0
  26. package/dist/runtime/index.d.ts +8 -4
  27. package/dist/runtime/index.js +4 -2
  28. package/dist/runtime/plugin-adapter.d.ts +14 -1
  29. package/dist/runtime/plugin-adapter.js +23 -9
  30. package/dist/runtime/run-contracts.d.ts +5 -3
  31. package/dist/runtime/run-contracts.js +11 -1
  32. package/dist/runtime/run-controller.d.ts +15 -1
  33. package/dist/runtime/run-controller.js +235 -94
  34. package/dist/runtime/skill-adapter.js +24 -53
  35. package/dist/runtime/verifier.js +101 -2
  36. package/dist/runtime/workflow-plan-adapter.d.ts +2 -1
  37. package/dist/runtime/workflow-plan-adapter.js +78 -11
  38. package/dist/runtime/workflow-runtime-manifest.d.ts +48 -0
  39. package/dist/runtime/workflow-runtime-manifest.js +196 -0
  40. package/dist/runtime/workflow-variable-scopes.d.ts +12 -0
  41. package/dist/runtime/workflow-variable-scopes.js +55 -0
  42. package/dist/schema/types.d.ts +9 -0
  43. package/dist/schema/validate.js +29 -1
  44. package/dist/sources/authoring-packet.d.ts +8 -0
  45. package/dist/sources/authoring-packet.js +85 -0
  46. package/dist/sources/discovery.d.ts +7 -0
  47. package/dist/sources/discovery.js +150 -0
  48. package/dist/sources/license.d.ts +5 -0
  49. package/dist/sources/license.js +94 -0
  50. package/dist/sources/manifest.d.ts +11 -0
  51. package/dist/sources/manifest.js +84 -0
  52. package/dist/sources/skill-markdown.d.ts +6 -0
  53. package/dist/sources/skill-markdown.js +174 -0
  54. package/dist/sources/types.d.ts +125 -0
  55. package/dist/sources/types.js +1 -0
  56. package/dist/templates/asset-refinery.dna.yaml +192 -0
  57. package/dist/templates/flutter-behavior-lock.dna.yaml +17 -0
  58. package/dist/templates/flutter-refactoring-rescue.dna.yaml +11 -0
  59. package/dist/templates/flutter-rewrite.dna.yaml +154 -28
  60. package/dist/templates/metadata.js +2 -0
  61. package/dist/templates/multi-agent-handoff-coordination.dna.yaml +102 -37
  62. package/dist/templates/persistent-executor.dna.yaml +61 -3
  63. package/dist/templates/research-improvement.dna.yaml +295 -0
  64. package/dist/templates/subagent-parallel.dna.yaml +90 -17
  65. package/dist/templates/workflow-evidence-review.dna.yaml +8 -0
  66. package/dist/workflow/candidate-projection.d.ts +33 -0
  67. package/dist/workflow/candidate-projection.js +70 -0
  68. package/package.json +3 -1
  69. package/scripts/sync-release-plugin.cjs +87 -0
@@ -79,21 +79,32 @@ async function resolvePackagePath() {
79
79
  }
80
80
  return null;
81
81
  }
82
- /**
83
- * Check if intentdna is already registered as a plugin.
84
- */
85
- function isPluginRegistered() {
82
+ export function parseInstalledPluginInfo(output) {
83
+ const pluginId = "intentdna@intentdna";
84
+ const pluginStart = output.indexOf(pluginId);
85
+ if (pluginStart === -1)
86
+ return null;
87
+ const block = output.slice(pluginStart).split(/\n\s*\n/, 1)[0];
88
+ return {
89
+ version: block.match(/Version:\s*([^\s]+)/)?.[1] ?? null,
90
+ enabled: /Status:\s*.*\benabled\b/i.test(block),
91
+ };
92
+ }
93
+ function getInstalledPluginInfo() {
86
94
  try {
87
95
  const result = execSync("claude plugin list 2>/dev/null", {
88
96
  encoding: "utf-8",
89
97
  timeout: 5000,
90
98
  });
91
- return result.includes("intentdna");
99
+ return parseInstalledPluginInfo(result);
92
100
  }
93
101
  catch {
94
- return false;
102
+ return null;
95
103
  }
96
104
  }
105
+ function isPluginRegistered() {
106
+ return getInstalledPluginInfo() !== null;
107
+ }
97
108
  // ── Main ──────────────────────────────────────────────────
98
109
  /**
99
110
  * Get the currently installed version of intentdna.
@@ -111,12 +122,10 @@ function getInstalledVersion() {
111
122
  return null;
112
123
  }
113
124
  }
114
- /**
115
- * Get the latest published version from npm registry.
116
- */
117
- function getLatestVersion() {
125
+ /** Get the version currently published on an npm dist-tag. */
126
+ function getPublishedVersion(channel) {
118
127
  try {
119
- return execSync("npm view intentdna version 2>/dev/null", {
128
+ return execSync(`npm view intentdna@${channel} version 2>/dev/null`, {
120
129
  encoding: "utf-8",
121
130
  timeout: 10000,
122
131
  }).trim();
@@ -125,8 +134,63 @@ function getLatestVersion() {
125
134
  return null;
126
135
  }
127
136
  }
137
+ function parseReleaseVersion(version) {
138
+ const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
139
+ if (!match)
140
+ return null;
141
+ return {
142
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
143
+ prerelease: match[4]?.split(".") ?? null,
144
+ };
145
+ }
146
+ function comparePrerelease(left, right) {
147
+ if (left === null && right === null)
148
+ return 0;
149
+ if (left === null)
150
+ return 1;
151
+ if (right === null)
152
+ return -1;
153
+ const length = Math.max(left.length, right.length);
154
+ for (let index = 0; index < length; index += 1) {
155
+ const leftPart = left[index];
156
+ const rightPart = right[index];
157
+ if (leftPart === undefined)
158
+ return -1;
159
+ if (rightPart === undefined)
160
+ return 1;
161
+ if (leftPart === rightPart)
162
+ continue;
163
+ const leftNumeric = /^\d+$/.test(leftPart);
164
+ const rightNumeric = /^\d+$/.test(rightPart);
165
+ if (leftNumeric && rightNumeric)
166
+ return Number(leftPart) < Number(rightPart) ? -1 : 1;
167
+ if (leftNumeric !== rightNumeric)
168
+ return leftNumeric ? -1 : 1;
169
+ return leftPart < rightPart ? -1 : 1;
170
+ }
171
+ return 0;
172
+ }
173
+ export function releaseChannelForVersion(version) {
174
+ if (!version)
175
+ return "latest";
176
+ return parseReleaseVersion(version)?.prerelease ? "next" : "latest";
177
+ }
178
+ export function shouldInstallLatestVersion(installedVersion, latestVersion) {
179
+ if (installedVersion === latestVersion)
180
+ return false;
181
+ const installed = parseReleaseVersion(installedVersion);
182
+ const latest = parseReleaseVersion(latestVersion);
183
+ if (!installed || !latest)
184
+ return true;
185
+ for (let index = 0; index < installed.core.length; index += 1) {
186
+ if (installed.core[index] === latest.core[index])
187
+ continue;
188
+ return installed.core[index] < latest.core[index];
189
+ }
190
+ return comparePrerelease(installed.prerelease, latest.prerelease) < 0;
191
+ }
128
192
  /**
129
- * Upgrade flow: npm update → re-register marketplace → re-install plugin.
193
+ * Upgrade flow: install from the current release channel → refresh marketplace → update and verify plugin.
130
194
  */
131
195
  async function runUpgrade(opts) {
132
196
  // Step 1: Detect claude CLI
@@ -138,20 +202,26 @@ async function runUpgrade(opts) {
138
202
  log(`Claude Code detected: ${claudeVersion}`);
139
203
  // Step 2: Check versions
140
204
  const currentVer = getInstalledVersion();
141
- const latestVer = getLatestVersion();
205
+ const releaseChannel = releaseChannelForVersion(currentVer);
206
+ const publishedVer = getPublishedVersion(releaseChannel);
142
207
  if (currentVer)
143
208
  log(`Current version: ${currentVer}`);
144
- if (latestVer)
145
- log(`Latest version: ${latestVer}`);
146
- if (currentVer && latestVer && currentVer === latestVer) {
147
- log("Already at latest version.");
209
+ log(`Target channel: ${releaseChannel}`);
210
+ if (publishedVer)
211
+ log(`Target version: ${publishedVer}`);
212
+ if (!publishedVer) {
213
+ log(`Upgrade failed: could not resolve intentdna@${releaseChannel} from npm.`);
214
+ return 1;
215
+ }
216
+ if (currentVer && !shouldInstallLatestVersion(currentVer, publishedVer)) {
217
+ log(`Already at the ${releaseChannel} channel version or newer; refusing to downgrade.`);
148
218
  }
149
219
  else {
150
220
  // Step 3: npm update
151
221
  log("");
152
- log("Updating intentdna...");
222
+ log(`Updating intentdna from the ${releaseChannel} channel...`);
153
223
  try {
154
- execSync("npm update -g intentdna", {
224
+ execSync(`npm install -g intentdna@${releaseChannel}`, {
155
225
  encoding: "utf-8",
156
226
  timeout: 60000,
157
227
  stdio: ["inherit", "pipe", "pipe"],
@@ -162,10 +232,19 @@ async function runUpgrade(opts) {
162
232
  catch (err) {
163
233
  const msg = err instanceof Error ? err.message : String(err);
164
234
  log(`npm update failed: ${msg}`);
165
- log("Try manually: npm update -g intentdna");
235
+ log(`Try manually: npm install -g intentdna@${releaseChannel}`);
166
236
  return 1;
167
237
  }
168
238
  }
239
+ const upgradedVersion = getInstalledVersion();
240
+ if (!upgradedVersion) {
241
+ log("Upgrade failed: could not verify the installed npm package version.");
242
+ return 1;
243
+ }
244
+ if (shouldInstallLatestVersion(upgradedVersion, publishedVer)) {
245
+ log(`Upgrade failed: installed npm version ${upgradedVersion} is older than ${releaseChannel} channel version ${publishedVer}.`);
246
+ return 1;
247
+ }
169
248
  // Step 4: Re-resolve package path
170
249
  const pkgPath = await resolvePackagePath();
171
250
  if (!pkgPath) {
@@ -173,11 +252,11 @@ async function runUpgrade(opts) {
173
252
  return 1;
174
253
  }
175
254
  log(`Package path: ${pkgPath}`);
176
- // Step 5: Re-register marketplace (picks up new plugin.json version)
255
+ // Step 5: Refresh marketplace (picks up new plugin.json version)
177
256
  try {
178
257
  log("");
179
- log(`Re-registering marketplace: ${pkgPath}`);
180
- execSync(`claude plugin marketplace add "${pkgPath}"`, {
258
+ log("Refreshing marketplace: intentdna");
259
+ execSync("claude plugin marketplace update intentdna", {
181
260
  encoding: "utf-8",
182
261
  timeout: 30000,
183
262
  stdio: ["inherit", "pipe", "pipe"],
@@ -186,35 +265,49 @@ async function runUpgrade(opts) {
186
265
  }
187
266
  catch (err) {
188
267
  const msg = err instanceof Error ? err.message : String(err);
189
- log(`Warning: marketplace re-register failed: ${msg}`);
268
+ log(`Marketplace refresh failed: ${msg}`);
269
+ log(`Try manually: claude plugin marketplace add "${pkgPath}"`);
270
+ return 1;
190
271
  }
191
- // Step 6: Re-install plugin (picks up new hooks.json)
272
+ // Step 6: Update plugin (picks up new hooks.json and MCP runtime)
192
273
  try {
193
- const installCmd = `claude plugin install intentdna@intentdna --scope ${opts.scope}`;
194
- log(`Re-installing plugin: ${installCmd}`);
195
- execSync(installCmd, {
274
+ const updateCmd = "claude plugin update intentdna@intentdna";
275
+ log(`Updating plugin: ${updateCmd}`);
276
+ execSync(updateCmd, {
196
277
  encoding: "utf-8",
197
278
  timeout: 30000,
198
279
  stdio: ["inherit", "pipe", "pipe"],
199
280
  });
200
- log("Plugin re-installed.");
281
+ log("Plugin updated.");
201
282
  }
202
283
  catch (err) {
203
284
  const msg = err instanceof Error ? err.message : String(err);
204
- log(`Warning: plugin re-install failed: ${msg}`);
285
+ log(`Plugin update failed: ${msg}`);
286
+ log(`Try manually: claude plugin install intentdna@intentdna --scope ${opts.scope}`);
287
+ return 1;
205
288
  }
206
289
  // Step 7: Verify
207
- if (isPluginRegistered()) {
290
+ const plugin = getInstalledPluginInfo();
291
+ if (!plugin?.version) {
208
292
  log("");
209
- log("Upgrade complete. Run 'dna sync' to recompile your DNA templates.");
210
- return 0;
293
+ log("Upgrade failed: the IntentDNA Claude plugin version could not be verified.");
294
+ log("Try: claude plugin list");
295
+ return 1;
211
296
  }
212
- else {
297
+ if (plugin.version !== upgradedVersion) {
213
298
  log("");
214
- log("Upgrade completed but plugin verification failed.");
215
- log("Try: claude plugin list");
299
+ log(`Upgrade failed: Claude plugin version ${plugin.version} does not match npm version ${upgradedVersion}.`);
300
+ return 1;
301
+ }
302
+ if (!plugin.enabled) {
303
+ log("");
304
+ log("Upgrade failed: the version-matched IntentDNA Claude plugin is disabled.");
216
305
  return 1;
217
306
  }
307
+ log("");
308
+ log(`Upgrade complete: npm and Claude plugin are both ${upgradedVersion}.`);
309
+ log("Restart Claude Code, then run 'dna sync' in each project before resuming workflows.");
310
+ return 0;
218
311
  }
219
312
  export async function runSetup(opts) {
220
313
  // Upgrade mode
@@ -0,0 +1,32 @@
1
+ import type { DiscoveredSkillSource, SourceSnapshot } from "../../sources/types.js";
2
+ export interface SourceAssetsOptions {
3
+ homeDir?: string;
4
+ projectDir?: string;
5
+ sourceDirs?: readonly string[];
6
+ customRoots?: readonly string[];
7
+ sourceId?: string;
8
+ snapshot?: boolean;
9
+ packet?: boolean;
10
+ write?: string;
11
+ json?: boolean;
12
+ generatedAt?: string;
13
+ }
14
+ export interface SourceAssetsCatalog {
15
+ schema_version: "intentdna.source_catalog.v1";
16
+ generated_at: string;
17
+ sources: DiscoveredSkillSource[];
18
+ network_performed: false;
19
+ mutation_performed: false;
20
+ runtime_authority: false;
21
+ }
22
+ export interface SourceSnapshotCollection {
23
+ schema_version: "intentdna.source_snapshot_collection.v1";
24
+ generated_at: string;
25
+ snapshots: SourceSnapshot[];
26
+ network_performed: false;
27
+ mutation_performed: false;
28
+ runtime_authority: false;
29
+ }
30
+ export declare function buildSourceAssetsCatalog(options?: SourceAssetsOptions): Promise<SourceAssetsCatalog>;
31
+ export declare function buildSourceAssetsSnapshots(options?: SourceAssetsOptions): Promise<SourceSnapshotCollection>;
32
+ export declare function runSourceAssets(options?: SourceAssetsOptions): Promise<number>;
@@ -0,0 +1,154 @@
1
+ import { lstat, mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { buildSourceAuthoringPacket } from "../../sources/authoring-packet.js";
4
+ import { discoverSkillSources } from "../../sources/discovery.js";
5
+ import { createSourceSnapshot } from "../../sources/manifest.js";
6
+ function unique(values) {
7
+ return [...new Set(values)];
8
+ }
9
+ function inside(root, target) {
10
+ const rel = relative(root, target);
11
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
12
+ }
13
+ function selectSource(sources, selector, preferredRoots = []) {
14
+ const exact = sources.find((source) => source.source_id === selector);
15
+ if (exact)
16
+ return exact;
17
+ const named = sources.filter((source) => source.name === selector);
18
+ for (const preferredRoot of preferredRoots) {
19
+ const root = resolve(preferredRoot);
20
+ const preferred = named.find((source) => {
21
+ const skillDir = resolve(source.skill_dir);
22
+ return skillDir === root || inside(root, skillDir);
23
+ });
24
+ if (preferred)
25
+ return preferred;
26
+ }
27
+ return named[0];
28
+ }
29
+ async function assertNoSymlinkedParents(root, target) {
30
+ const parentRelative = relative(root, dirname(target));
31
+ let current = root;
32
+ for (const segment of parentRelative.split(sep)) {
33
+ if (!segment)
34
+ continue;
35
+ current = resolve(current, segment);
36
+ try {
37
+ const metadata = await lstat(current);
38
+ if (metadata.isSymbolicLink()) {
39
+ throw new Error(`Source artifact parent must not be a symbolic link: ${current}`);
40
+ }
41
+ }
42
+ catch (error) {
43
+ if (error.code === "ENOENT")
44
+ break;
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+ async function writePacketOrSnapshot(payload, projectDir, destination) {
50
+ const authoringRoot = resolve(projectDir, ".dna", "authoring");
51
+ const target = resolve(projectDir, destination);
52
+ if (!inside(authoringRoot, target)) {
53
+ throw new Error(`Source artifacts may only be written inside ${authoringRoot}`);
54
+ }
55
+ await assertNoSymlinkedParents(projectDir, target);
56
+ await mkdir(dirname(target), { recursive: true });
57
+ await assertNoSymlinkedParents(projectDir, target);
58
+ await writeFile(target, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
59
+ return target;
60
+ }
61
+ export async function buildSourceAssetsCatalog(options = {}) {
62
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
63
+ const sources = await discoverSkillSources({
64
+ homeDir: options.homeDir,
65
+ projectDir: options.projectDir,
66
+ customRoots: unique([...(options.sourceDirs ?? []), ...(options.customRoots ?? [])]),
67
+ });
68
+ return {
69
+ schema_version: "intentdna.source_catalog.v1",
70
+ generated_at: generatedAt,
71
+ sources,
72
+ network_performed: false,
73
+ mutation_performed: false,
74
+ runtime_authority: false,
75
+ };
76
+ }
77
+ export async function buildSourceAssetsSnapshots(options = {}) {
78
+ const catalog = await buildSourceAssetsCatalog(options);
79
+ const snapshots = await Promise.all(catalog.sources.map((source) => createSourceSnapshot({
80
+ sourceId: source.source_id,
81
+ skillDir: source.skill_dir,
82
+ generatedAt: catalog.generated_at,
83
+ })));
84
+ return {
85
+ schema_version: "intentdna.source_snapshot_collection.v1",
86
+ generated_at: catalog.generated_at,
87
+ snapshots,
88
+ network_performed: false,
89
+ mutation_performed: false,
90
+ runtime_authority: false,
91
+ };
92
+ }
93
+ export async function runSourceAssets(options = {}) {
94
+ const projectDir = resolve(options.projectDir ?? process.cwd());
95
+ try {
96
+ const catalog = await buildSourceAssetsCatalog({ ...options, projectDir });
97
+ let payload = catalog;
98
+ if (options.sourceId) {
99
+ const source = selectSource(catalog.sources, options.sourceId, options.sourceDirs);
100
+ if (!source) {
101
+ process.stderr.write(`Unknown source asset: ${options.sourceId}\n`);
102
+ return 2;
103
+ }
104
+ if (!options.packet) {
105
+ process.stderr.write("Error: selecting a source requires --packet.\n");
106
+ return 2;
107
+ }
108
+ payload = await buildSourceAuthoringPacket({
109
+ skillDir: source.skill_dir,
110
+ projectDir,
111
+ generatedAt: catalog.generated_at,
112
+ sourceId: source.source_id,
113
+ });
114
+ }
115
+ else if (options.packet) {
116
+ process.stderr.write("Error: --packet requires a source ID or source name.\n");
117
+ return 2;
118
+ }
119
+ else if (options.snapshot) {
120
+ payload = await buildSourceAssetsSnapshots({ ...options, projectDir, generatedAt: catalog.generated_at });
121
+ }
122
+ if (options.write) {
123
+ if (payload.schema_version === "intentdna.source_catalog.v1") {
124
+ process.stderr.write("Error: --write requires --snapshot or --packet.\n");
125
+ return 2;
126
+ }
127
+ const written = await writePacketOrSnapshot(payload, projectDir, options.write);
128
+ if (options.json) {
129
+ process.stdout.write(`${JSON.stringify({ written, payload }, null, 2)}\n`);
130
+ }
131
+ else {
132
+ process.stderr.write(`Wrote source artifact: ${written}\n`);
133
+ }
134
+ return 0;
135
+ }
136
+ if (options.json) {
137
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
138
+ }
139
+ else if (payload.schema_version === "intentdna.source_catalog.v1") {
140
+ process.stderr.write(`Source assets: ${payload.sources.length}\n`);
141
+ for (const source of payload.sources) {
142
+ process.stderr.write(` - ${source.source_id} (${source.skill_markdown_path})\n`);
143
+ }
144
+ }
145
+ else {
146
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
147
+ }
148
+ return 0;
149
+ }
150
+ catch (error) {
151
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
152
+ return 2;
153
+ }
154
+ }
@@ -28,6 +28,8 @@ import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
28
28
  import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles, removeSkillFiles } from "../../runtime/skill-adapter.js";
29
29
  import { getControllerKindMetadata } from "../../schema/controller-registry.js";
30
30
  import { createCodexSyncTargetPlan, executeCodexSyncTargetPlan, formatCodexSyncTargetPlanSummary, removeCodexSyncTargetArtifacts } from "../../runtime/codex-sync-target.js";
31
+ import { createWorkflowRuntimeManifest } from "../../runtime/workflow-runtime-manifest.js";
32
+ import { flattenResolvedVariableScopes, indexWorkflowVariableScopes, resolvedVariablesForNamespace, } from "../../runtime/workflow-variable-scopes.js";
31
33
  import { trackedOutputsForSyncTargetPlan } from "../../runtime/sync-target-plan.js";
32
34
  import { getPackageVersion } from "../util/version.js";
33
35
  import { cascadeDNA } from "../../compiler/cascade.js";
@@ -167,6 +169,24 @@ async function resolveVariables(rawVars, configPath, options) {
167
169
  }
168
170
  return resolved;
169
171
  }
172
+ async function resolveVariableScopes(dnas, configPath, options) {
173
+ const index = indexWorkflowVariableScopes(dnas);
174
+ const variableNamespaces = [...index.rawByNamespace.entries()]
175
+ .filter(([, raw]) => Object.keys(raw).length > 0)
176
+ .map(([namespace]) => namespace);
177
+ const resolvedByNamespace = new Map();
178
+ for (const [namespace, raw] of index.rawByNamespace) {
179
+ const writableConfigPath = variableNamespaces.length === 1 ? configPath : null;
180
+ resolvedByNamespace.set(namespace, await resolveVariables(raw, writableConfigPath, options));
181
+ }
182
+ return {
183
+ flat: flattenResolvedVariableScopes(index, resolvedByNamespace),
184
+ workflowNamespaces: index.workflowNamespaces,
185
+ workflowAssets: index.workflowAssets,
186
+ controllerNamespaces: index.controllerNamespaces,
187
+ forNamespace: (namespace) => resolvedVariablesForNamespace(resolvedByNamespace, namespace),
188
+ };
189
+ }
170
190
  /**
171
191
  * Auto-detect DNA config file in current directory (legacy single-config).
172
192
  * Priority: .dna/config.yaml > .dna/config.yml > .dna/config.json > .dna.yaml > .dna.yml > .dna.json
@@ -783,13 +803,39 @@ export async function runSync(opts) {
783
803
  trackedByLock: true,
784
804
  });
785
805
  }
806
+ const cascadedForRuntime = cascadeDNA(loadedDNAs);
807
+ const variableConfigPath = opts.files.length === 1 ? opts.files[0] : null;
808
+ const variableScopes = await resolveVariableScopes(loadedDNAs, variableConfigPath, { dryRun: opts.dryRun });
809
+ const variables = variableScopes.flat;
810
+ const workflowPlans = [];
811
+ for (const [workflowKey, workflowDef] of Object.entries(cascadedForRuntime.workflows)) {
812
+ const result = compileWorkflow(workflowDef, { workflow_key: workflowKey });
813
+ if (!result.ok || !result.plan) {
814
+ throw new Error(`Workflow '${workflowKey}' compilation failed:\n${result.errors.map((error) => ` ${error.path}: ${error.message}`).join("\n")}`);
815
+ }
816
+ workflowPlans.push(result.plan);
817
+ }
818
+ if (!ir.compiled_ir_hash) {
819
+ throw new Error("Compiled IR hash is required for workflow runtime bindings");
820
+ }
821
+ const workflowRuntime = createWorkflowRuntimeManifest({
822
+ plans: workflowPlans.map((plan) => ({
823
+ plan,
824
+ resolvedVariables: variableScopes.forNamespace(variableScopes.workflowNamespaces.get(plan.workflow_key ?? plan.source_workflow) ?? ""),
825
+ workflowAsset: variableScopes.workflowAssets.get(plan.workflow_key ?? plan.source_workflow),
826
+ additionalRuntimeValues: ir.verifier_specs?.filter((spec) => spec.workflow_name === (plan.workflow_key ?? plan.source_workflow)) ?? [],
827
+ })),
828
+ compiledAt: ir.compiled_at,
829
+ compiledIrHash: ir.compiled_ir_hash,
830
+ sourceDnaIds: ir.source_dna_ids,
831
+ });
786
832
  // Step 3.5: Execute Claude Code core target plan (compiled IR, MCP, settings hooks)
787
833
  {
788
- const cascadedForCore = cascadeDNA(loadedDNAs);
789
834
  const claudeCorePlan = createClaudeCoreSyncTargetPlan({
790
835
  projectDir,
791
836
  ir,
792
- roles: cascadedForCore.roles,
837
+ roles: cascadedForRuntime.roles,
838
+ workflowRuntime,
793
839
  mode: opts.plugin ? "plugin" : "bin",
794
840
  settingsPath: opts.settingsPath,
795
841
  });
@@ -913,14 +959,6 @@ export async function runSync(opts) {
913
959
  const workflows = cascadedForSkills.workflows;
914
960
  const controllers = cascadedForSkills.controllers;
915
961
  const roles = cascadedForSkills.roles;
916
- // Collect and resolve variables from all DNA files
917
- const rawVars = {};
918
- for (const d of dnas) {
919
- if (d.variables)
920
- Object.assign(rawVars, d.variables);
921
- }
922
- const configPath = opts.files.length === 1 ? opts.files[0] : null;
923
- const variables = await resolveVariables(rawVars, configPath, { dryRun: opts.dryRun });
924
962
  // Step 8: Generate project-root .mcp.json only for Claude Code skills.
925
963
  if (skillSurface === "claude_code") {
926
964
  const mcpDeps = {};
@@ -947,14 +985,16 @@ export async function runSync(opts) {
947
985
  for (const [name, wfDef] of Object.entries(workflows)) {
948
986
  const result = compileWorkflow(wfDef, { workflow_key: name });
949
987
  if (result.ok) {
950
- skillResults.push(compileWorkflowToSkill(result.plan, roles, ir, variables, { surface: skillSurface }));
988
+ const workflowVariables = variableScopes.forNamespace(variableScopes.workflowNamespaces.get(name) ?? "");
989
+ skillResults.push(compileWorkflowToSkill(result.plan, roles, ir, workflowVariables, { surface: skillSurface }));
951
990
  }
952
991
  }
953
992
  if (skillSurface === "claude_code") {
954
993
  for (const [name, controllerDef] of Object.entries(controllers)) {
955
994
  const controllerKind = getControllerKindMetadata(controllerDef.kind);
956
995
  if (controllerKind?.supportsSkillGeneration) {
957
- skillResults.push(compileControllerToSkill(name, controllerDef, variables, workflows));
996
+ const controllerVariables = variableScopes.forNamespace(variableScopes.controllerNamespaces.get(name) ?? "");
997
+ skillResults.push(compileControllerToSkill(name, controllerDef, controllerVariables, workflows));
958
998
  }
959
999
  }
960
1000
  }