intentdna 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/dist/cli/commands/assets.d.ts +1 -0
- package/dist/cli/commands/assets.js +51 -17
- package/dist/cli/index.js +0 -0
- package/dist/hooks/cli.d.ts +3 -0
- package/dist/hooks/cli.js +109 -16
- package/dist/hooks/event-registry.js +1 -1
- package/dist/hooks/state.d.ts +2 -0
- package/dist/hooks/state.js +4 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-artifacts.d.ts +2 -0
- package/dist/mcp/tools-artifacts.js +137 -0
- package/dist/runtime/settings-adapter.js +2 -1
- package/dist/runtime/skill-adapter.js +34 -4
- package/dist/runtime/verifier.d.ts +3 -1
- package/dist/runtime/verifier.js +82 -27
- package/dist/templates/flutter-rewrite.dna.yaml +19 -9
- package/hooks/hooks.json +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
12
|
+
"version": "1.8.4",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Samuel"
|
|
15
15
|
},
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"version": "1.8.
|
|
28
|
+
"version": "1.8.4"
|
|
29
29
|
}
|
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ IntentDNA is not a model company, generic agent framework, prompt library, canva
|
|
|
17
17
|
## Install or update
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install --global intentdna@1.8.
|
|
20
|
+
npm install --global intentdna@1.8.4
|
|
21
21
|
dna --version
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Version `1.8.
|
|
24
|
+
Version `1.8.4` makes delegated workflow completion gates enforceable at the real Claude handoff boundary, returns bounded validator diagnostics to retries, and links C5-backed suggestions to concrete capture event IDs. Online generates copyable local commands only; you still run and review them in your own project.
|
|
25
25
|
|
|
26
26
|
## Product Shape
|
|
27
27
|
|
|
@@ -644,7 +644,7 @@ function suggestionAction(params) {
|
|
|
644
644
|
asset: params.asset,
|
|
645
645
|
suggestionId: params.suggestion.id,
|
|
646
646
|
saveTemplate: params.saveTemplate,
|
|
647
|
-
workflow: params.workflow,
|
|
647
|
+
workflow: params.workflow ?? params.suggestion.targetWorkflow,
|
|
648
648
|
}),
|
|
649
649
|
};
|
|
650
650
|
}
|
|
@@ -667,7 +667,7 @@ function buildSuggestionImprovementRoute(params) {
|
|
|
667
667
|
? suggestionReviewCommand({
|
|
668
668
|
asset: params.asset.name,
|
|
669
669
|
suggestionId: params.draftSuggestion.id,
|
|
670
|
-
workflow: "fix",
|
|
670
|
+
workflow: params.draftSuggestion.targetWorkflow ?? "fix",
|
|
671
671
|
decision: "accept",
|
|
672
672
|
})
|
|
673
673
|
: undefined;
|
|
@@ -722,8 +722,8 @@ function buildSuggestionImprovementRoute(params) {
|
|
|
722
722
|
];
|
|
723
723
|
}
|
|
724
724
|
function firstDraftableSuggestion(suggestions) {
|
|
725
|
-
return suggestions.find((suggestion) => suggestion.
|
|
726
|
-
suggestion.
|
|
725
|
+
return suggestions.find((suggestion) => suggestion.id === WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts && suggestion.targetWorkflow) ??
|
|
726
|
+
suggestions.find((suggestion) => suggestion.type === "template_improvement");
|
|
727
727
|
}
|
|
728
728
|
function evidenceSuggestionDraftCommand(asset, evidence, saveTemplate) {
|
|
729
729
|
if (!evidence || asset.status.evidencePosture !== "dogfood_linked")
|
|
@@ -753,7 +753,10 @@ function c5GapCaptures(index) {
|
|
|
753
753
|
}
|
|
754
754
|
function c5EvidenceRefs(captures) {
|
|
755
755
|
const refs = captures.flatMap((capture) => [
|
|
756
|
+
{ type: "capture_event_id", ref: capture.eventId },
|
|
756
757
|
{ type: "capture_event", ref: capture.path },
|
|
758
|
+
...(capture.workflowAsset !== "unknown" ? [{ type: "workflow_asset", ref: capture.workflowAsset }] : []),
|
|
759
|
+
...(capture.workflowId !== "unknown" ? [{ type: "workflow", ref: capture.workflowId }] : []),
|
|
757
760
|
...capture.evidenceRefs,
|
|
758
761
|
]);
|
|
759
762
|
const seen = new Set();
|
|
@@ -765,6 +768,31 @@ function c5EvidenceRefs(captures) {
|
|
|
765
768
|
return true;
|
|
766
769
|
});
|
|
767
770
|
}
|
|
771
|
+
function assetWorkflowTarget(asset, runtimeWorkflowId) {
|
|
772
|
+
if (!runtimeWorkflowId || runtimeWorkflowId === "unknown")
|
|
773
|
+
return undefined;
|
|
774
|
+
const workflowIds = new Set(asset.workflows.map((workflow) => workflow.id));
|
|
775
|
+
if (workflowIds.has(runtimeWorkflowId))
|
|
776
|
+
return runtimeWorkflowId;
|
|
777
|
+
if (asset.namespace && runtimeWorkflowId.startsWith(`${asset.namespace}_`)) {
|
|
778
|
+
const unprefixed = runtimeWorkflowId.slice(asset.namespace.length + 1);
|
|
779
|
+
if (workflowIds.has(unprefixed))
|
|
780
|
+
return unprefixed;
|
|
781
|
+
}
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
function c5TargetWorkflow(asset, captures) {
|
|
785
|
+
const targets = new Set();
|
|
786
|
+
for (const capture of captures) {
|
|
787
|
+
const target = assetWorkflowTarget(asset, capture.workflowId);
|
|
788
|
+
if (!target)
|
|
789
|
+
return undefined;
|
|
790
|
+
targets.add(target);
|
|
791
|
+
if (targets.size > 1)
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
return targets.size === 1 ? [...targets][0] : undefined;
|
|
795
|
+
}
|
|
768
796
|
function c5CaptureBootstrapCommands(assetName) {
|
|
769
797
|
return [
|
|
770
798
|
`dna init --template ${assetName}`,
|
|
@@ -836,7 +864,7 @@ function defaultSuggestionCommands(params) {
|
|
|
836
864
|
const reviewCommand = suggestionReviewCommand({
|
|
837
865
|
asset: params.asset.name,
|
|
838
866
|
suggestionId: params.suggestion.id,
|
|
839
|
-
workflow: "fix",
|
|
867
|
+
workflow: params.suggestion.targetWorkflow ?? "fix",
|
|
840
868
|
decision: "accept",
|
|
841
869
|
});
|
|
842
870
|
return [
|
|
@@ -846,7 +874,7 @@ function defaultSuggestionCommands(params) {
|
|
|
846
874
|
asset: params.asset.name,
|
|
847
875
|
suggestionId: params.suggestion.id,
|
|
848
876
|
saveTemplate: `${params.asset.name}-v2`,
|
|
849
|
-
workflow: "fix",
|
|
877
|
+
workflow: params.suggestion.targetWorkflow ?? "fix",
|
|
850
878
|
}),
|
|
851
879
|
`dna assets diff ${params.asset.name} ${params.asset.name}-v2`,
|
|
852
880
|
`dna assets adoption ${params.asset.name}-v2`,
|
|
@@ -926,20 +954,21 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
|
|
|
926
954
|
});
|
|
927
955
|
}
|
|
928
956
|
if (c5Gaps.length > 0) {
|
|
957
|
+
const targetWorkflow = c5TargetWorkflow(asset, c5Gaps);
|
|
929
958
|
const evidenceCommand = `dna assets evidence ${asset.name} --json`;
|
|
930
959
|
const suggestionsCommand = `dna workflow suggestions ${asset.name} --json`;
|
|
931
|
-
const reviewCommand = suggestionReviewCommand({
|
|
960
|
+
const reviewCommand = targetWorkflow ? suggestionReviewCommand({
|
|
932
961
|
asset: asset.name,
|
|
933
962
|
suggestionId: WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts,
|
|
934
|
-
workflow:
|
|
963
|
+
workflow: targetWorkflow,
|
|
935
964
|
decision: "accept",
|
|
936
|
-
});
|
|
937
|
-
const draftCommand = suggestionDraftCommand({
|
|
965
|
+
}) : undefined;
|
|
966
|
+
const draftCommand = targetWorkflow ? suggestionDraftCommand({
|
|
938
967
|
asset: asset.name,
|
|
939
968
|
suggestionId: WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts,
|
|
940
969
|
saveTemplate: `${asset.name}-v2`,
|
|
941
|
-
workflow:
|
|
942
|
-
});
|
|
970
|
+
workflow: targetWorkflow,
|
|
971
|
+
}) : undefined;
|
|
943
972
|
suggestions.push({
|
|
944
973
|
id: WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts,
|
|
945
974
|
type: "evidence_improvement",
|
|
@@ -947,14 +976,17 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
|
|
|
947
976
|
title: "Review captured local run facts before changing the workflow asset",
|
|
948
977
|
evidence: c5Gaps.slice(0, 5).map(compactEvidenceCapture).join("; "),
|
|
949
978
|
evidenceRefs: c5EvidenceRefs(c5Gaps),
|
|
979
|
+
targetWorkflow,
|
|
950
980
|
rationale: "C5 hook evidence shows blocked, failed, retry, unsupported, or human-intervention facts. Evidence can suggest a reviewed change, but it must not mutate policy or templates automatically.",
|
|
951
981
|
sourceModules: [],
|
|
952
|
-
nextAction:
|
|
982
|
+
nextAction: reviewCommand
|
|
983
|
+
? `Review and accept the C5-backed suggestion before drafting: ${reviewCommand}`
|
|
984
|
+
: `Inspect the C5 evidence and choose one matching workflow before review; mixed or unknown workflow attribution cannot be routed automatically.`,
|
|
953
985
|
nextCommands: [
|
|
954
986
|
evidenceCommand,
|
|
955
987
|
suggestionsCommand,
|
|
956
|
-
reviewCommand,
|
|
957
|
-
draftCommand,
|
|
988
|
+
...(reviewCommand ? [reviewCommand] : []),
|
|
989
|
+
...(draftCommand ? [draftCommand] : []),
|
|
958
990
|
`record explicit manager rationale for any C5-derived change to ${asset.name}`,
|
|
959
991
|
],
|
|
960
992
|
mutationBoundary: "C5 captured facts are local review evidence only; they do not edit templates, mutate policy, adopt assets, bind organizations, sync runtime artifacts, upload cloud reports, or create runtime authority.",
|
|
@@ -1055,8 +1087,10 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
|
|
|
1055
1087
|
index,
|
|
1056
1088
|
suggestion,
|
|
1057
1089
|
}));
|
|
1058
|
-
const activeFollowup = active.length > 0 ? activeEvidenceFollowupReviewRoute(asset.name, assets, index) : undefined;
|
|
1059
1090
|
const draftSuggestion = firstDraftableSuggestion(completedSuggestions);
|
|
1091
|
+
const activeFollowup = active.length > 0 && draftSuggestion?.id !== WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts
|
|
1092
|
+
? activeEvidenceFollowupReviewRoute(asset.name, assets, index)
|
|
1093
|
+
: undefined;
|
|
1060
1094
|
const nextCommands = [
|
|
1061
1095
|
`dna assets evidence ${asset.name}`,
|
|
1062
1096
|
];
|
|
@@ -1075,7 +1109,7 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
|
|
|
1075
1109
|
nextCommands.push(suggestionReviewCommand({
|
|
1076
1110
|
asset: asset.name,
|
|
1077
1111
|
suggestionId: draftSuggestion.id,
|
|
1078
|
-
workflow: "fix",
|
|
1112
|
+
workflow: draftSuggestion.targetWorkflow ?? "fix",
|
|
1079
1113
|
decision: "accept",
|
|
1080
1114
|
}));
|
|
1081
1115
|
}
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -99,10 +99,13 @@ export declare function runVerifiersForTest(projectDir: string, ir: ConstraintIR
|
|
|
99
99
|
workflow: string;
|
|
100
100
|
current_step: string;
|
|
101
101
|
current_role: string;
|
|
102
|
+
iteration?: number;
|
|
103
|
+
started_at?: string;
|
|
102
104
|
inputs?: Record<string, string>;
|
|
103
105
|
resolved_variables?: Record<string, string>;
|
|
104
106
|
}, when: VerifierSpec["when"], sessionId?: string, options?: {
|
|
105
107
|
commandTimeoutMs?: number;
|
|
108
|
+
triggerEvent?: HookEvent;
|
|
106
109
|
}): Promise<VerifierResultEntry[]>;
|
|
107
110
|
export declare function runStopVerifiersForTest(projectDir: string, ir: ConstraintIR, workflowState: {
|
|
108
111
|
workflow: string;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from "node:url";
|
|
|
23
23
|
import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
|
|
24
24
|
import { validateHookInput } from "./schema.js";
|
|
25
25
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
26
|
-
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
|
|
26
|
+
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
|
|
27
27
|
import { hookEventsForSurface } from "./event-registry.js";
|
|
28
28
|
import { writeAuditEvent } from "../audit/index.js";
|
|
29
29
|
import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
|
|
@@ -347,13 +347,16 @@ export async function runHookEvent(options) {
|
|
|
347
347
|
};
|
|
348
348
|
let wfStateRaw = null;
|
|
349
349
|
// Load workflow state for events that need it (handoff context + PreCompact preservation)
|
|
350
|
-
if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
|
|
350
|
+
if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact" || event === "SubagentStop") {
|
|
351
351
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
352
352
|
if ("output" in workflowState) {
|
|
353
353
|
await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
|
|
354
354
|
return finish(workflowState.output);
|
|
355
355
|
}
|
|
356
356
|
wfStateRaw = workflowState.state;
|
|
357
|
+
if (event === "SubagentStop" && wfStateRaw && !subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
|
|
358
|
+
wfStateRaw = null;
|
|
359
|
+
}
|
|
357
360
|
if (wfStateRaw && wfStateRaw.active) {
|
|
358
361
|
state.workflowState = {
|
|
359
362
|
current_step: wfStateRaw.current_step,
|
|
@@ -363,13 +366,15 @@ export async function runHookEvent(options) {
|
|
|
363
366
|
completed_artifacts: wfStateRaw.completed_artifacts,
|
|
364
367
|
iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
|
|
365
368
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
if (event !== "SubagentStop") {
|
|
370
|
+
const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
|
|
371
|
+
if ("output" in artifactFacts) {
|
|
372
|
+
appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
|
|
373
|
+
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
|
|
374
|
+
return finish(artifactFacts.output);
|
|
375
|
+
}
|
|
376
|
+
state.artifactFacts = artifactFacts.facts;
|
|
371
377
|
}
|
|
372
|
-
state.artifactFacts = artifactFacts.facts;
|
|
373
378
|
}
|
|
374
379
|
}
|
|
375
380
|
if (event === "PreToolUse" && wfStateRaw?.active) {
|
|
@@ -400,6 +405,46 @@ export async function runHookEvent(options) {
|
|
|
400
405
|
return finish(gateResult.output);
|
|
401
406
|
}
|
|
402
407
|
}
|
|
408
|
+
if (event === "SubagentStop" && wfStateRaw?.active && subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
|
|
409
|
+
const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
|
|
410
|
+
if ("output" in artifactFacts) {
|
|
411
|
+
appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
|
|
412
|
+
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
|
|
413
|
+
return finish(artifactFacts.output);
|
|
414
|
+
}
|
|
415
|
+
const verifierContext = {
|
|
416
|
+
workflow: wfStateRaw.workflow,
|
|
417
|
+
current_step: wfStateRaw.current_step,
|
|
418
|
+
current_role: wfStateRaw.current_role,
|
|
419
|
+
iteration: wfStateRaw.iteration,
|
|
420
|
+
started_at: wfStateRaw.started_at,
|
|
421
|
+
inputs: wfStateRaw.inputs,
|
|
422
|
+
resolved_variables: wfStateRaw.resolved_variables,
|
|
423
|
+
artifact_facts: artifactFacts.facts,
|
|
424
|
+
};
|
|
425
|
+
const verifierResults = [
|
|
426
|
+
...await runVerifiersForTest(projectDir, ir, verifierContext, "pre_handoff", sessionId, { triggerEvent: "SubagentStop" }),
|
|
427
|
+
...await runVerifiersForTest(projectDir, ir, verifierContext, "post_step", sessionId, { triggerEvent: "SubagentStop" }),
|
|
428
|
+
];
|
|
429
|
+
const blockingVerifierFailures = verifierResults.filter((result) => result.status === "fail" && result.severity === "block");
|
|
430
|
+
if (blockingVerifierFailures.length > 0) {
|
|
431
|
+
const output = blockOutput(`[Intent DNA] SubagentStop verifier failures at step '${wfStateRaw.current_step}':\n` +
|
|
432
|
+
blockingVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n"));
|
|
433
|
+
const traceId = randomUUID();
|
|
434
|
+
appendTrace(projectDir, {
|
|
435
|
+
trace_id: traceId,
|
|
436
|
+
event,
|
|
437
|
+
workflow: wfStateRaw.workflow,
|
|
438
|
+
step: wfStateRaw.current_step,
|
|
439
|
+
decision: "block",
|
|
440
|
+
reason: output.reason,
|
|
441
|
+
duration_ms: 0,
|
|
442
|
+
timestamp: new Date().toISOString(),
|
|
443
|
+
}, sessionId).catch(() => { });
|
|
444
|
+
await writeRuntimeDecision(output, { output, trace: { matched_rule: "validator", step_id: wfStateRaw.current_step } }, wfStateRaw, traceId);
|
|
445
|
+
return finish(output);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
403
448
|
// Special handling for Stop — needs async workflow state read + session summary
|
|
404
449
|
if (event === "Stop") {
|
|
405
450
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
@@ -423,6 +468,7 @@ export async function runHookEvent(options) {
|
|
|
423
468
|
workflow: wfState.workflow,
|
|
424
469
|
current_step: wfState.current_step,
|
|
425
470
|
current_role: wfState.current_role,
|
|
471
|
+
iteration: wfState.iteration,
|
|
426
472
|
started_at: wfState.started_at,
|
|
427
473
|
inputs: wfState.inputs,
|
|
428
474
|
resolved_variables: wfState.resolved_variables,
|
|
@@ -430,7 +476,7 @@ export async function runHookEvent(options) {
|
|
|
430
476
|
artifact_facts: stopArtifactFacts,
|
|
431
477
|
} : null;
|
|
432
478
|
const verifierResults = stopContext?.active
|
|
433
|
-
? await
|
|
479
|
+
? await runStopVerifiersForHook(projectDir, ir, stopContext, sessionId)
|
|
434
480
|
: [];
|
|
435
481
|
const blockingVerifierFailures = verifierResults.filter((result) => result.status === "fail" && result.severity === "block");
|
|
436
482
|
let stopOutput = blockingVerifierFailures.length > 0
|
|
@@ -660,16 +706,21 @@ function inferWorkflowAssetFromIr(ir, workflowId) {
|
|
|
660
706
|
const attribution = trustedEvidenceCaptureAttribution(ir, { workflowId });
|
|
661
707
|
return attribution.workflowAsset === "unknown" ? undefined : attribution.workflowAsset;
|
|
662
708
|
}
|
|
709
|
+
function agentTypeForWorkflowRole(role) {
|
|
710
|
+
if (!role)
|
|
711
|
+
return undefined;
|
|
712
|
+
return `dna-${role.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase()}`;
|
|
713
|
+
}
|
|
714
|
+
function subagentMatchesWorkflowRole(rawInput, wfState) {
|
|
715
|
+
return typeof rawInput.agent_type === "string" && rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
|
|
716
|
+
}
|
|
663
717
|
async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
|
|
664
718
|
const sessionId = typeof rawInput.session_id === "string"
|
|
665
719
|
? rawInput.session_id
|
|
666
720
|
: typeof rawInput.sessionId === "string"
|
|
667
721
|
? rawInput.sessionId
|
|
668
722
|
: undefined;
|
|
669
|
-
const
|
|
670
|
-
? await readWorkflowState(projectDir, sessionId)
|
|
671
|
-
: null;
|
|
672
|
-
const workflowState = sessionWorkflowState ?? await readWorkflowState(projectDir);
|
|
723
|
+
const workflowState = await readWorkflowStateWithRootFallback(projectDir, sessionId);
|
|
673
724
|
const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
|
|
674
725
|
return runCodexHook({ ...rawInput, event: codexHookEventForInput(rawInput, args) }, {
|
|
675
726
|
ir,
|
|
@@ -887,12 +938,21 @@ function artifactResolverErrorOutput(event, error) {
|
|
|
887
938
|
}
|
|
888
939
|
async function readWorkflowStateForHook(projectDir, event, sessionId) {
|
|
889
940
|
try {
|
|
890
|
-
|
|
941
|
+
const state = event === "SubagentStop"
|
|
942
|
+
? await readWorkflowStateWithRootFallback(projectDir, sessionId)
|
|
943
|
+
: await readWorkflowState(projectDir, sessionId);
|
|
944
|
+
return { state };
|
|
891
945
|
}
|
|
892
946
|
catch (error) {
|
|
893
947
|
return { output: artifactResolverErrorOutput(event, error) };
|
|
894
948
|
}
|
|
895
949
|
}
|
|
950
|
+
async function readWorkflowStateWithRootFallback(projectDir, sessionId) {
|
|
951
|
+
if (!sessionId)
|
|
952
|
+
return readWorkflowState(projectDir);
|
|
953
|
+
const sessionState = await readWorkflowState(projectDir, sessionId);
|
|
954
|
+
return sessionState ?? readWorkflowState(projectDir);
|
|
955
|
+
}
|
|
896
956
|
async function resolveWorkflowArtifactFactsForHook(projectDir, ir, wfState, event, sessionId) {
|
|
897
957
|
try {
|
|
898
958
|
return { facts: await resolveWorkflowArtifactFacts(projectDir, ir, wfState, sessionId) };
|
|
@@ -1082,6 +1142,8 @@ async function runVerifierSpec(projectDir, ir, spec, workflowState, sessionId, o
|
|
|
1082
1142
|
const entry = {
|
|
1083
1143
|
verifier_id: spec.id,
|
|
1084
1144
|
when: spec.when,
|
|
1145
|
+
iteration: workflowState.iteration,
|
|
1146
|
+
trigger_event: options?.triggerEvent,
|
|
1085
1147
|
severity: spec.severity,
|
|
1086
1148
|
kind: spec.kind,
|
|
1087
1149
|
workflow: workflowState.workflow,
|
|
@@ -1109,10 +1171,41 @@ export async function runVerifiersForTest(projectDir, ir, workflowState, when, s
|
|
|
1109
1171
|
}
|
|
1110
1172
|
return results;
|
|
1111
1173
|
}
|
|
1174
|
+
async function reusableSubagentStopResults(projectDir, ir, workflowState, when, sessionId) {
|
|
1175
|
+
if (!sessionId)
|
|
1176
|
+
return null;
|
|
1177
|
+
const specs = (ir.verifier_specs ?? []).filter((spec) => spec.when === when &&
|
|
1178
|
+
spec.workflow_name === workflowState.workflow &&
|
|
1179
|
+
spec.step_id === workflowState.current_step);
|
|
1180
|
+
if (specs.length === 0)
|
|
1181
|
+
return [];
|
|
1182
|
+
const startedAt = workflowState.started_at ? Date.parse(workflowState.started_at) : Number.NaN;
|
|
1183
|
+
const prior = (await readVerifierResults(projectDir, sessionId)).filter((result) => result.trigger_event === "SubagentStop" &&
|
|
1184
|
+
result.workflow === workflowState.workflow &&
|
|
1185
|
+
result.step_id === workflowState.current_step &&
|
|
1186
|
+
result.when === when &&
|
|
1187
|
+
(workflowState.iteration === undefined || result.iteration === workflowState.iteration) &&
|
|
1188
|
+
(Number.isNaN(startedAt) || Date.parse(result.timestamp) >= startedAt));
|
|
1189
|
+
const latestByVerifier = new Map();
|
|
1190
|
+
for (const result of prior)
|
|
1191
|
+
latestByVerifier.set(result.verifier_id, result);
|
|
1192
|
+
if (!specs.every((spec) => latestByVerifier.has(spec.id)))
|
|
1193
|
+
return null;
|
|
1194
|
+
return specs.map((spec) => latestByVerifier.get(spec.id));
|
|
1195
|
+
}
|
|
1196
|
+
async function runStopVerifiersForHook(projectDir, ir, workflowState, sessionId) {
|
|
1197
|
+
const results = [];
|
|
1198
|
+
for (const when of ["pre_handoff", "post_step"]) {
|
|
1199
|
+
const reusable = await reusableSubagentStopResults(projectDir, ir, workflowState, when, sessionId);
|
|
1200
|
+
results.push(...(reusable ?? await runVerifiersForTest(projectDir, ir, workflowState, when, sessionId, { triggerEvent: "Stop" })));
|
|
1201
|
+
}
|
|
1202
|
+
results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "stop", sessionId, { triggerEvent: "Stop" }));
|
|
1203
|
+
return results;
|
|
1204
|
+
}
|
|
1112
1205
|
export async function runStopVerifiersForTest(projectDir, ir, workflowState, sessionId, options) {
|
|
1113
1206
|
const results = [];
|
|
1114
|
-
results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "post_step", sessionId, options));
|
|
1115
1207
|
results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "pre_handoff", sessionId, options));
|
|
1208
|
+
results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "post_step", sessionId, options));
|
|
1116
1209
|
results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "stop", sessionId, options));
|
|
1117
1210
|
return results;
|
|
1118
1211
|
}
|
|
@@ -1229,7 +1322,7 @@ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
|
|
|
1229
1322
|
if (!wfState || !wfState.active)
|
|
1230
1323
|
return null;
|
|
1231
1324
|
// Match agent_type to current role in workflow
|
|
1232
|
-
const expectedAgentType =
|
|
1325
|
+
const expectedAgentType = agentTypeForWorkflowRole(wfState.current_role);
|
|
1233
1326
|
if (agentType !== expectedAgentType)
|
|
1234
1327
|
return null;
|
|
1235
1328
|
// Look up step config from the compiled IR's workflow
|
package/dist/hooks/state.d.ts
CHANGED
package/dist/hooks/state.js
CHANGED
|
@@ -338,6 +338,8 @@ export function verifierResultId(result) {
|
|
|
338
338
|
.update(JSON.stringify({
|
|
339
339
|
verifier_id: result.verifier_id,
|
|
340
340
|
when: result.when,
|
|
341
|
+
iteration: result.iteration ?? 0,
|
|
342
|
+
trigger_event: result.trigger_event ?? "",
|
|
341
343
|
severity: result.severity,
|
|
342
344
|
kind: result.kind,
|
|
343
345
|
workflow: result.workflow ?? "",
|
|
@@ -370,6 +372,8 @@ function verifierResultKey(result) {
|
|
|
370
372
|
return [
|
|
371
373
|
result.verifier_id,
|
|
372
374
|
result.when,
|
|
375
|
+
result.iteration ?? 0,
|
|
376
|
+
result.trigger_event ?? "",
|
|
373
377
|
result.severity,
|
|
374
378
|
result.kind,
|
|
375
379
|
result.workflow ?? "",
|
package/dist/mcp/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createCompileTools } from "./tools-compile.js";
|
|
|
16
16
|
import { createEnforceTools } from "./tools-enforce.js";
|
|
17
17
|
import { createObservabilityTools } from "./tools-observability.js";
|
|
18
18
|
import { createContextTools } from "./tools-context.js";
|
|
19
|
+
import { createArtifactTools } from "./tools-artifacts.js";
|
|
19
20
|
// Parse args
|
|
20
21
|
const args = process.argv.slice(2);
|
|
21
22
|
let projectDir = process.cwd();
|
|
@@ -38,6 +39,7 @@ const tools = [
|
|
|
38
39
|
...createStateTools(projectDir),
|
|
39
40
|
...createCompileTools(projectDir),
|
|
40
41
|
...createContextTools(projectDir),
|
|
42
|
+
...createArtifactTools(projectDir),
|
|
41
43
|
...createEnforceTools(projectDir),
|
|
42
44
|
...createObservabilityTools(projectDir),
|
|
43
45
|
];
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { errorResult, textResult } from "./server.js";
|
|
5
|
+
function sha256(value) {
|
|
6
|
+
return createHash("sha256").update(value).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
function isContained(root, target) {
|
|
9
|
+
return target === root || target.startsWith(`${root}${sep}`);
|
|
10
|
+
}
|
|
11
|
+
function canonicalJson(value) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
const record = value;
|
|
16
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(value) ?? "null";
|
|
19
|
+
}
|
|
20
|
+
function decodeJsonPointer(pointer) {
|
|
21
|
+
if (!pointer.startsWith("/") || /~(?:[^01]|$)/.test(pointer)) {
|
|
22
|
+
throw new Error("exclude_json_pointers must contain valid non-root JSON pointers");
|
|
23
|
+
}
|
|
24
|
+
return pointer.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
25
|
+
}
|
|
26
|
+
function omitJsonPointer(value, pointer) {
|
|
27
|
+
const segments = decodeJsonPointer(pointer);
|
|
28
|
+
let cursor = value;
|
|
29
|
+
for (const segment of segments.slice(0, -1)) {
|
|
30
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor) || !Object.hasOwn(cursor, segment)) {
|
|
31
|
+
throw new Error(`JSON pointer does not identify an object member: ${pointer}`);
|
|
32
|
+
}
|
|
33
|
+
cursor = cursor[segment];
|
|
34
|
+
}
|
|
35
|
+
const leaf = segments.at(-1);
|
|
36
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor) || !Object.hasOwn(cursor, leaf)) {
|
|
37
|
+
throw new Error(`JSON pointer does not identify an object member: ${pointer}`);
|
|
38
|
+
}
|
|
39
|
+
delete cursor[leaf];
|
|
40
|
+
}
|
|
41
|
+
async function resolveArtifact(projectDir, requestedPath) {
|
|
42
|
+
if (!requestedPath || requestedPath.includes("\0") || isAbsolute(requestedPath)) {
|
|
43
|
+
throw new Error("path must be a non-empty project-relative path");
|
|
44
|
+
}
|
|
45
|
+
const root = await realpath(projectDir);
|
|
46
|
+
const lexicalTarget = resolve(root, requestedPath);
|
|
47
|
+
if (!isContained(root, lexicalTarget))
|
|
48
|
+
throw new Error("path escapes project directory");
|
|
49
|
+
let absolutePath;
|
|
50
|
+
try {
|
|
51
|
+
absolutePath = await realpath(lexicalTarget);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error("artifact is not a readable regular file");
|
|
55
|
+
}
|
|
56
|
+
if (!isContained(root, absolutePath))
|
|
57
|
+
throw new Error("path resolves outside project directory");
|
|
58
|
+
const artifactStat = await stat(absolutePath);
|
|
59
|
+
if (!artifactStat.isFile())
|
|
60
|
+
throw new Error("artifact is not a readable regular file");
|
|
61
|
+
return {
|
|
62
|
+
absolutePath,
|
|
63
|
+
projectRelativePath: relative(root, lexicalTarget).split(sep).join("/"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function createArtifactTools(projectDir) {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
name: "dna_artifact_digest",
|
|
70
|
+
description: "Read-only SHA-256 digest for one project-relative regular file. Supports raw bytes, whitespace-normalized text, and canonical JSON with explicit object-member exclusions; refuses project escapes and writes nothing.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
path: { type: "string", description: "Project-relative path to a regular file" },
|
|
75
|
+
mode: {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["bytes", "whitespace_normalized_text", "canonical_json"],
|
|
78
|
+
description: "Digest representation (default: bytes)",
|
|
79
|
+
},
|
|
80
|
+
exclude_json_pointers: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: { type: "string" },
|
|
83
|
+
description: "For canonical_json only, object members to omit using RFC 6901 JSON pointers",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["path"],
|
|
87
|
+
},
|
|
88
|
+
handler: async (args) => {
|
|
89
|
+
try {
|
|
90
|
+
const requestedPath = typeof args.path === "string" ? args.path : "";
|
|
91
|
+
const mode = (args.mode ?? "bytes");
|
|
92
|
+
if (!["bytes", "whitespace_normalized_text", "canonical_json"].includes(mode)) {
|
|
93
|
+
return errorResult("mode must be bytes, whitespace_normalized_text, or canonical_json");
|
|
94
|
+
}
|
|
95
|
+
const excluded = args.exclude_json_pointers === undefined
|
|
96
|
+
? []
|
|
97
|
+
: Array.isArray(args.exclude_json_pointers) && args.exclude_json_pointers.every((item) => typeof item === "string")
|
|
98
|
+
? args.exclude_json_pointers
|
|
99
|
+
: undefined;
|
|
100
|
+
if (!excluded)
|
|
101
|
+
return errorResult("exclude_json_pointers must be an array of strings");
|
|
102
|
+
if (mode !== "canonical_json" && excluded.length > 0) {
|
|
103
|
+
return errorResult("exclude_json_pointers is only valid with canonical_json mode");
|
|
104
|
+
}
|
|
105
|
+
const artifact = await resolveArtifact(projectDir, requestedPath);
|
|
106
|
+
const content = await readFile(artifact.absolutePath);
|
|
107
|
+
const artifactStat = await stat(artifact.absolutePath);
|
|
108
|
+
const contentSha256 = sha256(content);
|
|
109
|
+
let digest = contentSha256;
|
|
110
|
+
if (mode === "whitespace_normalized_text") {
|
|
111
|
+
digest = sha256(content.toString("utf-8").replace(/\s+/g, " ").trim());
|
|
112
|
+
}
|
|
113
|
+
else if (mode === "canonical_json") {
|
|
114
|
+
const parsed = JSON.parse(content.toString("utf-8"));
|
|
115
|
+
for (const pointer of excluded)
|
|
116
|
+
omitJsonPointer(parsed, pointer);
|
|
117
|
+
digest = sha256(canonicalJson(parsed));
|
|
118
|
+
}
|
|
119
|
+
return textResult(JSON.stringify({
|
|
120
|
+
schema_version: "intentdna.artifact_digest.v1",
|
|
121
|
+
path: artifact.projectRelativePath,
|
|
122
|
+
mode,
|
|
123
|
+
sha256: digest,
|
|
124
|
+
content_sha256: contentSha256,
|
|
125
|
+
size_bytes: content.byteLength,
|
|
126
|
+
modified_at: artifactStat.mtime.toISOString(),
|
|
127
|
+
excluded_json_pointers: excluded,
|
|
128
|
+
read_only: true,
|
|
129
|
+
}, null, 2));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return errorResult(error instanceof Error ? error.message : "artifact digest failed");
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
@@ -46,6 +46,7 @@ export function recommendHookTimeouts(ir, baseTimeout = 10) {
|
|
|
46
46
|
spec.checkpoint?.assert === "no_test_regression")) ?? false;
|
|
47
47
|
if (hasStopCommandVerifiers) {
|
|
48
48
|
timeouts.stop = Math.max(baseTimeout, 45);
|
|
49
|
+
timeouts.subagentStop = Math.max(baseTimeout, 45);
|
|
49
50
|
}
|
|
50
51
|
return timeouts;
|
|
51
52
|
}
|
|
@@ -70,7 +71,7 @@ export function detectEnabledEvents(ir) {
|
|
|
70
71
|
if (ir.prompt_directives.some(d => d.priority === "high")) {
|
|
71
72
|
events.push("userPromptSubmit");
|
|
72
73
|
}
|
|
73
|
-
if (hasRoleScope) {
|
|
74
|
+
if (hasRoleScope || hasStopVerifiers) {
|
|
74
75
|
events.push("subagentStop");
|
|
75
76
|
}
|
|
76
77
|
if (ir.prompt_directives.length > 0) {
|
|
@@ -448,10 +448,19 @@ function workflowSkillMapEntry(skillName, dirName, plan) {
|
|
|
448
448
|
produces,
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
|
+
function inferredWorkflowAsset(ir) {
|
|
452
|
+
const sourceIds = [...new Set(ir?.source_dna_ids ?? [])]
|
|
453
|
+
.filter((sourceId) => sourceId.length > 0)
|
|
454
|
+
.filter((sourceId) => !sourceId.startsWith("species:"))
|
|
455
|
+
.filter((sourceId) => !sourceId.startsWith("enterprise:"));
|
|
456
|
+
return sourceIds.length === 1 ? sourceIds[0] : undefined;
|
|
457
|
+
}
|
|
451
458
|
export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
452
459
|
const lines = [];
|
|
453
460
|
const skillName = `dna-${toKebabCase(plan.name)}`;
|
|
454
461
|
const surface = options?.surface ?? "claude_code";
|
|
462
|
+
const workflowId = plan.workflow_key ?? plan.source_workflow;
|
|
463
|
+
const workflowAsset = inferredWorkflowAsset(ir);
|
|
455
464
|
assertSafeGeneratedName(skillName, "skill name");
|
|
456
465
|
// Frontmatter
|
|
457
466
|
lines.push("---");
|
|
@@ -478,9 +487,20 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
478
487
|
lines.push("");
|
|
479
488
|
}
|
|
480
489
|
lines.push("<Workflow_State>");
|
|
481
|
-
lines.push("At workflow start,
|
|
490
|
+
lines.push("At workflow start, call the MCP tool `dna_workflow_write` before any step or hook-triggering action so hook verifier evidence is attributed to this workflow instead of `*:direct`.");
|
|
491
|
+
lines.push(`Use workflow id \`${workflowId}\`.`);
|
|
492
|
+
if (workflowAsset) {
|
|
493
|
+
lines.push(`Use workflow_asset \`${workflowAsset}\`; it is the single non-species/non-enterprise IR source id.`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
lines.push("Do not set workflow_asset: IR does not provide exactly one non-species/non-enterprise source id, so the asset cannot be inferred safely.");
|
|
497
|
+
}
|
|
498
|
+
const firstStep = plan.steps[0];
|
|
499
|
+
lines.push(`Initial state write: workflow=${workflowId}, current_step=${firstStep.id}, current_role=${firstStep.role}, iteration=1, active=true.`);
|
|
482
500
|
lines.push("Record the invocation argument as inputs.ARGUMENTS so handoff artifact paths like $ARGUMENTS can be resolved deterministically.");
|
|
483
501
|
lines.push("If template variables are shown below, persist their resolved values as resolved_variables.");
|
|
502
|
+
lines.push("Before each workflow step, call `dna_workflow_write` again with workflow, workflow_asset only when safely inferred above, current_step, current_role, iteration, active=true, inputs.ARGUMENTS, and resolved_variables.");
|
|
503
|
+
lines.push("At workflow completion or terminal failure, call `dna_workflow_write` with active=false using the same workflow id and safe workflow_asset value.");
|
|
484
504
|
lines.push("</Workflow_State>");
|
|
485
505
|
lines.push("");
|
|
486
506
|
// Required Context — context files that must be read before any work
|
|
@@ -607,6 +627,7 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
607
627
|
const agentPrompt = escapePrompt(promptParts.join(" "));
|
|
608
628
|
lines.push(`${stepNumber}. **${step.id}**${optional}`);
|
|
609
629
|
lines.push(` ${step.description}${runIf}`);
|
|
630
|
+
lines.push(` Before this step, call \`dna_workflow_write\` with workflow=${workflowId}, current_step=${step.id}, current_role=${step.role}, iteration=<current iteration>, active=true, inputs.ARGUMENTS, resolved_variables, and ${workflowAsset ? `workflow_asset=${workflowAsset}` : "no workflow_asset unless one has been explicitly supplied by trusted workflow state"}.`);
|
|
610
631
|
lines.push("");
|
|
611
632
|
if (humanOwned) {
|
|
612
633
|
lines.push(" **Human-owned gate (`model: human`)**");
|
|
@@ -640,6 +661,11 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
640
661
|
const artifacts = step.handoff.produces.map(p => p.description).join(", ");
|
|
641
662
|
lines.push(` Verify produced artifacts: ${artifacts}`);
|
|
642
663
|
}
|
|
664
|
+
pushWorkflowStepVerifierLines(lines, step, " ");
|
|
665
|
+
if (step.completion?.length || step.checkpoints?.length) {
|
|
666
|
+
lines.push(" If any completion/checkpoint gate fails, preserve the exact verifier diagnostics verbatim: id, check/assert or command, message, evidence, target/artifact, and exit_code when available.");
|
|
667
|
+
lines.push(` Feed those exact diagnostics to ${step.on_fail === "retry_with_feedback" ? "this step's on_fail retry" : "the retry_from/on_fail step"} before any retry, reanalysis, or downstream transition.`);
|
|
668
|
+
}
|
|
643
669
|
lines.push("");
|
|
644
670
|
// Reflection gate: inject Reflection_Gate after steps with max_attempts
|
|
645
671
|
if (step.max_attempts) {
|
|
@@ -648,8 +674,9 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
648
674
|
const blockedPath = step.blocked_items_path ?? "blocked_items.md";
|
|
649
675
|
lines.push(`<Reflection_Gate>`);
|
|
650
676
|
lines.push(`max_attempts=${step.max_attempts}, handoff_to=${handoffStep}, max_handoffs=${maxH}`);
|
|
651
|
-
lines.push(`No
|
|
652
|
-
lines.push(`
|
|
677
|
+
lines.push(`No declared completion progress after ${step.max_attempts} attempts → handoff to ${handoffStep} for re-analysis.`);
|
|
678
|
+
lines.push(`When the declared gates are test or no_test_regression gates, preserve the existing test-progress meaning as part of declared completion progress.`);
|
|
679
|
+
lines.push(`After ${maxH} handoffs with no declared completion progress → SKIP and record to ${blockedPath}.`);
|
|
653
680
|
lines.push(`</Reflection_Gate>`);
|
|
654
681
|
lines.push("");
|
|
655
682
|
}
|
|
@@ -662,7 +689,9 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
662
689
|
lines.push(`- max_retries: ${plan.retry.max_retries}`);
|
|
663
690
|
lines.push(`- retry_from: ${plan.retry.retry_from ?? "first_failed_step"}`);
|
|
664
691
|
lines.push(`- backoff: ${plan.retry.backoff}`);
|
|
665
|
-
lines.push("- On a blocking checkpoint or validator failure, preserve the exact diagnostics and return them to the retry_from step.");
|
|
692
|
+
lines.push("- On a blocking completion, checkpoint, or validator failure, preserve the exact diagnostics and return them verbatim to the retry_from/on_fail step.");
|
|
693
|
+
lines.push("- Exact diagnostics means every available id/result_id, check/assert or command, message, evidence, target/artifact, and exit_code. Do not paraphrase away failed paths, command output, or policy-denied exit codes.");
|
|
694
|
+
lines.push("- Retry prompts must include the prior failed step id, the declared gates that failed, and the exact verifier diagnostics before asking for another attempt.");
|
|
666
695
|
lines.push("- Repeat only the retry slice and its dependent steps; do not continue to save, adoption, or sync while validation is red.");
|
|
667
696
|
lines.push("- Stop and report the remaining diagnostics when the retry budget is exhausted.");
|
|
668
697
|
lines.push("</Retry_Policy>");
|
|
@@ -797,6 +826,7 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
|
797
826
|
}
|
|
798
827
|
// Workflow Boundary — prevent cross-workflow execution
|
|
799
828
|
lines.push("<Workflow_Boundary>");
|
|
829
|
+
lines.push(`Before reporting final success, terminal failure, or exhaustion, call \`dna_workflow_write\` with workflow=${workflowId}, current_step=workflow_complete, current_role=workflow, active=false, and ${workflowAsset ? `workflow_asset=${workflowAsset}` : "no workflow_asset unless one has been explicitly supplied by trusted workflow state"}.`);
|
|
800
830
|
lines.push("This workflow is COMPLETE. Do NOT proceed to any other workflow.");
|
|
801
831
|
lines.push("Report your results and STOP. The user will decide the next step.");
|
|
802
832
|
lines.push("</Workflow_Boundary>");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CompletionCheck, ConstraintIR, StepCheckpoint, VerifierCommandPolicy } from "../schema/types.js";
|
|
2
2
|
export declare const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30000;
|
|
3
3
|
export declare const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
|
|
4
|
+
export declare const VERIFIER_DIAGNOSTIC_PREFIX = "INTENTDNA_DIAGNOSTIC:";
|
|
5
|
+
export declare const MAX_VERIFIER_DIAGNOSTIC_BYTES = 2048;
|
|
4
6
|
export interface VerifierRuntimeContext {
|
|
5
7
|
projectDir: string;
|
|
6
8
|
variables?: Record<string, string>;
|
|
@@ -29,7 +31,7 @@ export declare function hasUnresolvedVerifierTemplate(value: string): boolean;
|
|
|
29
31
|
export declare function unresolvedVerifierTemplateMessage(value: string): string;
|
|
30
32
|
export declare function hasUnsafeShellControl(command: string): boolean;
|
|
31
33
|
export declare function isVerifierCommandAllowed(policy: VerifierCommandPolicy | undefined, command: string): boolean;
|
|
32
|
-
export declare function verifierCommandPolicyMessage(
|
|
34
|
+
export declare function verifierCommandPolicyMessage(_command: string): string;
|
|
33
35
|
export declare function isBuiltinAssertAllowed(policy: VerifierCommandPolicy | undefined, assertName: string): boolean;
|
|
34
36
|
export declare function verifierAssertPolicyMessage(assertName: string): string;
|
|
35
37
|
export declare function trimVerifierEvidence(raw: string | undefined): string | undefined;
|
package/dist/runtime/verifier.js
CHANGED
|
@@ -3,6 +3,9 @@ import { readFile, realpath, stat } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
4
|
export const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30_000;
|
|
5
5
|
export const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
|
|
6
|
+
export const VERIFIER_DIAGNOSTIC_PREFIX = "INTENTDNA_DIAGNOSTIC:";
|
|
7
|
+
export const MAX_VERIFIER_DIAGNOSTIC_BYTES = 2048;
|
|
8
|
+
const VERIFIER_DIAGNOSTIC_CODE = /^INTENTDNA_DIAGNOSTIC:[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
6
9
|
const SAFE_COMMAND_VARIABLE_RE = /^[A-Za-z0-9_-]+$/;
|
|
7
10
|
const SAFE_PATH_VARIABLE_RE = /^[-A-Za-z0-9_./]+$/;
|
|
8
11
|
export function resolveVerifierTemplate(template, variables) {
|
|
@@ -59,7 +62,7 @@ function resolveVerifierCommand(rawCommand, policy, variables) {
|
|
|
59
62
|
return { target: declaredCommand, evidence: "policy_denied", exit_code: 126, message: verifierCommandPolicyMessage(declaredCommand) };
|
|
60
63
|
}
|
|
61
64
|
const safeVariables = validateVerifierTemplateVariables(declaredCommand, variables, "command");
|
|
62
|
-
if (
|
|
65
|
+
if (safeVariables.valid === false) {
|
|
63
66
|
return { target: declaredCommand, evidence: safeVariables.evidence, exit_code: 126, message: safeVariables.message };
|
|
64
67
|
}
|
|
65
68
|
const command = resolveVerifierTemplate(declaredCommand, variables).trim();
|
|
@@ -137,8 +140,8 @@ export function isVerifierCommandAllowed(policy, command) {
|
|
|
137
140
|
return false;
|
|
138
141
|
return policy.allow_command_prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
|
|
139
142
|
}
|
|
140
|
-
export function verifierCommandPolicyMessage(
|
|
141
|
-
return
|
|
143
|
+
export function verifierCommandPolicyMessage(_command) {
|
|
144
|
+
return "Verifier command not allowed by verifier_policy";
|
|
142
145
|
}
|
|
143
146
|
export function isBuiltinAssertAllowed(policy, assertName) {
|
|
144
147
|
return policy?.allow_builtin_asserts?.includes(assertName) ?? false;
|
|
@@ -156,12 +159,52 @@ export function trimVerifierEvidence(raw) {
|
|
|
156
159
|
? trimmed.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
|
|
157
160
|
: trimmed;
|
|
158
161
|
}
|
|
162
|
+
function sanitizeVerifierDiagnosticLine(raw) {
|
|
163
|
+
return raw
|
|
164
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000D\u000E-\u001F\u007F]/g, "")
|
|
165
|
+
.trim();
|
|
166
|
+
}
|
|
167
|
+
function trimDiagnosticBytes(raw) {
|
|
168
|
+
const marker = "...[truncated]";
|
|
169
|
+
if (Buffer.byteLength(raw, "utf8") <= MAX_VERIFIER_DIAGNOSTIC_BYTES)
|
|
170
|
+
return raw;
|
|
171
|
+
let trimmed = raw;
|
|
172
|
+
while (trimmed.length > 0 && Buffer.byteLength(`${trimmed}${marker}`, "utf8") > MAX_VERIFIER_DIAGNOSTIC_BYTES) {
|
|
173
|
+
trimmed = trimmed.slice(0, -1);
|
|
174
|
+
}
|
|
175
|
+
return `${trimmed}${marker}`;
|
|
176
|
+
}
|
|
177
|
+
function extractVerifierDiagnostics(raw) {
|
|
178
|
+
return raw
|
|
179
|
+
.split(/\r?\n/)
|
|
180
|
+
.map((line) => sanitizeVerifierDiagnosticLine(line))
|
|
181
|
+
.filter((line) => VERIFIER_DIAGNOSTIC_CODE.test(line));
|
|
182
|
+
}
|
|
183
|
+
function collectVerifierDiagnostics(pendingLine, chunk, diagnostics) {
|
|
184
|
+
const lines = `${pendingLine}${chunk}`.split("\n");
|
|
185
|
+
const nextPendingLine = lines.pop() ?? "";
|
|
186
|
+
diagnostics.push(...extractVerifierDiagnostics(lines.join("\n")));
|
|
187
|
+
return nextPendingLine.length > MAX_VERIFIER_EVIDENCE_BYTES
|
|
188
|
+
? nextPendingLine.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
|
|
189
|
+
: nextPendingLine;
|
|
190
|
+
}
|
|
191
|
+
function summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, diagnostics = []) {
|
|
192
|
+
const summary = stdoutBytes === 0 && stderrBytes === 0
|
|
193
|
+
? undefined
|
|
194
|
+
: `stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`;
|
|
195
|
+
const diagnostic = diagnostics.length > 0
|
|
196
|
+
? trimDiagnosticBytes(diagnostics.join("\n"))
|
|
197
|
+
: undefined;
|
|
198
|
+
return trimVerifierEvidence([summary, diagnostic].filter(Boolean).join("\n"));
|
|
199
|
+
}
|
|
159
200
|
export function summarizeCommandEvidence(stdout, stderr) {
|
|
160
201
|
const stdoutBytes = Buffer.byteLength(stdout, "utf8");
|
|
161
202
|
const stderrBytes = Buffer.byteLength(stderr, "utf8");
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
203
|
+
return summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, extractVerifierDiagnostics(`${stdout}\n${stderr}`));
|
|
204
|
+
}
|
|
205
|
+
function appendFailureDiagnostic(message, evidence) {
|
|
206
|
+
const diagnostics = extractVerifierDiagnostics(evidence ?? "").join("\n");
|
|
207
|
+
return diagnostics ? `${message}\n${diagnostics}` : message;
|
|
165
208
|
}
|
|
166
209
|
export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS, env) {
|
|
167
210
|
return new Promise((resolvePromise) => {
|
|
@@ -170,9 +213,12 @@ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAU
|
|
|
170
213
|
stdio: ["ignore", "pipe", "pipe"],
|
|
171
214
|
env: { ...process.env, ...(env ?? {}) },
|
|
172
215
|
});
|
|
173
|
-
let
|
|
174
|
-
let
|
|
216
|
+
let stdoutPendingLine = "";
|
|
217
|
+
let stderrPendingLine = "";
|
|
218
|
+
let stdoutBytes = 0;
|
|
219
|
+
let stderrBytes = 0;
|
|
175
220
|
let settled = false;
|
|
221
|
+
const diagnostics = [];
|
|
176
222
|
const settle = (result) => {
|
|
177
223
|
if (settled)
|
|
178
224
|
return;
|
|
@@ -183,29 +229,36 @@ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAU
|
|
|
183
229
|
const timeoutId = setTimeout(() => {
|
|
184
230
|
child.kill("SIGTERM");
|
|
185
231
|
setTimeout(() => child.kill("SIGKILL"), 1000).unref();
|
|
186
|
-
|
|
232
|
+
diagnostics.push(...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`));
|
|
233
|
+
settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, diagnostics) });
|
|
187
234
|
}, timeoutMs);
|
|
188
235
|
child.stdout.on("data", (chunk) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
236
|
+
const text = String(chunk);
|
|
237
|
+
stdoutBytes += Buffer.byteLength(text, "utf8");
|
|
238
|
+
stdoutPendingLine = collectVerifierDiagnostics(stdoutPendingLine, text, diagnostics);
|
|
192
239
|
});
|
|
193
240
|
child.stderr.on("data", (chunk) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
241
|
+
const text = String(chunk);
|
|
242
|
+
stderrBytes += Buffer.byteLength(text, "utf8");
|
|
243
|
+
stderrPendingLine = collectVerifierDiagnostics(stderrPendingLine, text, diagnostics);
|
|
197
244
|
});
|
|
198
245
|
child.on("error", () => settle({
|
|
199
246
|
passed: false,
|
|
200
247
|
timedOut: false,
|
|
201
248
|
exitCode: 1,
|
|
202
|
-
evidence:
|
|
249
|
+
evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, [
|
|
250
|
+
...diagnostics,
|
|
251
|
+
...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`),
|
|
252
|
+
]),
|
|
203
253
|
}));
|
|
204
254
|
child.on("close", (code) => settle({
|
|
205
255
|
passed: code === 0,
|
|
206
256
|
timedOut: false,
|
|
207
257
|
exitCode: code ?? 1,
|
|
208
|
-
evidence:
|
|
258
|
+
evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, [
|
|
259
|
+
...diagnostics,
|
|
260
|
+
...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`),
|
|
261
|
+
]),
|
|
209
262
|
}));
|
|
210
263
|
});
|
|
211
264
|
}
|
|
@@ -296,7 +349,7 @@ export async function runCompletionVerifier(completion, ir, context) {
|
|
|
296
349
|
}
|
|
297
350
|
if (completion.file_exists) {
|
|
298
351
|
const safeVariables = validateVerifierTemplateVariables(completion.file_exists, context.variables, "path");
|
|
299
|
-
if (
|
|
352
|
+
if (safeVariables.valid === false)
|
|
300
353
|
return { passed: false, target: completion.file_exists, evidence: safeVariables.evidence, message: safeVariables.message };
|
|
301
354
|
const target = resolveVerifierTemplate(completion.file_exists, context.variables);
|
|
302
355
|
if (hasUnresolvedVerifierTemplate(target))
|
|
@@ -316,7 +369,7 @@ export async function runCompletionVerifier(completion, ir, context) {
|
|
|
316
369
|
}
|
|
317
370
|
if (completion.file_not_empty) {
|
|
318
371
|
const safeVariables = validateVerifierTemplateVariables(completion.file_not_empty, context.variables, "path");
|
|
319
|
-
if (
|
|
372
|
+
if (safeVariables.valid === false)
|
|
320
373
|
return { passed: false, target: completion.file_not_empty, evidence: safeVariables.evidence, message: safeVariables.message };
|
|
321
374
|
const target = resolveVerifierTemplate(completion.file_not_empty, context.variables);
|
|
322
375
|
if (hasUnresolvedVerifierTemplate(target))
|
|
@@ -338,10 +391,10 @@ export async function runCompletionVerifier(completion, ir, context) {
|
|
|
338
391
|
}
|
|
339
392
|
if (completion.file_contains) {
|
|
340
393
|
const safeTargetVariables = validateVerifierTemplateVariables(completion.file_contains.path, context.variables, "path");
|
|
341
|
-
if (
|
|
394
|
+
if (safeTargetVariables.valid === false)
|
|
342
395
|
return { passed: false, target: completion.file_contains.path, evidence: safeTargetVariables.evidence, message: safeTargetVariables.message };
|
|
343
396
|
const safePatternVariables = validateVerifierTemplateVariables(completion.file_contains.pattern, context.variables);
|
|
344
|
-
if (
|
|
397
|
+
if (safePatternVariables.valid === false)
|
|
345
398
|
return { passed: false, target: completion.file_contains.pattern, evidence: safePatternVariables.evidence, message: safePatternVariables.message };
|
|
346
399
|
const target = resolveVerifierTemplate(completion.file_contains.path, context.variables);
|
|
347
400
|
const pattern = resolveVerifierTemplate(completion.file_contains.pattern, context.variables);
|
|
@@ -379,8 +432,8 @@ export async function runCompletionVerifier(completion, ir, context) {
|
|
|
379
432
|
message: commandResult.passed
|
|
380
433
|
? undefined
|
|
381
434
|
: commandResult.timedOut
|
|
382
|
-
? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms
|
|
383
|
-
:
|
|
435
|
+
? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
|
|
436
|
+
: appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
|
|
384
437
|
};
|
|
385
438
|
}
|
|
386
439
|
export async function runCheckpointVerifier(checkpoint, ir, context) {
|
|
@@ -405,8 +458,8 @@ export async function runCheckpointVerifier(checkpoint, ir, context) {
|
|
|
405
458
|
message: commandResult.passed
|
|
406
459
|
? checkpoint.message
|
|
407
460
|
: commandResult.timedOut
|
|
408
|
-
? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms
|
|
409
|
-
:
|
|
461
|
+
? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
|
|
462
|
+
: appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
|
|
410
463
|
};
|
|
411
464
|
}
|
|
412
465
|
if (checkpoint.assert === "clean_working_tree") {
|
|
@@ -454,8 +507,10 @@ export async function runCheckpointVerifier(checkpoint, ir, context) {
|
|
|
454
507
|
message: commandResult.passed
|
|
455
508
|
? checkpoint.message
|
|
456
509
|
: commandResult.timedOut
|
|
457
|
-
? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms
|
|
458
|
-
: checkpoint.message
|
|
510
|
+
? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
|
|
511
|
+
: checkpoint.message
|
|
512
|
+
? appendFailureDiagnostic(checkpoint.message, commandResult.evidence)
|
|
513
|
+
: appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
|
|
459
514
|
};
|
|
460
515
|
}
|
|
461
516
|
return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
|
|
@@ -166,7 +166,8 @@ context_files:
|
|
|
166
166
|
|
|
167
167
|
verifier_policy:
|
|
168
168
|
allow_commands:
|
|
169
|
-
- "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{
|
|
169
|
+
- "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const legacySame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const modern=s=>s&&typeof s.sidecar_semantic_sha256==='string'&&typeof s.diagnosis_semantic_sha256==='string';const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');if(fs.existsSync(inside(reviewPath))){const prev=readJson(reviewPath);if(prev.verdict==='REQUEST_REANALYSIS'){const ps=prev.contract_snapshot||{};const unchanged=modern(ps)?ps.sidecar_semantic_sha256===sidecarSemanticHash&&ps.diagnosis_semantic_sha256===diagnosisSemanticHash:legacySame(ps,snap);if(unchanged)fail('reanalysis_unchanged');const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(cm<=rm||dm<=rm)fail('reanalysis_not_rewritten')}}\""
|
|
170
|
+
- "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.sidecar_semantic_sha256===b.sidecar_semantic_sha256&&a.diagnosis_semantic_sha256===b.diagnosis_semantic_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
|
|
170
171
|
allow_builtin_asserts:
|
|
171
172
|
- clean_working_tree
|
|
172
173
|
|
|
@@ -248,7 +249,7 @@ roles:
|
|
|
248
249
|
analyzer:
|
|
249
250
|
description: "Reads code and classifies failing tests. Writes diagnosis spec only."
|
|
250
251
|
tool_permissions:
|
|
251
|
-
allow: [Read, Grep, Glob, Write]
|
|
252
|
+
allow: [Read, Grep, Glob, Write, mcp__intentdna__dna_artifact_digest]
|
|
252
253
|
deny: [Bash, Edit, NotebookEdit]
|
|
253
254
|
scope:
|
|
254
255
|
read: ["**/*"]
|
|
@@ -263,13 +264,14 @@ roles:
|
|
|
263
264
|
- "The sidecar allowed_kinds array must be exactly BUG, UNIMPLEMENTED, INFRA, REMOVED, TEST_BUG in that order; PLACEHOLDER_CONTRACT is never a kind"
|
|
264
265
|
- "A placeholder-contract symptom may appear only in observed_failures.symptom and must map to concrete TEST_BUG and/or UNIMPLEMENTED work_items with evidence"
|
|
265
266
|
- "If reanalyzing after REQUEST_REANALYSIS, update every reviewer sections_to_fix item and report sections_updated"
|
|
267
|
+
- "Use mcp__intentdna__dna_artifact_digest for every exact contract SHA-256; never guess a digest or use Bash to compute one"
|
|
266
268
|
- "DO NOT include fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
|
|
267
269
|
- "DO NOT run tests. DO NOT edit app code. Analysis only."
|
|
268
270
|
|
|
269
271
|
analysis_reviewer:
|
|
270
272
|
description: "Reviews analyzer output quality and writes durable diagnosis review verdict."
|
|
271
273
|
tool_permissions:
|
|
272
|
-
allow: [Read, Grep, Glob, Write]
|
|
274
|
+
allow: [Read, Grep, Glob, Write, mcp__intentdna__dna_artifact_digest]
|
|
273
275
|
deny: [Bash, Edit, NotebookEdit]
|
|
274
276
|
scope:
|
|
275
277
|
read: ["**/*"]
|
|
@@ -278,6 +280,7 @@ roles:
|
|
|
278
280
|
- "REQUIRED FIRST: Read all context files listed in SKILL.md"
|
|
279
281
|
- "Verify analyzer spec: v1 references exist? Classifications sound?"
|
|
280
282
|
- "Independently validate .dna/specs/diagnosis-$ARGUMENTS.contract.json against the template sidecar contract; do not use Bash"
|
|
283
|
+
- "Use mcp__intentdna__dna_artifact_digest to independently verify exact raw, normalized-text, and canonical-JSON SHA-256 values"
|
|
281
284
|
- "Check missing: any failing test not covered?"
|
|
282
285
|
- "Verify diagnosis remains evidence-only: no fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
|
|
283
286
|
- "Output a structured verdict block with contract_version, contract_valid, failure_reason, contract_artifact_reviewed, contract_snapshot, verdict, artifact_reviewed, sections_to_fix, evidence_paths, confidence, summary, and updated_at"
|
|
@@ -486,16 +489,20 @@ workflows:
|
|
|
486
489
|
- observed_failures: one entry per failing/skipped/hung test with id, test_name, symptom, evidence_paths, and no kind field
|
|
487
490
|
- work_items: one or more entries mapping observed_failure_ids to kind, evidence_paths, v1_evidence_paths, and v2_evidence_paths
|
|
488
491
|
- summary_counts: exact count of work_items by BUG / UNIMPLEMENTED / INFRA / REMOVED / TEST_BUG
|
|
489
|
-
- contract_snapshot: baseline_source_sha256, diagnosis_artifact_sha256, observed_failure_count, work_item_count, allowed_kinds
|
|
490
|
-
- staleness:
|
|
492
|
+
- contract_snapshot: baseline_source_sha256, diagnosis_artifact_sha256, sidecar_semantic_sha256, diagnosis_semantic_sha256, observed_failure_count, work_item_count, allowed_kinds
|
|
493
|
+
- staleness: behavior_doc, baseline, and diagnosis_artifact entries each contain the exact project-relative path and current sha256; is_stale is false
|
|
494
|
+
- Obtain hashes with mcp__intentdna__dna_artifact_digest, which is read-only and project-relative: bytes mode for behavior/baseline/diagnosis, whitespace_normalized_text for diagnosis_semantic_sha256, and canonical_json for the sidecar with exclude_json_pointers=["/contract_snapshot/sidecar_semantic_sha256", "/contract_snapshot/diagnosis_artifact_sha256", "/staleness/diagnosis_artifact"] for sidecar_semantic_sha256. Write the returned values back to the sidecar and re-run the canonical digest to confirm it is stable. Never guess hashes.
|
|
491
495
|
7. Contract rules:
|
|
492
496
|
- observed_failures must not be empty; work_items must not be empty.
|
|
493
497
|
- Every observed_failure id must be mapped by at least one work_item.
|
|
494
498
|
- Every work_item kind must be one of the five allowed kinds exactly.
|
|
495
499
|
- PLACEHOLDER_CONTRACT is forbidden as a kind. If a placeholder-contract symptom exists, keep it in observed_failures.symptom and map it to concrete TEST_BUG and/or UNIMPLEMENTED work_items with evidence.
|
|
496
500
|
- REMOVED may be diagnosed but must be marked manual/user-approved before any future fix can execute it.
|
|
497
|
-
- Mixed unknown kinds, missing evidence, count mismatches, missing/changed contract_snapshot, and stale baseline/diagnosis snapshots make the contract invalid.
|
|
501
|
+
- Mixed unknown kinds, missing evidence, count mismatches, missing/changed contract_snapshot, and stale behavior/baseline/diagnosis snapshots make the contract invalid.
|
|
498
502
|
8. If this is a reanalysis after REQUEST_REANALYSIS or DIAGNOSIS_CONTRACT_INVALID, read the reviewer verdict first, update every section listed in sections_to_fix plus the summary table and contract sidecar, and report sections_updated in your final response. Reanalysis is not successful unless both the spec artifact and contract sidecar are rewritten.
|
|
503
|
+
9. Before handing off to review, the producer-side diagnosis contract completion gate must pass. It rejects grouped observations that hide per-test failures, duplicate/bad ids, unmapped observed_failures, missing evidence, count mismatches, bad hashes, stale state, and no-material-change reanalysis after REQUEST_REANALYSIS. Reanalysis compares the canonical sidecar with its self-digest and raw diagnosis snapshot fields omitted, plus a whitespace-normalized diagnosis hash, so a real sidecar-only correction is allowed while formatting-only Markdown changes are not.
|
|
504
|
+
completion:
|
|
505
|
+
- command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const legacySame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const modern=s=>s&&typeof s.sidecar_semantic_sha256==='string'&&typeof s.diagnosis_semantic_sha256==='string';const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');if(fs.existsSync(inside(reviewPath))){const prev=readJson(reviewPath);if(prev.verdict==='REQUEST_REANALYSIS'){const ps=prev.contract_snapshot||{};const unchanged=modern(ps)?ps.sidecar_semantic_sha256===sidecarSemanticHash&&ps.diagnosis_semantic_sha256===diagnosisSemanticHash:legacySame(ps,snap);if(unchanged)fail('reanalysis_unchanged');const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(cm<=rm||dm<=rm)fail('reanalysis_not_rewritten')}}\""
|
|
499
506
|
handoff:
|
|
500
507
|
produces:
|
|
501
508
|
- type: file
|
|
@@ -527,8 +534,9 @@ workflows:
|
|
|
527
534
|
- summary_counts match work_items by kind
|
|
528
535
|
- every work_item has evidence_paths and only allowed kinds
|
|
529
536
|
- PLACEHOLDER_CONTRACT is rejected as a kind but placeholder-contract symptoms are allowed when mapped to TEST_BUG and/or UNIMPLEMENTED
|
|
530
|
-
- contract_snapshot includes diagnosis_artifact_sha256, baseline_source_sha256, observed_failure_count, work_item_count, and allowed_kinds
|
|
531
|
-
- staleness.is_stale is false; stale
|
|
537
|
+
- contract_snapshot includes diagnosis_artifact_sha256, baseline_source_sha256, sidecar_semantic_sha256, diagnosis_semantic_sha256, observed_failure_count, work_item_count, and allowed_kinds
|
|
538
|
+
- staleness.is_stale is false and behavior_doc, baseline, and diagnosis_artifact path+sha256 entries match current project files; stale source snapshots are invalid
|
|
539
|
+
- independently recompute hashes with mcp__intentdna__dna_artifact_digest using the same bytes, whitespace_normalized_text, and canonical_json modes; canonical_json must omit /contract_snapshot/sidecar_semantic_sha256, /contract_snapshot/diagnosis_artifact_sha256, and /staleness/diagnosis_artifact
|
|
532
540
|
- REMOVED work_items are reviewable diagnosis facts only and must not authorize automatic fix execution
|
|
533
541
|
|
|
534
542
|
Output exactly one structured verdict block:
|
|
@@ -541,6 +549,8 @@ workflows:
|
|
|
541
549
|
"contract_snapshot": {
|
|
542
550
|
"baseline_source_sha256": "sha256 from contract sidecar",
|
|
543
551
|
"diagnosis_artifact_sha256": "sha256 from contract sidecar",
|
|
552
|
+
"sidecar_semantic_sha256": "canonical routing sidecar sha256 from contract sidecar",
|
|
553
|
+
"diagnosis_semantic_sha256": "whitespace-normalized diagnosis sha256 from contract sidecar",
|
|
544
554
|
"observed_failure_count": 0,
|
|
545
555
|
"work_item_count": 0,
|
|
546
556
|
"allowed_kinds": ["BUG", "UNIMPLEMENTED", "INFRA", "REMOVED", "TEST_BUG"]
|
|
@@ -560,7 +570,7 @@ workflows:
|
|
|
560
570
|
APPROVE → diagnosis complete.
|
|
561
571
|
REQUEST_REANALYSIS → sections_to_fix must be passed back to analyze. If contract_valid is false, set failure_reason to DIAGNOSIS_CONTRACT_INVALID and include the exact invalid rule in sections_to_fix. docs/behavior/blocked_items.md is historical context only and must not be treated as the current run verdict.
|
|
562
572
|
completion:
|
|
563
|
-
- command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{
|
|
573
|
+
- command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.sidecar_semantic_sha256===b.sidecar_semantic_sha256&&a.diagnosis_semantic_sha256===b.diagnosis_semantic_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
|
|
564
574
|
handoff:
|
|
565
575
|
consumes:
|
|
566
576
|
- type: file
|
package/hooks/hooks.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreToolUse", "timeout": 5 }] }],
|
|
4
4
|
"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PostToolUse", "timeout": 3 }] }],
|
|
5
5
|
"UserPromptSubmit": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook UserPromptSubmit", "timeout": 5 }] }],
|
|
6
|
-
"SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout":
|
|
6
|
+
"SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 45 }] }],
|
|
7
7
|
"PreCompact": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreCompact", "timeout": 3 }] }],
|
|
8
8
|
"Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Notification", "timeout": 3 }] }],
|
|
9
9
|
"Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Stop", "timeout": 45 }] }],
|