intentdna 1.5.20 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -2
- package/dist/cli/commands/sync.js +10 -8
- package/dist/compiler/cascade.d.ts +2 -1
- package/dist/compiler/cascade.js +25 -0
- package/dist/hooks/cli.d.ts +16 -1
- package/dist/hooks/cli.js +242 -33
- package/dist/hooks/enforce.d.ts +6 -6
- package/dist/hooks/enforce.js +103 -42
- package/dist/hooks/state.d.ts +21 -1
- package/dist/hooks/state.js +119 -4
- package/dist/mcp/tools-state.js +8 -0
- package/dist/runtime/settings-adapter.js +2 -2
- package/dist/runtime/skill-adapter.d.ts +2 -1
- package/dist/runtime/skill-adapter.js +170 -0
- package/dist/schema/types.d.ts +64 -5
- package/dist/schema/validate.js +111 -0
- package/dist/templates/code-review-pipeline.dna.yaml +1 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +3 -0
- package/dist/templates/flutter-rewrite.dna.yaml +132 -23
- package/dist/templates/full-pipeline.dna.yaml +3 -0
- package/package.json +1 -1
- package/spec/schema-spec.md +3 -0
package/dist/hooks/enforce.js
CHANGED
|
@@ -58,7 +58,7 @@ export function enforcePreToolUse(ir, input, state, roles) {
|
|
|
58
58
|
}
|
|
59
59
|
// Layer 6: Handoff — check consumed artifacts are available
|
|
60
60
|
if (state?.workflowState && ir.workflows_ir) {
|
|
61
|
-
const result = enforceHandoffConsumes(ir, state.workflowState, state.
|
|
61
|
+
const result = enforceHandoffConsumes(ir, state.workflowState, state.artifactFacts ?? []);
|
|
62
62
|
if (result)
|
|
63
63
|
return { output: result, trace: { matched_rule: "handoff", step_id: state.workflowState.current_step } };
|
|
64
64
|
}
|
|
@@ -283,20 +283,18 @@ export function enforceStop(ir, input, workflowState) {
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
+
const handoffResult = enforceHandoffProduces(ir, {
|
|
287
|
+
current_step: workflowState.current_step,
|
|
288
|
+
workflow: workflowState.workflow,
|
|
289
|
+
artifact_facts: workflowState.artifact_facts ?? [],
|
|
290
|
+
});
|
|
291
|
+
if (handoffResult)
|
|
292
|
+
return handoffResult;
|
|
286
293
|
if (workflowCheckpoints.length === 0) {
|
|
287
294
|
return null;
|
|
288
295
|
}
|
|
289
296
|
const blocking = workflowCheckpoints.flatMap(cp => cp.checkpoints.filter(c => (c.action ?? "block") === "block"));
|
|
290
297
|
if (blocking.length === 0) {
|
|
291
|
-
if (workflowState.workflow) {
|
|
292
|
-
const handoffResult = enforceHandoffProduces(ir, {
|
|
293
|
-
current_step: workflowState.current_step,
|
|
294
|
-
workflow: workflowState.workflow,
|
|
295
|
-
completed_artifacts: workflowState.completed_artifacts,
|
|
296
|
-
});
|
|
297
|
-
if (handoffResult)
|
|
298
|
-
return handoffResult;
|
|
299
|
-
}
|
|
300
298
|
return null;
|
|
301
299
|
}
|
|
302
300
|
const messages = blocking.map(c => c.message);
|
|
@@ -359,26 +357,23 @@ function getStepAdditionalPaths(ir, state) {
|
|
|
359
357
|
* artifacts have been produced by a preceding step.
|
|
360
358
|
* Returns block output if a required artifact is missing, null if all satisfied.
|
|
361
359
|
*/
|
|
362
|
-
function enforceHandoffConsumes(ir, wfState,
|
|
360
|
+
function enforceHandoffConsumes(ir, wfState, artifactFacts) {
|
|
363
361
|
const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
|
|
364
|
-
if (!activeWf || activeWf.handoff_chain
|
|
362
|
+
if (!activeWf || (activeWf.handoff_chain?.length ?? 0) === 0)
|
|
365
363
|
return null;
|
|
366
364
|
const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
|
|
367
365
|
if (!currentEntry?.consumes || currentEntry.consumes.length === 0)
|
|
368
366
|
return null;
|
|
369
|
-
const completedStepIds = new Set((wfState.completed_artifacts ?? []).map(a => a.step_id));
|
|
370
|
-
// For each consumed artifact, find the producing step and check it completed
|
|
371
367
|
for (const consumed of currentEntry.consumes) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires artifact from step '${producer.step_id}' (${consumed.description}). Run step '${producer.step_id}' first.`);
|
|
368
|
+
if (consumed.required === false)
|
|
369
|
+
continue;
|
|
370
|
+
const producerStep = consumed.from ?? findProducerStep(activeWf.handoff_chain, consumed) ?? "external";
|
|
371
|
+
const fact = artifactFacts.find(a => artifactMatches(a, consumed, producerStep, wfState.workflow));
|
|
372
|
+
if (!fact) {
|
|
373
|
+
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires artifact '${consumed.description}' from '${producerStep}', but no verified manifest was resolved.`);
|
|
374
|
+
}
|
|
375
|
+
if (consumed.type === "git_commit" && typeof fact.metadata?.commit !== "string") {
|
|
376
|
+
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires git_commit artifact from '${producerStep}', but no commit identity was recorded.`);
|
|
382
377
|
}
|
|
383
378
|
}
|
|
384
379
|
return null;
|
|
@@ -390,22 +385,52 @@ function enforceHandoffConsumes(ir, wfState, existingPaths) {
|
|
|
390
385
|
*/
|
|
391
386
|
export function enforceHandoffProduces(ir, wfState) {
|
|
392
387
|
const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
|
|
393
|
-
if (!activeWf || activeWf.handoff_chain
|
|
388
|
+
if (!activeWf || (activeWf.handoff_chain?.length ?? 0) === 0)
|
|
394
389
|
return null;
|
|
395
390
|
const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
|
|
396
391
|
if (!currentEntry?.produces || currentEntry.produces.length === 0)
|
|
397
392
|
return null;
|
|
398
|
-
const
|
|
393
|
+
const facts = wfState.artifact_facts ?? [];
|
|
399
394
|
for (const produced of currentEntry.produces) {
|
|
400
|
-
if (produced.
|
|
395
|
+
if (produced.required === false)
|
|
396
|
+
continue;
|
|
397
|
+
const fact = facts.find(a => artifactMatches(a, produced, wfState.current_step, wfState.workflow));
|
|
398
|
+
if (!fact) {
|
|
401
399
|
return {
|
|
402
|
-
output: blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}'
|
|
400
|
+
output: blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}' before proceeding; no verified manifest was resolved.`),
|
|
401
|
+
trace: { matched_rule: "handoff", step_id: wfState.current_step },
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (produced.type === "git_commit" && typeof fact.metadata?.commit !== "string") {
|
|
405
|
+
return {
|
|
406
|
+
output: blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce git_commit artifact '${produced.description}' with recorded commit identity before proceeding.`),
|
|
403
407
|
trace: { matched_rule: "handoff", step_id: wfState.current_step },
|
|
404
408
|
};
|
|
405
409
|
}
|
|
406
410
|
}
|
|
407
411
|
return null;
|
|
408
412
|
}
|
|
413
|
+
function findProducerStep(handoffChain, consumed) {
|
|
414
|
+
const producer = handoffChain.find(h => h.produces?.some(p => p.type === consumed.type && ((p.path && consumed.path && p.path === consumed.path) ||
|
|
415
|
+
(p.name && consumed.name && p.name === consumed.name) ||
|
|
416
|
+
(p.artifact_id && consumed.artifact_id && p.artifact_id === consumed.artifact_id))));
|
|
417
|
+
return producer?.step_id ?? null;
|
|
418
|
+
}
|
|
419
|
+
function artifactMatches(fact, artifact, producerStep, workflow) {
|
|
420
|
+
if (fact.workflow !== workflow)
|
|
421
|
+
return false;
|
|
422
|
+
if (fact.producer_step !== producerStep)
|
|
423
|
+
return false;
|
|
424
|
+
if (fact.type !== artifact.type)
|
|
425
|
+
return false;
|
|
426
|
+
if (artifact.path && fact.contract_path !== artifact.path && fact.path !== artifact.path)
|
|
427
|
+
return false;
|
|
428
|
+
if (artifact.name && fact.name !== artifact.name && fact.identity !== artifact.name)
|
|
429
|
+
return false;
|
|
430
|
+
if (artifact.artifact_id && fact.artifact_id !== artifact.artifact_id && fact.identity !== artifact.artifact_id)
|
|
431
|
+
return false;
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
409
434
|
function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
410
435
|
if (!input.agent_type)
|
|
411
436
|
return null;
|
|
@@ -414,7 +439,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
|
414
439
|
const filePath = extractFilePath(input);
|
|
415
440
|
if (!filePath)
|
|
416
441
|
return null;
|
|
417
|
-
return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths);
|
|
442
|
+
return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths, true);
|
|
418
443
|
}
|
|
419
444
|
// Bash: extract potential write targets from command
|
|
420
445
|
if (input.tool_name === "Bash") {
|
|
@@ -425,7 +450,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
|
425
450
|
if (writePaths.length === 0)
|
|
426
451
|
return null;
|
|
427
452
|
for (const p of writePaths) {
|
|
428
|
-
const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths);
|
|
453
|
+
const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths, false);
|
|
429
454
|
if (result)
|
|
430
455
|
return result;
|
|
431
456
|
}
|
|
@@ -434,7 +459,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
|
434
459
|
return null;
|
|
435
460
|
}
|
|
436
461
|
/** Check a single file path against role scope. Shared by Write tools and Bash. */
|
|
437
|
-
function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = []) {
|
|
462
|
+
function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = [], blockViolation = true) {
|
|
438
463
|
const cwd = input.cwd;
|
|
439
464
|
let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
|
|
440
465
|
// Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
|
|
@@ -449,10 +474,16 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePa
|
|
|
449
474
|
? [...writeGlobs, ...additionalWritePaths]
|
|
450
475
|
: writeGlobs;
|
|
451
476
|
if (allWriteGlobs.length === 0) {
|
|
452
|
-
|
|
477
|
+
const message = `Role '${entry.role_name}' has no write permission (path: ${filePath}).`;
|
|
478
|
+
return blockViolation
|
|
479
|
+
? blockOutput(`[Intent DNA]: ${message}`)
|
|
480
|
+
: allowOutput(`WARN [Intent DNA]: ${message}`);
|
|
453
481
|
}
|
|
454
482
|
if (!checkWriteAllowed(relativePath, allWriteGlobs)) {
|
|
455
|
-
|
|
483
|
+
const message = `Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${allWriteGlobs.join(", ")}).`;
|
|
484
|
+
return blockViolation
|
|
485
|
+
? blockOutput(`[Intent DNA]: ${message}`)
|
|
486
|
+
: allowOutput(`WARN [Intent DNA]: ${message}`);
|
|
456
487
|
}
|
|
457
488
|
return null; // Role matched, write allowed
|
|
458
489
|
}
|
|
@@ -540,20 +571,48 @@ function enforceOutputSchema(roles, input) {
|
|
|
540
571
|
return null;
|
|
541
572
|
}
|
|
542
573
|
// ── Public Helpers ──────────────────────────────────────────
|
|
543
|
-
/** Check if a file path is allowed by a list of write globs
|
|
574
|
+
/** Check if a file path is allowed by a list of write globs. */
|
|
544
575
|
export function checkWriteAllowed(filePath, allowedGlobs) {
|
|
576
|
+
const normalizedPath = normalize(filePath).replace(/\\/g, "/");
|
|
545
577
|
for (const glob of allowedGlobs) {
|
|
546
|
-
const
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
578
|
+
const normalizedGlob = normalize(glob).replace(/\\/g, "/");
|
|
579
|
+
if (normalizedGlob.includes("*")) {
|
|
580
|
+
if (globToRegExp(normalizedGlob).test(normalizedPath))
|
|
581
|
+
return true;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (normalizedPath.startsWith(normalizedGlob))
|
|
553
585
|
return true;
|
|
554
586
|
}
|
|
555
587
|
return false;
|
|
556
588
|
}
|
|
589
|
+
function globToRegExp(glob) {
|
|
590
|
+
let source = "^";
|
|
591
|
+
for (let i = 0; i < glob.length; i++) {
|
|
592
|
+
const char = glob[i];
|
|
593
|
+
if (char === "*") {
|
|
594
|
+
if (glob[i + 1] === "*") {
|
|
595
|
+
if (glob[i + 2] === "/") {
|
|
596
|
+
source += "(?:.*/)?";
|
|
597
|
+
i += 2;
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
source += ".*";
|
|
601
|
+
i += 1;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
source += "[^/]*";
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
source += escapeRegExp(char);
|
|
610
|
+
}
|
|
611
|
+
return new RegExp(`${source}$`);
|
|
612
|
+
}
|
|
613
|
+
function escapeRegExp(char) {
|
|
614
|
+
return char.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
|
|
615
|
+
}
|
|
557
616
|
/**
|
|
558
617
|
* Extract directory prefix from a glob pattern.
|
|
559
618
|
* "src/**\/*.ts" → "src/"
|
|
@@ -624,7 +683,9 @@ function isWriteTool(toolName) {
|
|
|
624
683
|
return WRITE_TOOLS.has(toolName);
|
|
625
684
|
}
|
|
626
685
|
function extractFilePath(input) {
|
|
627
|
-
const fp = input.
|
|
686
|
+
const fp = input.tool_name === "NotebookEdit"
|
|
687
|
+
? input.tool_input.notebook_path
|
|
688
|
+
: input.tool_input.file_path;
|
|
628
689
|
if (typeof fp === "string" && fp)
|
|
629
690
|
return fp;
|
|
630
691
|
return null;
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* All read operations are fail-safe (return null on error, never throw).
|
|
10
10
|
* All write operations use atomic temp+rename pattern.
|
|
11
11
|
*/
|
|
12
|
-
import type { CompletedArtifactEntry, VerifierKind, VerifierSeverity, VerifierWhen } from "../schema/types.js";
|
|
12
|
+
import type { ArtifactFact, ArtifactKey, ArtifactManifest, CompletedArtifactEntry, HandoffArtifact, HandoffType, VerifierKind, VerifierSeverity, VerifierWhen } from "../schema/types.js";
|
|
13
13
|
/** Workflow state written by runtime and read by hooks */
|
|
14
14
|
export interface DNAWorkflowState {
|
|
15
15
|
active: boolean;
|
|
@@ -19,6 +19,8 @@ export interface DNAWorkflowState {
|
|
|
19
19
|
iteration: number;
|
|
20
20
|
session_id: string;
|
|
21
21
|
started_at: string;
|
|
22
|
+
inputs?: Record<string, string>;
|
|
23
|
+
resolved_variables?: Record<string, string>;
|
|
22
24
|
completed_artifacts?: CompletedArtifactEntry[];
|
|
23
25
|
}
|
|
24
26
|
/** Audit log entry (one per line in JSON Lines format) */
|
|
@@ -48,12 +50,28 @@ export interface SurgeonAttemptState {
|
|
|
48
50
|
lesson: string;
|
|
49
51
|
}>;
|
|
50
52
|
}
|
|
53
|
+
export declare function safePathComponent(value: string, label: string): string;
|
|
51
54
|
/**
|
|
52
55
|
* Resolve state directory path.
|
|
53
56
|
* With session isolation: `.dna/state/sessions/<sessionId>/`
|
|
54
57
|
* Without: `.dna/state/`
|
|
55
58
|
*/
|
|
56
59
|
export declare function resolveStateDir(projectDir: string, sessionId?: string): string;
|
|
60
|
+
export declare const ARTIFACT_RESOLVER_VERSION = 1;
|
|
61
|
+
export declare function isRequiredArtifact(artifact: HandoffArtifact): boolean;
|
|
62
|
+
export declare function artifactIdentity(artifact: HandoffArtifact, resolvedPath?: string): string | null;
|
|
63
|
+
export declare function artifactHash(key: ArtifactKey): string;
|
|
64
|
+
export declare function buildArtifactKey(params: {
|
|
65
|
+
workflow: string;
|
|
66
|
+
sessionId: string;
|
|
67
|
+
producerStep: string;
|
|
68
|
+
type: HandoffType;
|
|
69
|
+
identity: string;
|
|
70
|
+
}): ArtifactKey;
|
|
71
|
+
export declare function artifactManifestPath(projectDir: string, key: ArtifactKey): string;
|
|
72
|
+
export declare function resolveArtifactTemplate(template: string, state?: Pick<DNAWorkflowState, "inputs" | "resolved_variables">): string | null;
|
|
73
|
+
export declare function writeArtifactManifest(projectDir: string, manifest: Omit<ArtifactManifest, "id" | "recorded_at" | "resolver_version">): Promise<ArtifactFact>;
|
|
74
|
+
export declare function readArtifactManifest(projectDir: string, key: ArtifactKey): Promise<ArtifactFact | null>;
|
|
57
75
|
/**
|
|
58
76
|
* Read current workflow state. Returns null if not found or stale.
|
|
59
77
|
* @deprecated Prefer `new DNAStateManager(projectDir, sessionId).readWorkflowState()`.
|
|
@@ -83,6 +101,8 @@ export declare function appendAudit(projectDir: string, entry: AuditEntry): Prom
|
|
|
83
101
|
export declare function appendCompletedArtifact(projectDir: string, stepId: string, artifact: {
|
|
84
102
|
type: string;
|
|
85
103
|
path: string;
|
|
104
|
+
artifact_id?: string;
|
|
105
|
+
metadata?: Record<string, unknown>;
|
|
86
106
|
}, sessionId?: string): Promise<void>;
|
|
87
107
|
/** Atomic write: write to temp file then rename. */
|
|
88
108
|
export declare function atomicWrite(filePath: string, data: string): Promise<void>;
|
package/dist/hooks/state.js
CHANGED
|
@@ -10,10 +10,27 @@
|
|
|
10
10
|
* All write operations use atomic temp+rename pattern.
|
|
11
11
|
*/
|
|
12
12
|
import { readFile, writeFile, rename, mkdir, appendFile, unlink, readdir, stat } from "node:fs/promises";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
13
14
|
import { join, dirname } from "node:path";
|
|
14
15
|
// ── Constants ──────────────────────────────────────────────
|
|
15
16
|
const WORKFLOW_FILE = "workflow.json";
|
|
16
17
|
const DEFAULT_STALENESS_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
18
|
+
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9._-]{1,128}$/;
|
|
19
|
+
const USER_ARTIFACT_PATH_ARGUMENTS = new Set(["ARGUMENTS", "arguments", "task_id"]);
|
|
20
|
+
export function safePathComponent(value, label) {
|
|
21
|
+
if (value === "." || value === ".." || !SAFE_PATH_COMPONENT.test(value)) {
|
|
22
|
+
throw new Error(`Invalid ${label}`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function safeUserArtifactPathArgument(value, label) {
|
|
27
|
+
try {
|
|
28
|
+
return safePathComponent(value, label);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
// ── Path Resolution ────────────────────────────────────────
|
|
18
35
|
/**
|
|
19
36
|
* Resolve state directory path.
|
|
@@ -23,10 +40,97 @@ const DEFAULT_STALENESS_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
|
23
40
|
export function resolveStateDir(projectDir, sessionId) {
|
|
24
41
|
const base = join(projectDir, ".dna", "state");
|
|
25
42
|
if (sessionId) {
|
|
26
|
-
return join(base, "sessions", sessionId);
|
|
43
|
+
return join(base, "sessions", safePathComponent(sessionId, "session_id"));
|
|
27
44
|
}
|
|
28
45
|
return base;
|
|
29
46
|
}
|
|
47
|
+
export const ARTIFACT_RESOLVER_VERSION = 1;
|
|
48
|
+
export function isRequiredArtifact(artifact) {
|
|
49
|
+
return artifact.required !== false;
|
|
50
|
+
}
|
|
51
|
+
export function artifactIdentity(artifact, resolvedPath) {
|
|
52
|
+
return artifact.artifact_id ?? artifact.name ?? resolvedPath ?? (artifact.type === "git_commit" ? "git_commit" : null);
|
|
53
|
+
}
|
|
54
|
+
export function artifactHash(key) {
|
|
55
|
+
return createHash("sha256")
|
|
56
|
+
.update(JSON.stringify(key))
|
|
57
|
+
.digest("hex")
|
|
58
|
+
.slice(0, 16);
|
|
59
|
+
}
|
|
60
|
+
export function buildArtifactKey(params) {
|
|
61
|
+
return {
|
|
62
|
+
workflow: params.workflow,
|
|
63
|
+
session_id: params.sessionId,
|
|
64
|
+
producer_step: params.producerStep,
|
|
65
|
+
type: params.type,
|
|
66
|
+
identity: params.identity,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function artifactManifestPath(projectDir, key) {
|
|
70
|
+
return join(resolveStateDir(projectDir, key.session_id), "handoffs", safePathComponent(key.workflow, "workflow"), safePathComponent(key.producer_step, "producer_step"), `${artifactHash(key)}.json`);
|
|
71
|
+
}
|
|
72
|
+
export function resolveArtifactTemplate(template, state) {
|
|
73
|
+
let resolved = template;
|
|
74
|
+
const inputs = state?.inputs ?? {};
|
|
75
|
+
const resolvedVariables = state?.resolved_variables ?? {};
|
|
76
|
+
resolved = resolved.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
|
|
77
|
+
const inputValue = inputs[name];
|
|
78
|
+
if (inputValue !== undefined) {
|
|
79
|
+
return safeUserArtifactPathArgument(inputValue, name) ?? `{{${name}}}`;
|
|
80
|
+
}
|
|
81
|
+
const resolvedValue = resolvedVariables[name];
|
|
82
|
+
if (resolvedValue === undefined)
|
|
83
|
+
return `{{${name}}}`;
|
|
84
|
+
if (USER_ARTIFACT_PATH_ARGUMENTS.has(name)) {
|
|
85
|
+
return safeUserArtifactPathArgument(resolvedValue, name) ?? `{{${name}}}`;
|
|
86
|
+
}
|
|
87
|
+
return resolvedValue;
|
|
88
|
+
});
|
|
89
|
+
const args = inputs.ARGUMENTS ?? inputs.arguments ?? inputs.task_id ?? resolvedVariables.ARGUMENTS ?? resolvedVariables.arguments ?? resolvedVariables.task_id;
|
|
90
|
+
if (resolved.includes("$ARGUMENTS")) {
|
|
91
|
+
if (args === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
const safeArgs = safeUserArtifactPathArgument(args, "ARGUMENTS");
|
|
94
|
+
if (!safeArgs)
|
|
95
|
+
return null;
|
|
96
|
+
resolved = resolved.replaceAll("$ARGUMENTS", safeArgs);
|
|
97
|
+
}
|
|
98
|
+
if (/\{\{\w+\}\}/.test(resolved))
|
|
99
|
+
return null;
|
|
100
|
+
return resolved;
|
|
101
|
+
}
|
|
102
|
+
export async function writeArtifactManifest(projectDir, manifest) {
|
|
103
|
+
const id = artifactHash(manifest.key);
|
|
104
|
+
const full = {
|
|
105
|
+
...manifest,
|
|
106
|
+
id,
|
|
107
|
+
recorded_at: new Date().toISOString(),
|
|
108
|
+
resolver_version: ARTIFACT_RESOLVER_VERSION,
|
|
109
|
+
};
|
|
110
|
+
const manifestPath = artifactManifestPath(projectDir, manifest.key);
|
|
111
|
+
await atomicWrite(manifestPath, JSON.stringify(full, null, 2));
|
|
112
|
+
return { ...full, manifest_path: manifestPath };
|
|
113
|
+
}
|
|
114
|
+
export async function readArtifactManifest(projectDir, key) {
|
|
115
|
+
const manifestPath = artifactManifestPath(projectDir, key);
|
|
116
|
+
try {
|
|
117
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
118
|
+
if (manifest.resolver_version !== ARTIFACT_RESOLVER_VERSION)
|
|
119
|
+
return null;
|
|
120
|
+
if (manifest.key.workflow !== key.workflow || manifest.key.session_id !== key.session_id ||
|
|
121
|
+
manifest.key.producer_step !== key.producer_step || manifest.key.type !== key.type ||
|
|
122
|
+
manifest.key.identity !== key.identity)
|
|
123
|
+
return null;
|
|
124
|
+
if (manifest.workflow !== key.workflow || manifest.session_id !== key.session_id ||
|
|
125
|
+
manifest.producer_step !== key.producer_step || manifest.type !== key.type ||
|
|
126
|
+
manifest.identity !== key.identity)
|
|
127
|
+
return null;
|
|
128
|
+
return { ...manifest, manifest_path: manifestPath };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
30
134
|
// ── Workflow State ─────────────────────────────────────────
|
|
31
135
|
/**
|
|
32
136
|
* Read current workflow state. Returns null if not found or stale.
|
|
@@ -121,13 +225,24 @@ export async function appendCompletedArtifact(projectDir, stepId, artifact, sess
|
|
|
121
225
|
stepEntry = { step_id: stepId, artifacts: [] };
|
|
122
226
|
artifacts.push(stepEntry);
|
|
123
227
|
}
|
|
124
|
-
|
|
125
|
-
|
|
228
|
+
const verifiedAt = new Date().toISOString();
|
|
229
|
+
const existing = stepEntry.artifacts.find(a => a.type === artifact.type && a.path === artifact.path);
|
|
230
|
+
if (existing) {
|
|
231
|
+
existing.verified_at = verifiedAt;
|
|
232
|
+
if (artifact.artifact_id)
|
|
233
|
+
existing.artifact_id = artifact.artifact_id;
|
|
234
|
+
if (artifact.metadata)
|
|
235
|
+
existing.metadata = artifact.metadata;
|
|
236
|
+
state.completed_artifacts = artifacts;
|
|
237
|
+
await writeWorkflowState(projectDir, state, sessionId);
|
|
126
238
|
return;
|
|
239
|
+
}
|
|
127
240
|
stepEntry.artifacts.push({
|
|
128
241
|
type: artifact.type,
|
|
129
242
|
path: artifact.path,
|
|
130
|
-
verified_at:
|
|
243
|
+
verified_at: verifiedAt,
|
|
244
|
+
...(artifact.artifact_id ? { artifact_id: artifact.artifact_id } : {}),
|
|
245
|
+
...(artifact.metadata ? { metadata: artifact.metadata } : {}),
|
|
131
246
|
});
|
|
132
247
|
state.completed_artifacts = artifacts;
|
|
133
248
|
await writeWorkflowState(projectDir, state, sessionId);
|
package/dist/mcp/tools-state.js
CHANGED
|
@@ -11,6 +11,10 @@ import { resolve } from "node:path";
|
|
|
11
11
|
import { readWorkflowState, writeWorkflowState, readTraces, listSessions } from "../hooks/state.js";
|
|
12
12
|
import { loadCompiledIR } from "../runtime/plugin-adapter.js";
|
|
13
13
|
import { textResult } from "./server.js";
|
|
14
|
+
function isStringRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null &&
|
|
16
|
+
Object.values(value).every(v => typeof v === "string");
|
|
17
|
+
}
|
|
14
18
|
export function createStateTools(projectDir) {
|
|
15
19
|
return [
|
|
16
20
|
// ── dna_status ────────────────────────────────────────
|
|
@@ -95,6 +99,8 @@ export function createStateTools(projectDir) {
|
|
|
95
99
|
iteration: { type: "number", description: "Current iteration number" },
|
|
96
100
|
active: { type: "boolean", description: "Whether workflow is active (false to deactivate)" },
|
|
97
101
|
session_id: { type: "string", description: "Session ID for isolation" },
|
|
102
|
+
inputs: { type: "object", description: "Workflow runtime inputs such as ARGUMENTS" },
|
|
103
|
+
resolved_variables: { type: "object", description: "Resolved template variables for artifact paths" },
|
|
98
104
|
},
|
|
99
105
|
required: ["workflow", "current_step", "current_role"],
|
|
100
106
|
},
|
|
@@ -109,6 +115,8 @@ export function createStateTools(projectDir) {
|
|
|
109
115
|
iteration: typeof args.iteration === "number" ? args.iteration : (existing?.iteration ?? 1),
|
|
110
116
|
session_id: sessionId ?? existing?.session_id ?? "",
|
|
111
117
|
started_at: existing?.started_at ?? new Date().toISOString(),
|
|
118
|
+
inputs: isStringRecord(args.inputs) ? args.inputs : existing?.inputs,
|
|
119
|
+
resolved_variables: isStringRecord(args.resolved_variables) ? args.resolved_variables : existing?.resolved_variables,
|
|
112
120
|
completed_artifacts: existing?.completed_artifacts,
|
|
113
121
|
};
|
|
114
122
|
await writeWorkflowState(projectDir, state, sessionId);
|
|
@@ -70,10 +70,10 @@ export function detectEnabledEvents(ir) {
|
|
|
70
70
|
const hasStopVerifiers = ir.verifier_specs?.some((spec) => spec.when === "stop" || spec.when === "pre_handoff" || spec.when === "post_step") ?? false;
|
|
71
71
|
const hasPostToolVerifiers = ir.verifier_specs?.some((spec) => spec.when === "post_tool_use") ?? false;
|
|
72
72
|
const hasHandoffs = ir.workflows_ir?.some((wf) => wf.handoff_chain.length > 0) ?? false;
|
|
73
|
-
if (hasGates || hasFilters || hasRoleScope || hasCheckpoints) {
|
|
73
|
+
if (hasGates || hasFilters || hasRoleScope || hasCheckpoints || hasHandoffs) {
|
|
74
74
|
events.push("preToolUse");
|
|
75
75
|
}
|
|
76
|
-
if (ir.post_execution_validators.length > 0 || hasPostToolVerifiers) {
|
|
76
|
+
if (ir.post_execution_validators.length > 0 || hasPostToolVerifiers || hasHandoffs) {
|
|
77
77
|
events.push("postToolUse");
|
|
78
78
|
}
|
|
79
79
|
if (ir.prompt_directives.some(d => d.priority === "high")) {
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Skills are compiled views of DNA's structured definitions — not stored content.
|
|
8
8
|
*/
|
|
9
|
-
import type { WorkflowPlan, RoleDef, ConstraintIR } from "../schema/types.js";
|
|
9
|
+
import type { WorkflowPlan, WorkflowDef, RoleDef, ConstraintIR, ControllerDef } from "../schema/types.js";
|
|
10
10
|
export interface SkillResult {
|
|
11
11
|
name: string;
|
|
12
12
|
fileName: string;
|
|
13
13
|
dirName: string;
|
|
14
14
|
content: string;
|
|
15
15
|
}
|
|
16
|
+
export declare function compileControllerToSkill(controllerKey: string, controller: ControllerDef, variables?: Record<string, string>, workflows?: Record<string, WorkflowDef>): SkillResult;
|
|
16
17
|
/**
|
|
17
18
|
* Compile a WorkflowPlan + roles + IR into a SKILL.md file.
|
|
18
19
|
* Variables from DNA config are substituted into prompts and descriptions.
|