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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/candidate-promotion.d.ts +15 -0
- package/dist/cli/commands/candidate-promotion.js +197 -0
- package/dist/cli/commands/run-lifecycle.js +1 -0
- package/dist/cli/commands/run.js +6 -4
- package/dist/cli/commands/source-assets.d.ts +32 -0
- package/dist/cli/commands/source-assets.js +154 -0
- package/dist/cli/commands/templates.js +59 -37
- package/dist/cli/index.js +161 -5
- package/dist/compiler/cascade.js +1 -1
- package/dist/hooks/cli.d.ts +6 -0
- package/dist/hooks/cli.js +74 -6
- package/dist/mcp/index.js +0 -0
- package/dist/runtime/evaluator.d.ts +13 -0
- package/dist/runtime/evaluator.js +46 -0
- package/dist/runtime/index.d.ts +6 -2
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/run-contracts.d.ts +5 -3
- package/dist/runtime/run-contracts.js +11 -1
- package/dist/runtime/run-controller.d.ts +15 -1
- package/dist/runtime/run-controller.js +235 -94
- package/dist/runtime/verifier.js +101 -2
- package/dist/runtime/workflow-plan-adapter.d.ts +2 -1
- package/dist/runtime/workflow-plan-adapter.js +78 -11
- package/dist/schema/types.d.ts +5 -0
- package/dist/schema/validate.js +29 -1
- package/dist/sources/authoring-packet.d.ts +8 -0
- package/dist/sources/authoring-packet.js +85 -0
- package/dist/sources/discovery.d.ts +7 -0
- package/dist/sources/discovery.js +150 -0
- package/dist/sources/license.d.ts +5 -0
- package/dist/sources/license.js +94 -0
- package/dist/sources/manifest.d.ts +11 -0
- package/dist/sources/manifest.js +84 -0
- package/dist/sources/skill-markdown.d.ts +6 -0
- package/dist/sources/skill-markdown.js +174 -0
- package/dist/sources/types.d.ts +125 -0
- package/dist/sources/types.js +1 -0
- package/dist/templates/asset-refinery.dna.yaml +192 -0
- package/dist/templates/flutter-rewrite.dna.yaml +154 -28
- package/dist/templates/metadata.js +2 -0
- package/dist/templates/multi-agent-handoff-coordination.dna.yaml +102 -37
- package/dist/templates/persistent-executor.dna.yaml +61 -3
- package/dist/templates/research-improvement.dna.yaml +295 -0
- package/dist/templates/subagent-parallel.dna.yaml +90 -17
- package/dist/workflow/candidate-projection.d.ts +33 -0
- package/dist/workflow/candidate-projection.js +70 -0
- package/package.json +1 -1
- package/dist/runtime/diagnosis-contract-verifier.d.ts +0 -11
- 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.
|
|
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.
|
|
28
|
+
"version": "1.9.0-rc.1"
|
|
29
29
|
}
|
|
@@ -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
|
+
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
1947
|
-
const
|
|
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: `
|
|
1945
|
+
error: `Candidate already exists: ${params.preview.templateName}`,
|
|
1951
1946
|
path,
|
|
1952
1947
|
};
|
|
1953
1948
|
}
|
|
1954
|
-
await mkdir(
|
|
1955
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
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);
|