intentdna 1.8.7 → 1.9.0-rc.1

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 (51) 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 +6 -4
  7. package/dist/cli/commands/source-assets.d.ts +32 -0
  8. package/dist/cli/commands/source-assets.js +154 -0
  9. package/dist/cli/commands/templates.js +59 -37
  10. package/dist/cli/index.js +161 -5
  11. package/dist/compiler/cascade.js +1 -1
  12. package/dist/hooks/cli.d.ts +6 -0
  13. package/dist/hooks/cli.js +74 -6
  14. package/dist/mcp/index.js +0 -0
  15. package/dist/runtime/evaluator.d.ts +13 -0
  16. package/dist/runtime/evaluator.js +46 -0
  17. package/dist/runtime/index.d.ts +6 -2
  18. package/dist/runtime/index.js +3 -1
  19. package/dist/runtime/run-contracts.d.ts +5 -3
  20. package/dist/runtime/run-contracts.js +11 -1
  21. package/dist/runtime/run-controller.d.ts +15 -1
  22. package/dist/runtime/run-controller.js +235 -94
  23. package/dist/runtime/verifier.js +101 -2
  24. package/dist/runtime/workflow-plan-adapter.d.ts +2 -1
  25. package/dist/runtime/workflow-plan-adapter.js +78 -11
  26. package/dist/schema/types.d.ts +5 -0
  27. package/dist/schema/validate.js +29 -1
  28. package/dist/sources/authoring-packet.d.ts +8 -0
  29. package/dist/sources/authoring-packet.js +85 -0
  30. package/dist/sources/discovery.d.ts +7 -0
  31. package/dist/sources/discovery.js +150 -0
  32. package/dist/sources/license.d.ts +5 -0
  33. package/dist/sources/license.js +94 -0
  34. package/dist/sources/manifest.d.ts +11 -0
  35. package/dist/sources/manifest.js +84 -0
  36. package/dist/sources/skill-markdown.d.ts +6 -0
  37. package/dist/sources/skill-markdown.js +174 -0
  38. package/dist/sources/types.d.ts +125 -0
  39. package/dist/sources/types.js +1 -0
  40. package/dist/templates/asset-refinery.dna.yaml +192 -0
  41. package/dist/templates/flutter-rewrite.dna.yaml +154 -28
  42. package/dist/templates/metadata.js +2 -0
  43. package/dist/templates/multi-agent-handoff-coordination.dna.yaml +102 -37
  44. package/dist/templates/persistent-executor.dna.yaml +61 -3
  45. package/dist/templates/research-improvement.dna.yaml +295 -0
  46. package/dist/templates/subagent-parallel.dna.yaml +90 -17
  47. package/dist/workflow/candidate-projection.d.ts +33 -0
  48. package/dist/workflow/candidate-projection.js +70 -0
  49. package/package.json +1 -1
  50. package/dist/runtime/diagnosis-contract-verifier.d.ts +0 -11
  51. package/dist/runtime/diagnosis-contract-verifier.js +0 -417
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
12
- "version": "1.8.7",
12
+ "version": "1.9.0-rc.1",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.8.7"
28
+ "version": "1.9.0-rc.1"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.7",
3
+ "version": "1.9.0-rc.1",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -0,0 +1,15 @@
1
+ export interface PromoteCandidateOptions {
2
+ projectDir: string;
3
+ candidatePath: string;
4
+ templateName: string;
5
+ }
6
+ export interface CandidatePromotionResult {
7
+ schema_version: "intentdna.candidate_promotion_result.v1";
8
+ candidate_path: string;
9
+ template_path: string;
10
+ receipt_path: string;
11
+ candidate_sha256: string;
12
+ template_sha256: string;
13
+ exact_bytes_preserved: true;
14
+ }
15
+ export declare function promoteCandidate(options: PromoteCandidateOptions): Promise<CandidatePromotionResult>;
@@ -0,0 +1,197 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, realpath, rm, stat, } from "node:fs/promises";
3
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { formatDiagnostics, hasDiagnosticErrors } from "../../compiler/diagnostics.js";
5
+ import { loadDNAWithDiagnostics } from "../../compiler/input-resolver.js";
6
+ const CANDIDATE_SUFFIX = ".candidate.dna.yaml";
7
+ const SAFE_TEMPLATE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
8
+ function sha256(bytes) {
9
+ return createHash("sha256").update(bytes).digest("hex");
10
+ }
11
+ function isErrnoException(error) {
12
+ return error instanceof Error && "code" in error;
13
+ }
14
+ function isStrictlyWithin(parent, child) {
15
+ const pathFromParent = relative(parent, child);
16
+ return pathFromParent !== ""
17
+ && pathFromParent !== ".."
18
+ && !pathFromParent.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)
19
+ && !isAbsolute(pathFromParent);
20
+ }
21
+ function validateTemplateName(templateName) {
22
+ if (!SAFE_TEMPLATE_NAME.test(templateName)) {
23
+ throw new Error("Invalid template name: use one safe path component containing only letters, numbers, hyphens, or underscores");
24
+ }
25
+ }
26
+ async function assertPathMissing(path, label) {
27
+ try {
28
+ await lstat(path);
29
+ }
30
+ catch (error) {
31
+ if (isErrnoException(error) && error.code === "ENOENT")
32
+ return;
33
+ throw error;
34
+ }
35
+ throw new Error(`${label} already exists; overwrite is not allowed`);
36
+ }
37
+ async function ensureSafeTemplatesDirectory(templatesDir, dnaRealPath) {
38
+ try {
39
+ const entry = await lstat(templatesDir);
40
+ if (entry.isSymbolicLink()) {
41
+ throw new Error("Templates directory must not be a symlink");
42
+ }
43
+ if (!entry.isDirectory()) {
44
+ throw new Error("Templates path is not a directory");
45
+ }
46
+ }
47
+ catch (error) {
48
+ if (!isErrnoException(error) || error.code !== "ENOENT")
49
+ throw error;
50
+ await mkdir(templatesDir);
51
+ }
52
+ const entry = await lstat(templatesDir);
53
+ if (entry.isSymbolicLink()) {
54
+ throw new Error("Templates directory must not be a symlink");
55
+ }
56
+ if (!entry.isDirectory()) {
57
+ throw new Error("Templates path is not a directory");
58
+ }
59
+ const templatesRealPath = await realpath(templatesDir);
60
+ if (!isStrictlyWithin(dnaRealPath, templatesRealPath)) {
61
+ throw new Error("Templates directory escapes the project .dna directory");
62
+ }
63
+ return templatesRealPath;
64
+ }
65
+ async function removeCreatedPath(path) {
66
+ try {
67
+ await rm(path, { force: true });
68
+ return undefined;
69
+ }
70
+ catch (error) {
71
+ return error;
72
+ }
73
+ }
74
+ export async function promoteCandidate(options) {
75
+ validateTemplateName(options.templateName);
76
+ const projectPath = resolve(options.projectDir);
77
+ const projectRealPath = await realpath(projectPath);
78
+ const dnaPath = join(projectPath, ".dna");
79
+ const authoringPath = join(dnaPath, "authoring");
80
+ const candidatePath = resolve(projectPath, options.candidatePath);
81
+ if (!isStrictlyWithin(authoringPath, candidatePath)) {
82
+ throw new Error("Candidate must be located within the project .dna/authoring directory");
83
+ }
84
+ const candidateFilename = basename(candidatePath);
85
+ if (!candidateFilename.endsWith(CANDIDATE_SUFFIX)
86
+ || candidateFilename.length === CANDIDATE_SUFFIX.length) {
87
+ throw new Error(`Candidate filename must end with ${CANDIDATE_SUFFIX}`);
88
+ }
89
+ const dnaRealPath = await realpath(dnaPath);
90
+ if (!isStrictlyWithin(projectRealPath, dnaRealPath)) {
91
+ throw new Error("Project .dna directory escapes the project root");
92
+ }
93
+ const authoringRealPath = await realpath(authoringPath);
94
+ if (!isStrictlyWithin(dnaRealPath, authoringRealPath)) {
95
+ throw new Error("Authoring directory escapes the project .dna directory");
96
+ }
97
+ const candidateRealPath = await realpath(candidatePath);
98
+ if (!isStrictlyWithin(authoringRealPath, candidateRealPath)) {
99
+ throw new Error("Candidate symlink or path escapes the project authoring directory");
100
+ }
101
+ const candidateEntry = await stat(candidateRealPath);
102
+ if (!candidateEntry.isFile()) {
103
+ throw new Error("Candidate must be a regular file in the project authoring directory");
104
+ }
105
+ const candidateBytesBeforeValidation = await readFile(candidateRealPath);
106
+ const validation = await loadDNAWithDiagnostics(candidateRealPath);
107
+ if (!validation.dna || hasDiagnosticErrors(validation.diagnostics)) {
108
+ const details = formatDiagnostics(validation.diagnostics);
109
+ throw new Error(`Candidate validation failed${details ? `:\n${details}` : ""}`);
110
+ }
111
+ const candidateRealPathAfterValidation = await realpath(candidatePath);
112
+ if (candidateRealPathAfterValidation !== candidateRealPath) {
113
+ throw new Error("Candidate path changed during validation; promotion refused");
114
+ }
115
+ const candidateBytes = await readFile(candidateRealPathAfterValidation);
116
+ if (!candidateBytes.equals(candidateBytesBeforeValidation)) {
117
+ throw new Error("Candidate bytes changed during validation; promotion refused");
118
+ }
119
+ const templatesPath = join(dnaPath, "templates");
120
+ const templatesRealPath = await ensureSafeTemplatesDirectory(templatesPath, dnaRealPath);
121
+ const templatePath = join(templatesPath, `${options.templateName}.dna.yaml`);
122
+ const templateWritePath = join(templatesRealPath, `${options.templateName}.dna.yaml`);
123
+ const candidateName = candidateFilename.slice(0, -CANDIDATE_SUFFIX.length);
124
+ const receiptPath = join(authoringPath, `${candidateName}.candidate-review.json`);
125
+ const receiptWritePath = join(authoringRealPath, `${candidateName}.candidate-review.json`);
126
+ await assertPathMissing(templateWritePath, "Promotion target");
127
+ await assertPathMissing(receiptWritePath, "Candidate review receipt");
128
+ const candidateHash = sha256(candidateBytes);
129
+ let templateCreated = false;
130
+ let receiptCreated = false;
131
+ let templateHandle;
132
+ let receiptHandle;
133
+ try {
134
+ templateHandle = await open(templateWritePath, "wx");
135
+ templateCreated = true;
136
+ await templateHandle.writeFile(candidateBytes);
137
+ await templateHandle.sync();
138
+ await templateHandle.close();
139
+ templateHandle = undefined;
140
+ const currentTemplatesRealPath = await realpath(templatesPath);
141
+ if (currentTemplatesRealPath !== templatesRealPath) {
142
+ throw new Error("Templates directory changed during promotion; promotion refused");
143
+ }
144
+ const promotedBytes = await readFile(templateWritePath);
145
+ const templateHash = sha256(promotedBytes);
146
+ if (!promotedBytes.equals(candidateBytes) || templateHash !== candidateHash) {
147
+ throw new Error("Promoted template does not preserve the validated candidate bytes exactly");
148
+ }
149
+ const receipt = {
150
+ schema_version: "intentdna.candidate_promotion_receipt.v1",
151
+ decision: "promoted",
152
+ source: "explicit_human_promotion",
153
+ mutation_performed: true,
154
+ promoted_at: new Date().toISOString(),
155
+ candidate_path: candidatePath,
156
+ template_path: templatePath,
157
+ candidate_sha256: candidateHash,
158
+ template_sha256: templateHash,
159
+ exact_bytes_preserved: true,
160
+ };
161
+ const receiptBytes = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`, "utf8");
162
+ receiptHandle = await open(receiptWritePath, "wx");
163
+ receiptCreated = true;
164
+ await receiptHandle.writeFile(receiptBytes);
165
+ await receiptHandle.sync();
166
+ await receiptHandle.close();
167
+ receiptHandle = undefined;
168
+ return {
169
+ schema_version: "intentdna.candidate_promotion_result.v1",
170
+ candidate_path: candidatePath,
171
+ template_path: templatePath,
172
+ receipt_path: receiptPath,
173
+ candidate_sha256: candidateHash,
174
+ template_sha256: templateHash,
175
+ exact_bytes_preserved: true,
176
+ };
177
+ }
178
+ catch (error) {
179
+ await receiptHandle?.close().catch(() => undefined);
180
+ await templateHandle?.close().catch(() => undefined);
181
+ const cleanupErrors = [];
182
+ if (receiptCreated) {
183
+ const cleanupError = await removeCreatedPath(receiptWritePath);
184
+ if (cleanupError)
185
+ cleanupErrors.push(cleanupError);
186
+ }
187
+ if (templateCreated) {
188
+ const cleanupError = await removeCreatedPath(templateWritePath);
189
+ if (cleanupError)
190
+ cleanupErrors.push(cleanupError);
191
+ }
192
+ if (cleanupErrors.length > 0) {
193
+ throw new AggregateError([error, ...cleanupErrors], "Candidate promotion failed and cleanup did not complete");
194
+ }
195
+ throw error;
196
+ }
197
+ }
@@ -9,6 +9,7 @@ const TASK_STATES = [
9
9
  "ready",
10
10
  "claimed",
11
11
  "running",
12
+ "round_succeeded",
12
13
  "succeeded",
13
14
  "failed",
14
15
  "skipped",
@@ -22,7 +22,7 @@ import { DurableRunStore, RunStoreError, } from "../../runtime/run-store.js";
22
22
  import { createClaudeExecutionProvider } from "../../runtime/providers/claude.js";
23
23
  import { createCodexExecutionProvider } from "../../runtime/providers/codex.js";
24
24
  import { runCheckpointVerifier, runCompletionVerifier, } from "../../runtime/verifier.js";
25
- import { adaptWorkflowPlan, } from "../../runtime/workflow-plan-adapter.js";
25
+ import { adaptWorkflowPlan, adaptWorkflowRetryLoop, } from "../../runtime/workflow-plan-adapter.js";
26
26
  import { executeWorkerAttempt, } from "../../runtime/worker-executor.js";
27
27
  import { allocateAttemptWorkspace, applyWorkspaceLifecycleDecision, planAttemptWorkspace, } from "../../runtime/workspace-isolation.js";
28
28
  import { runLifecycleCancel, runLifecycleInspect, runLifecycleResume, runLifecycleStart, runLifecycleStatus, } from "./run-lifecycle.js";
@@ -237,6 +237,7 @@ async function compileCanonicalRun(metadata) {
237
237
  return {
238
238
  ...assets,
239
239
  definitions: adaptWorkflowPlan(assets.plan, adapterOptions),
240
+ retry_loop: adaptWorkflowRetryLoop(assets.plan, adapterOptions),
240
241
  };
241
242
  }
242
243
  async function syncRuntimeArtifacts(compiled, metadata) {
@@ -371,7 +372,7 @@ async function applyStepValidation(metadata, compiled, packet, execution) {
371
372
  }
372
373
  const variables = {
373
374
  ...metadata.variables,
374
- round: String(packet.attempt_number),
375
+ round: String(packet.workflow_round),
375
376
  };
376
377
  let failure = null;
377
378
  for (const completion of step.completion ?? []) {
@@ -468,7 +469,7 @@ function createWorkspaceAwareExecutor(metadata, compiled, runStore) {
468
469
  workflow: metadata.workflow_name,
469
470
  current_step: packet.step_id,
470
471
  current_role: packet.role,
471
- iteration: packet.attempt_number,
472
+ iteration: packet.workflow_round,
472
473
  session_id: packet.worker_session_id,
473
474
  worker_session_id: packet.worker_session_id,
474
475
  run_id: packet.run_id,
@@ -478,7 +479,7 @@ function createWorkspaceAwareExecutor(metadata, compiled, runStore) {
478
479
  inputs: { ...metadata.variables },
479
480
  resolved_variables: {
480
481
  ...metadata.variables,
481
- round: String(packet.attempt_number),
482
+ round: String(packet.workflow_round),
482
483
  },
483
484
  });
484
485
  }
@@ -533,6 +534,7 @@ function buildRuntime(metadata, compiled) {
533
534
  run_store: runStore,
534
535
  handoff_resolver: resolver,
535
536
  steps: compiled.definitions,
537
+ workflow_retry_loop: compiled.retry_loop,
536
538
  provider_for_step: () => provider,
537
539
  reconcile_attempt: reconcileLocalProcess,
538
540
  execute_attempt: createWorkspaceAwareExecutor(metadata, compiled, runStore),
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dna templates — template catalog and deployment commands.
3
3
  */
4
- import { readdirSync, copyFileSync, existsSync, readFileSync, mkdirSync } from "node:fs";
4
+ import { readdirSync, copyFileSync, existsSync, readFileSync, mkdirSync, lstatSync, } from "node:fs";
5
5
  import { mkdir, readdir, writeFile } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
7
  import { resolve, join, basename, dirname } from "node:path";
@@ -15,6 +15,7 @@ import { resolveAuthToken } from "./auth.js";
15
15
  import { missingCliApiTokenReason } from "./cloud-auth-guidance.js";
16
16
  import { createWorkflowDraft, workflowDraftToTemplateYaml, } from "../../workflow/index.js";
17
17
  import { WORKFLOW_ASSET_IDS } from "../../templates/metadata.js";
18
+ import { buildSourceAuthoringPacket } from "../../sources/authoring-packet.js";
18
19
  /**
19
20
  * Resolve the global intentdna templates directory.
20
21
  * Strategy: npm root -g + /intentdna/dist/templates
@@ -1774,6 +1775,9 @@ function candidateTemplateName(sourceId) {
1774
1775
  function normalizeTemplateSaveName(name) {
1775
1776
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "source-candidate";
1776
1777
  }
1778
+ function isSafeCandidateName(name) {
1779
+ return /^[a-z][a-z0-9-]{1,62}$/.test(name);
1780
+ }
1777
1781
  function workflowFromSourceCandidate(candidate) {
1778
1782
  const closestTemplate = candidate.coverage.matchedTemplates[0]?.name;
1779
1783
  const comparePrompt = closestTemplate
@@ -1922,37 +1926,50 @@ function buildSourceCandidatePreview(candidate, requestedTemplateName) {
1922
1926
  boundary: "Read-only candidate preview. It prints a reviewable template YAML draft but does not write files, configure projects, sync runtimes, deploy, or mutate evolution markers.",
1923
1927
  };
1924
1928
  }
1925
- function writeSourceCandidatePreview(preview) {
1926
- process.stderr.write("Template reference candidate preview\n");
1927
- process.stderr.write(`Reference: ${preview.candidate.id}\n`);
1928
- process.stderr.write(`Template: ${preview.templateName}\n`);
1929
- process.stderr.write(`Decision: ${preview.decision.status}\n`);
1930
- process.stderr.write(`Workflow: ${preview.draft.workflow.name}\n`);
1931
- process.stderr.write(`Steps: ${preview.draft.workflow.steps.length}\n`);
1932
- process.stderr.write("\nReview commands\n");
1933
- for (const command of preview.reviewCommands)
1934
- process.stderr.write(` - ${command}\n`);
1935
- process.stderr.write("\nBlocked until review\n");
1936
- for (const blocker of preview.blockedUntilReview)
1937
- process.stderr.write(` - ${blocker}\n`);
1938
- process.stderr.write(`Boundary: ${preview.boundary}\n`);
1939
- process.stderr.write("\n--- YAML preview is written to stdout ---\n");
1940
- process.stdout.write(preview.templateYaml);
1941
- if (!preview.templateYaml.endsWith("\n"))
1942
- process.stdout.write("\n");
1943
- }
1944
1929
  async function saveSourceCandidatePreview(params) {
1945
- const projectDir = params.projectDir ?? process.cwd();
1946
- const templateDir = join(projectDir, ".dna", "templates");
1947
- const path = join(templateDir, `${params.preview.templateName}.dna.yaml`);
1930
+ const projectDir = resolve(params.projectDir ?? process.cwd());
1931
+ const dnaDir = join(projectDir, ".dna");
1932
+ const authoringDir = join(dnaDir, "authoring");
1933
+ const path = join(authoringDir, `${params.preview.templateName}.candidate.dna.yaml`);
1934
+ for (const stagingParent of [dnaDir, authoringDir]) {
1935
+ if (existsSync(stagingParent)
1936
+ && lstatSync(stagingParent).isSymbolicLink()) {
1937
+ return {
1938
+ error: `Candidate staging path must not contain a symlink: ${stagingParent}`,
1939
+ path,
1940
+ };
1941
+ }
1942
+ }
1948
1943
  if (existsSync(path)) {
1949
1944
  return {
1950
- error: `Workflow asset draft already exists: ${params.preview.templateName}`,
1945
+ error: `Candidate already exists: ${params.preview.templateName}`,
1951
1946
  path,
1952
1947
  };
1953
1948
  }
1954
- await mkdir(templateDir, { recursive: true });
1955
- await writeFile(path, params.preview.templateYaml, "utf-8");
1949
+ await mkdir(authoringDir, { recursive: true });
1950
+ for (const stagingParent of [dnaDir, authoringDir]) {
1951
+ if (lstatSync(stagingParent).isSymbolicLink()) {
1952
+ return {
1953
+ error: `Candidate staging path must not contain a symlink: ${stagingParent}`,
1954
+ path,
1955
+ };
1956
+ }
1957
+ }
1958
+ try {
1959
+ await writeFile(path, params.preview.templateYaml, {
1960
+ encoding: "utf-8",
1961
+ flag: "wx",
1962
+ });
1963
+ }
1964
+ catch (error) {
1965
+ if (error.code === "EEXIST") {
1966
+ return {
1967
+ error: `Candidate already exists: ${params.preview.templateName}`,
1968
+ path,
1969
+ };
1970
+ }
1971
+ throw error;
1972
+ }
1956
1973
  const closestTemplate = params.preview.candidate.coverage.matchedTemplates[0]?.name ?? "<current-template>";
1957
1974
  return {
1958
1975
  mode: "saved_template_source_candidate",
@@ -1963,19 +1980,18 @@ async function saveSourceCandidatePreview(params) {
1963
1980
  decision: params.preview.decision,
1964
1981
  reviewCommands: params.preview.reviewCommands,
1965
1982
  nextCommands: [
1966
- `dna assets inspect ${params.preview.templateName} --evidence`,
1983
+ `dna validate ${path}`,
1984
+ `dna workflow project-candidate ${path} --target claude --dry-run`,
1985
+ `dna workflow project-candidate ${path} --target codex --dry-run`,
1967
1986
  `dna assets diff ${closestTemplate} ${params.preview.templateName}`,
1968
1987
  `dna assets adoption ${params.preview.templateName}`,
1969
- `dna org bind --asset ${params.preview.templateName} --status planned --sync-target codex`,
1970
- "dna org rollout",
1971
- "dna sync --codex --dry-run",
1972
1988
  ],
1973
1989
  blockedUntilReview: [
1974
1990
  "Runtime sync remains blocked until asset diff/adoption and organization rollout review pass.",
1975
1991
  "Vercel deployment is unrelated unless a web/public surface changes.",
1976
1992
  "Evolution marker mutation remains blocked until evidence-backed suggestions are reviewed.",
1977
1993
  ],
1978
- boundary: "Saved reference candidate as a project-local draft only. It is not adopted, organization-bound, synced, deployed, or evolved.",
1994
+ boundary: "Saved an unreviewed candidate under .dna/authoring only. It is not promoted, adopted, organization-bound, synced, deployed, or evolved.",
1979
1995
  };
1980
1996
  }
1981
1997
  function writeSavedSourceCandidate(result) {
@@ -2029,8 +2045,12 @@ export async function runTemplatesSources(opts = {}) {
2029
2045
  return 2;
2030
2046
  }
2031
2047
  if (opts.candidate) {
2032
- const preview = buildSourceCandidatePreview(candidate, opts.saveTemplate);
2033
2048
  if (opts.saveTemplate) {
2049
+ if (!isSafeCandidateName(opts.saveTemplate)) {
2050
+ process.stderr.write("Error: invalid candidate name; use 2-63 lowercase letters, digits, or hyphens, starting with a letter.\n");
2051
+ return 2;
2052
+ }
2053
+ const preview = buildSourceCandidatePreview(candidate, opts.saveTemplate);
2034
2054
  const saved = await saveSourceCandidatePreview({ preview, projectDir: opts.projectDir });
2035
2055
  if ("error" in saved) {
2036
2056
  process.stderr.write(`${saved.error}\n`);
@@ -2045,11 +2065,13 @@ export async function runTemplatesSources(opts = {}) {
2045
2065
  writeSavedSourceCandidate(saved);
2046
2066
  return 0;
2047
2067
  }
2048
- if (opts.json) {
2049
- process.stdout.write(`${JSON.stringify(preview, null, 2)}\n`);
2050
- return 0;
2051
- }
2052
- writeSourceCandidatePreview(preview);
2068
+ const packet = await buildSourceAuthoringPacket({
2069
+ skillDir: dirname(candidate.path),
2070
+ projectDir: opts.projectDir,
2071
+ sourceId: candidate.id,
2072
+ });
2073
+ process.stderr.write("Warning: deprecated --candidate alias; use --packet for source authoring packets.\n");
2074
+ process.stdout.write(`${JSON.stringify(packet, null, 2)}\n`);
2053
2075
  return 0;
2054
2076
  }
2055
2077
  const packet = await buildTemplateSourceReviewPacket(candidate);