intentdna 1.7.5 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +201 -0
- package/README.md +143 -194
- package/dist/assets/candidates.d.ts +124 -0
- package/dist/assets/candidates.js +1249 -0
- package/dist/assets/catalog.d.ts +101 -0
- package/dist/assets/catalog.js +311 -0
- package/dist/assets/evidence.d.ts +93 -0
- package/dist/assets/evidence.js +391 -0
- package/dist/assets/index.d.ts +4 -0
- package/dist/assets/index.js +4 -0
- package/dist/assets/metadata.d.ts +19 -0
- package/dist/assets/metadata.js +227 -0
- package/dist/cli/commands/assets.d.ts +65 -0
- package/dist/cli/commands/assets.js +2871 -0
- package/dist/cli/commands/auth.d.ts +322 -0
- package/dist/cli/commands/auth.js +1592 -0
- package/dist/cli/commands/cloud-auth-guidance.d.ts +2 -0
- package/dist/cli/commands/cloud-auth-guidance.js +4 -0
- package/dist/cli/commands/cloud.d.ts +1359 -0
- package/dist/cli/commands/cloud.js +8364 -0
- package/dist/cli/commands/evolve.js +18 -2
- package/dist/cli/commands/generate.js +25 -2
- package/dist/cli/commands/guard.d.ts +1 -1
- package/dist/cli/commands/guard.js +1 -1
- package/dist/cli/commands/guide.d.ts +15 -0
- package/dist/cli/commands/guide.js +1747 -0
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.js +95 -60
- package/dist/cli/commands/org.d.ts +16 -0
- package/dist/cli/commands/org.js +2393 -0
- package/dist/cli/commands/release.d.ts +59 -0
- package/dist/cli/commands/release.js +716 -0
- package/dist/cli/commands/report.d.ts +82 -0
- package/dist/cli/commands/report.js +1516 -0
- package/dist/cli/commands/run.d.ts +8 -1
- package/dist/cli/commands/run.js +129 -57
- package/dist/cli/commands/sync.d.ts +8 -1
- package/dist/cli/commands/sync.js +436 -141
- package/dist/cli/commands/templates.d.ts +64 -0
- package/dist/cli/commands/templates.js +2151 -1
- package/dist/cli/commands/workflow.d.ts +26 -0
- package/dist/cli/commands/workflow.js +3053 -0
- package/dist/cli/index.js +703 -36
- package/dist/compiler/cascade.js +4 -9
- package/dist/compiler/compile.js +1 -0
- package/dist/governance/evidence-capture-attribution.d.ts +12 -0
- package/dist/governance/evidence-capture-attribution.js +46 -0
- package/dist/governance/index.d.ts +2 -0
- package/dist/governance/index.js +1 -0
- package/dist/hooks/cli.d.ts +9 -0
- package/dist/hooks/cli.js +170 -411
- package/dist/hooks/enforce.d.ts +1 -0
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/protocol.js +1 -1
- package/dist/hooks/state.d.ts +41 -0
- package/dist/hooks/state.js +126 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp/tools-state.js +2 -0
- package/dist/organization/index.d.ts +121 -0
- package/dist/organization/index.js +413 -0
- package/dist/runtime/claude-sync-target.d.ts +49 -0
- package/dist/runtime/claude-sync-target.js +280 -0
- package/dist/runtime/codex-adapter.d.ts +254 -0
- package/dist/runtime/codex-adapter.js +1204 -0
- package/dist/runtime/codex-sync-target.d.ts +33 -0
- package/dist/runtime/codex-sync-target.js +241 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/output-artifact-registry.d.ts +1 -1
- package/dist/runtime/output-artifact-registry.js +2 -0
- package/dist/runtime/settings-adapter.d.ts +1 -1
- package/dist/runtime/settings-adapter.js +1 -1
- package/dist/runtime/skill-adapter.d.ts +5 -1
- package/dist/runtime/skill-adapter.js +164 -39
- package/dist/runtime/sync-artifact-registry.d.ts +99 -0
- package/dist/runtime/sync-artifact-registry.js +194 -0
- package/dist/runtime/sync-target-plan.d.ts +45 -0
- package/dist/runtime/sync-target-plan.js +19 -0
- package/dist/runtime/verifier.d.ts +51 -0
- package/dist/runtime/verifier.js +462 -0
- package/dist/schema/controller-registry.d.ts +8 -1
- package/dist/schema/controller-registry.js +8 -0
- package/dist/schema/types.d.ts +35 -23
- package/dist/schema/types.js +10 -1
- package/dist/schema/validate.js +22 -22
- package/dist/schema/validators/controllers.js +38 -20
- package/dist/schema/workflow-authoring-contract.d.ts +29 -0
- package/dist/schema/workflow-authoring-contract.js +109 -0
- package/dist/schema/yaml-parser.js +18 -0
- package/dist/templates/api-backend.dna.yaml +19 -0
- package/dist/templates/brainstorming-first.dna.yaml +19 -0
- package/dist/templates/code-cleanup.dna.yaml +19 -0
- package/dist/templates/code-review-pipeline.dna.yaml +18 -0
- package/dist/templates/documentation-writer.dna.yaml +19 -0
- package/dist/templates/flutter-behavior-lock.dna.yaml +19 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +19 -0
- package/dist/templates/flutter-rewrite-diagnosis-review.dna.yaml +210 -0
- package/dist/templates/flutter-rewrite-fix-review-loop.dna.yaml +260 -0
- package/dist/templates/flutter-rewrite-new.dna.yaml +506 -0
- package/dist/templates/flutter-rewrite.dna.yaml +100 -4
- package/dist/templates/frontend-quality.dna.yaml +19 -0
- package/dist/templates/full-pipeline.dna.yaml +20 -0
- package/dist/templates/marketplace.d.ts +92 -0
- package/dist/templates/marketplace.js +502 -0
- package/dist/templates/metadata.d.ts +25 -0
- package/dist/templates/metadata.js +136 -3
- package/dist/templates/mobile-dev.dna.yaml +19 -0
- package/dist/templates/multi-agent-handoff-coordination.dna.yaml +156 -0
- package/dist/templates/multi-perspective-review.dna.yaml +19 -0
- package/dist/templates/persistent-executor.dna.yaml +19 -0
- package/dist/templates/qa-loop.dna.yaml +18 -0
- package/dist/templates/release-evidence-gate.dna.yaml +131 -0
- package/dist/templates/requirements-gate.dna.yaml +19 -0
- package/dist/templates/research-orchestration.dna.yaml +19 -0
- package/dist/templates/root-cause-analysis.dna.yaml +19 -0
- package/dist/templates/safe-refactoring.dna.yaml +321 -0
- package/dist/templates/secure-dev.dna.yaml +19 -0
- package/dist/templates/subagent-parallel.dna.yaml +19 -0
- package/dist/templates/systematic-debugging.dna.yaml +19 -0
- package/dist/templates/tdd-strict.dna.yaml +19 -0
- package/dist/templates/ui-visual-regression-qa.dna.yaml +133 -0
- package/dist/templates/workflow-author.dna.yaml +302 -0
- package/dist/templates/workflow-blocked-scope-triage.dna.yaml +242 -0
- package/dist/templates/workflow-completion-audit.dna.yaml +254 -0
- package/dist/templates/workflow-cross-boundary-resource-approval.dna.yaml +249 -0
- package/dist/templates/workflow-evidence-review.dna.yaml +198 -0
- package/dist/templates/workflow-module-inventory.dna.yaml +257 -0
- package/dist/templates/workflow-runtime-smoke-review.dna.yaml +252 -0
- package/dist/templates/workflow-verification-command-routing.dna.yaml +278 -0
- package/dist/workflow/authoring-packet.d.ts +67 -0
- package/dist/workflow/authoring-packet.js +218 -0
- package/dist/workflow/draft.d.ts +159 -0
- package/dist/workflow/draft.js +800 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +3 -0
- package/dist/workflow/suggestion-review.d.ts +65 -0
- package/dist/workflow/suggestion-review.js +245 -0
- package/package.json +1 -1
- package/spec/README.md +15 -48
- package/spec/codex-adapter-contract.md +176 -0
- package/spec/sync-target-plan.md +158 -0
- package/spec/workflow-capability-ir.md +153 -0
- package/spec/agent-dispatch-fix.md +0 -116
- package/spec/control-plane-convergence-handoff-2026-04-24.md +0 -432
- package/spec/flutter-rewrite-template-optimization.md +0 -301
- package/spec/foundation-hardening.md +0 -178
- package/spec/hooks-infra-harness-hardening.md +0 -741
- package/spec/multi-config.md +0 -172
- package/spec/parallel-isolation.md +0 -268
- package/spec/template-version-namespace-fix.md +0 -653
- package/spec/workflow-pipeline.md +0 -101
|
@@ -19,26 +19,54 @@ function resolveContainedPath(baseDir, ...segments) {
|
|
|
19
19
|
}
|
|
20
20
|
return target;
|
|
21
21
|
}
|
|
22
|
+
function isHumanRole(role) {
|
|
23
|
+
return role?.model?.trim().toLowerCase() === "human";
|
|
24
|
+
}
|
|
25
|
+
function isInteractiveRole(role) {
|
|
26
|
+
return role?.model?.trim().toLowerCase() === "interactive";
|
|
27
|
+
}
|
|
22
28
|
// ── Compile Controller → Skill ─────────────────────────────
|
|
23
29
|
export function compileControllerToSkill(controllerKey, controller, variables, workflows) {
|
|
24
30
|
const kindMetadata = getControllerKindMetadata(controller.kind);
|
|
25
31
|
if (!kindMetadata?.supportsSkillGeneration) {
|
|
26
32
|
throw new Error(`Unsupported controller kind: ${controller.kind}`);
|
|
27
33
|
}
|
|
34
|
+
const diagnosisWorkflowName = controller.diagnosis_workflow;
|
|
35
|
+
const fixWorkflowName = controller.fix_workflow;
|
|
36
|
+
const diagnosisArtifactPath = controller.diagnosis_artifact_path;
|
|
37
|
+
const diagnosisReviewArtifactPath = controller.diagnosis_review_artifact_path;
|
|
38
|
+
const reviewArtifactPath = controller.review_artifact_path;
|
|
39
|
+
const analyzerRole = controller.roles.analyzer;
|
|
40
|
+
const analysisReviewerRole = controller.roles.analysis_reviewer;
|
|
41
|
+
const surgeonRole = controller.roles.surgeon;
|
|
42
|
+
const fixReviewerRole = controller.roles.fix_reviewer;
|
|
43
|
+
const testRunnerRole = controller.roles.test_runner;
|
|
44
|
+
if (!diagnosisWorkflowName ||
|
|
45
|
+
!fixWorkflowName ||
|
|
46
|
+
!diagnosisArtifactPath ||
|
|
47
|
+
!diagnosisReviewArtifactPath ||
|
|
48
|
+
!reviewArtifactPath ||
|
|
49
|
+
!analyzerRole ||
|
|
50
|
+
!analysisReviewerRole ||
|
|
51
|
+
!surgeonRole ||
|
|
52
|
+
!fixReviewerRole ||
|
|
53
|
+
!testRunnerRole) {
|
|
54
|
+
throw new Error(`Controller '${controllerKey}' is missing fix-loop skill generation fields`);
|
|
55
|
+
}
|
|
28
56
|
const skillName = `dna-${toKebabCase(controllerKey)}`;
|
|
29
57
|
assertSafeGeneratedName(skillName, "skill name");
|
|
30
|
-
const analyzerAgent = `dna-${toKebabCase(
|
|
31
|
-
const analysisReviewerAgent = `dna-${toKebabCase(
|
|
32
|
-
const surgeonAgent = `dna-${toKebabCase(
|
|
33
|
-
const fixReviewerAgent = `dna-${toKebabCase(
|
|
34
|
-
const testRunnerAgent = `dna-${toKebabCase(
|
|
35
|
-
const diagnosisWorkflow = workflows?.[
|
|
36
|
-
const fixWorkflow = workflows?.[
|
|
37
|
-
const diagnosisAnalyzeStep = requireStepByRole(diagnosisWorkflow,
|
|
38
|
-
const diagnosisReviewStep = requireStepByRole(diagnosisWorkflow,
|
|
39
|
-
const fixBugsStep = requireStepByRole(fixWorkflow,
|
|
40
|
-
const reviewChangesStep = requireStepByRole(fixWorkflow,
|
|
41
|
-
const verifyReportStep = requireStepByRole(fixWorkflow,
|
|
58
|
+
const analyzerAgent = `dna-${toKebabCase(analyzerRole)}`;
|
|
59
|
+
const analysisReviewerAgent = `dna-${toKebabCase(analysisReviewerRole)}`;
|
|
60
|
+
const surgeonAgent = `dna-${toKebabCase(surgeonRole)}`;
|
|
61
|
+
const fixReviewerAgent = `dna-${toKebabCase(fixReviewerRole)}`;
|
|
62
|
+
const testRunnerAgent = `dna-${toKebabCase(testRunnerRole)}`;
|
|
63
|
+
const diagnosisWorkflow = workflows?.[diagnosisWorkflowName];
|
|
64
|
+
const fixWorkflow = workflows?.[fixWorkflowName];
|
|
65
|
+
const diagnosisAnalyzeStep = requireStepByRole(diagnosisWorkflow, analyzerRole, diagnosisWorkflowName);
|
|
66
|
+
const diagnosisReviewStep = requireStepByRole(diagnosisWorkflow, analysisReviewerRole, diagnosisWorkflowName);
|
|
67
|
+
const fixBugsStep = requireStepByRole(fixWorkflow, surgeonRole, fixWorkflowName);
|
|
68
|
+
const reviewChangesStep = requireStepByRole(fixWorkflow, fixReviewerRole, fixWorkflowName);
|
|
69
|
+
const verifyReportStep = requireStepByRole(fixWorkflow, testRunnerRole, fixWorkflowName);
|
|
42
70
|
const stepPrompt = (step) => step.prompt || step.description || "";
|
|
43
71
|
const policy = normalizeControllerPolicy(controller);
|
|
44
72
|
const lines = [];
|
|
@@ -63,8 +91,8 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
63
91
|
lines.push("<Controller>");
|
|
64
92
|
lines.push(`kind: ${controller.kind}`);
|
|
65
93
|
lines.push(`max_rounds: ${controller.max_rounds}`);
|
|
66
|
-
lines.push(`diagnosis_workflow: ${
|
|
67
|
-
lines.push(`fix_workflow: ${
|
|
94
|
+
lines.push(`diagnosis_workflow: ${diagnosisWorkflowName}`);
|
|
95
|
+
lines.push(`fix_workflow: ${fixWorkflowName}`);
|
|
68
96
|
lines.push(`progress_artifact_path: ${controller.progress_artifact_path}`);
|
|
69
97
|
lines.push("</Controller>");
|
|
70
98
|
lines.push("");
|
|
@@ -74,16 +102,17 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
74
102
|
lines.push(" - If it contains '/', '.', '..', spaces, braces, glob characters, or path separators, stop with the configured stop report.");
|
|
75
103
|
lines.push(" - Do not read or write controller artifacts until the module argument is safe.");
|
|
76
104
|
lines.push("1. Check diagnosis artifact freshness before any fix round.");
|
|
77
|
-
lines.push(` - Diagnosis artifact: ${
|
|
78
|
-
lines.push(` - Diagnosis review artifact: ${
|
|
105
|
+
lines.push(` - Diagnosis artifact: ${diagnosisArtifactPath}`);
|
|
106
|
+
lines.push(` - Diagnosis review artifact: ${diagnosisReviewArtifactPath}`);
|
|
79
107
|
lines.push("2. Create a stable controller_run_id once for this invocation before using any artifact path.");
|
|
80
108
|
lines.push(" - Reuse the same controller_run_id in every child Agent prompt and in every progress validation.");
|
|
81
109
|
lines.push(" - Do not regenerate controller_run_id inside fix rounds.");
|
|
82
|
-
lines.push("3. Reuse diagnosis only when all diagnosis reuse criteria pass:");
|
|
110
|
+
lines.push("3. Reuse the diagnosis artifact only when all diagnosis reuse criteria pass:");
|
|
83
111
|
for (const criterion of policy.diagnosis_reuse_criteria) {
|
|
84
112
|
lines.push(` - ${criterion}`);
|
|
85
113
|
}
|
|
86
|
-
lines.push(
|
|
114
|
+
lines.push(" Reuse skips fresh analyzer and analysis_reviewer agents; it never skips the declared diagnosis contract completion gates or the persisted diagnosis review JSON approval gate before any surgeon step.");
|
|
115
|
+
lines.push(`4. If diagnosis is missing, invalid, stale, or scope-changed, run the analyzer once with fresh context:`);
|
|
87
116
|
lines.push(" a. Spawn fresh analyzer agent with controller_run_id, then wait for completion.");
|
|
88
117
|
lines.push(" ```");
|
|
89
118
|
lines.push(" Agent(");
|
|
@@ -91,6 +120,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
91
120
|
lines.push(` prompt="controller_run_id=<controller_run_id>. ${escapePrompt(stepPrompt(diagnosisAnalyzeStep))}"`);
|
|
92
121
|
lines.push(" )");
|
|
93
122
|
lines.push(" ```");
|
|
123
|
+
pushWorkflowStepVerifierLines(lines, diagnosisAnalyzeStep, " ");
|
|
94
124
|
lines.push(` b. Diagnosis artifact gate: read ${controller.diagnosis_artifact_path}; validate it exists, is non-empty, names the safe module argument, and is current for this module before continuing.`);
|
|
95
125
|
lines.push(" c. Spawn fresh analysis_reviewer agent with controller_run_id, then wait for completion.");
|
|
96
126
|
lines.push(" ```");
|
|
@@ -99,9 +129,12 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
99
129
|
lines.push(` prompt="controller_run_id=<controller_run_id>; expected_review_artifact=${controller.diagnosis_review_artifact_path}. ${escapePrompt(stepPrompt(diagnosisReviewStep))}"`);
|
|
100
130
|
lines.push(" )");
|
|
101
131
|
lines.push(" ```");
|
|
102
|
-
lines
|
|
132
|
+
pushWorkflowStepVerifierLines(lines, diagnosisReviewStep, " ");
|
|
133
|
+
lines.push("5. If the diagnosis artifact was reused, do not spawn analyzer or analysis_reviewer; rerun the declared diagnosis contract completion gates against the existing artifacts:");
|
|
134
|
+
pushWorkflowStepVerifierLines(lines, diagnosisReviewStep, " ");
|
|
135
|
+
lines.push(`6. Diagnosis review JSON gate: read ${controller.diagnosis_review_artifact_path}; require existing valid JSON with verdict APPROVE before fix rounds.`);
|
|
103
136
|
lines.push(" If the review JSON is missing, invalid, or verdict is not APPROVE, stop immediately with the configured stop report; do NOT enter fix rounds.");
|
|
104
|
-
lines.push(`
|
|
137
|
+
lines.push(`7. For round 1..${controller.max_rounds}, set current_round to the loop number and run one fresh context fix round with sequential gates:`);
|
|
105
138
|
lines.push(" a. Spawn fresh surgeon agent with controller_run_id and current_round, then wait for completion.");
|
|
106
139
|
lines.push(" ```");
|
|
107
140
|
lines.push(" Agent(");
|
|
@@ -109,6 +142,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
109
142
|
lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context round for $ARGUMENTS. ${escapePrompt(stepPrompt(fixBugsStep))}"`);
|
|
110
143
|
lines.push(" )");
|
|
111
144
|
lines.push(" ```");
|
|
145
|
+
pushWorkflowStepVerifierLines(lines, fixBugsStep, " ");
|
|
112
146
|
lines.push(" b. Fix handoff gate: validate the current round handoff and fix commit, or an explicit no-commit handoff for current_round, before review.");
|
|
113
147
|
lines.push(" c. Spawn fresh fix_reviewer agent with controller_run_id, current_round, and expected review artifact path, then wait for completion.");
|
|
114
148
|
lines.push(" ```");
|
|
@@ -117,6 +151,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
117
151
|
lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>; expected_review_artifact=${controller.review_artifact_path}. Fresh context review for $ARGUMENTS. ${escapePrompt(stepPrompt(reviewChangesStep))}"`);
|
|
118
152
|
lines.push(" )");
|
|
119
153
|
lines.push(" ```");
|
|
154
|
+
pushWorkflowStepVerifierLines(lines, reviewChangesStep, " ");
|
|
120
155
|
lines.push(` d. Review JSON gate: read ${controller.review_artifact_path}; validate it exists, is valid JSON, module equals the safe module argument, round equals the provided current_round, run_id equals the provided controller_run_id, updated_at is present, and verdict is APPROVE or REQUEST_CHANGES before verification.`);
|
|
121
156
|
lines.push(" e. Spawn fresh test_runner agent only after the review gate passes; include controller_run_id and current_round, then wait for completion.");
|
|
122
157
|
lines.push(" ```");
|
|
@@ -125,6 +160,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
125
160
|
lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context verification for $ARGUMENTS. The progress JSON run_id must equal the provided controller_run_id and round must equal the provided current_round; do not use manual-run during controller execution. ${escapePrompt(stepPrompt(verifyReportStep))}"`);
|
|
126
161
|
lines.push(" )");
|
|
127
162
|
lines.push(" ```");
|
|
163
|
+
pushWorkflowStepVerifierLines(lines, verifyReportStep, " ");
|
|
128
164
|
lines.push(` f. Progress JSON read/provenance validation: read ${controller.progress_artifact_path}. The controller must read this JSON sidecar, not Markdown prose.`);
|
|
129
165
|
lines.push(" g. Verify progress JSON provenance before branching:");
|
|
130
166
|
for (const check of policy.progress_provenance_checks) {
|
|
@@ -135,7 +171,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
135
171
|
for (const branch of policy.verdict_branches) {
|
|
136
172
|
lines.push(` - ${branch}`);
|
|
137
173
|
}
|
|
138
|
-
lines.push(`
|
|
174
|
+
lines.push(`8. If max ${controller.max_rounds} rounds is reached, stop with the configured stop report.`);
|
|
139
175
|
lines.push("</Steps>");
|
|
140
176
|
lines.push("");
|
|
141
177
|
lines.push("<Verdict_Mapping>");
|
|
@@ -195,6 +231,37 @@ function requireStepByRole(workflow, role, workflowName) {
|
|
|
195
231
|
}
|
|
196
232
|
return step;
|
|
197
233
|
}
|
|
234
|
+
function pushWorkflowStepVerifierLines(lines, step, indent) {
|
|
235
|
+
if (step.completion?.length) {
|
|
236
|
+
lines.push(`${indent}Completion gates for workflow step '${step.id}' must pass in declaration order before treating the step as complete:`);
|
|
237
|
+
for (let i = 0; i < step.completion.length; i++) {
|
|
238
|
+
lines.push(`${indent} ${i + 1}. ${describeCompletionCheck(step.completion[i])}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (step.checkpoints?.length) {
|
|
242
|
+
lines.push(`${indent}Checkpoint gates for workflow step '${step.id}' must pass in declaration order before transitioning:`);
|
|
243
|
+
for (let i = 0; i < step.checkpoints.length; i++) {
|
|
244
|
+
const checkpoint = step.checkpoints[i];
|
|
245
|
+
const action = checkpoint.action ?? "block";
|
|
246
|
+
const command = checkpoint.command ? `; command: ${checkpoint.command}` : "";
|
|
247
|
+
lines.push(`${indent} ${i + 1}. ${checkpoint.assert}: ${checkpoint.message} (${action}${command})`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (step.completion?.some((check) => check.command_success) || step.checkpoints?.some((check) => check.command)) {
|
|
251
|
+
lines.push(`${indent}Command-backed completion/checkpoint gates must use runtime verifier_policy semantics: first authorize the declared raw command against exact allow_commands or allow_command_prefixes, then resolve templates safely, execute the exact resolved command, require exit code 0 for pass, treat policy denial as exit_code=126, and treat any non-zero exit code as blocking failure unless the checkpoint action is warn.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function describeCompletionCheck(check) {
|
|
255
|
+
if (check.file_exists)
|
|
256
|
+
return `file_exists: ${check.file_exists}`;
|
|
257
|
+
if (check.file_not_empty)
|
|
258
|
+
return `file_not_empty: ${check.file_not_empty}`;
|
|
259
|
+
if (check.file_contains)
|
|
260
|
+
return `file_contains: ${check.file_contains.path} matches ${check.file_contains.pattern}`;
|
|
261
|
+
if (check.command_success)
|
|
262
|
+
return `command_success: execute exact command "${check.command_success}", require exit code 0, and block on any non-zero exit code`;
|
|
263
|
+
return "invalid completion check: exactly one completion check field is required";
|
|
264
|
+
}
|
|
198
265
|
function createSkillLauncher(params) {
|
|
199
266
|
const lines = [];
|
|
200
267
|
lines.push("---");
|
|
@@ -289,9 +356,9 @@ function renderControllerPolicyLines(lines, controller) {
|
|
|
289
356
|
function renderControllerPolicyText(text, controller) {
|
|
290
357
|
const replacements = {
|
|
291
358
|
"$progress_artifact_path": controller.progress_artifact_path,
|
|
292
|
-
"$diagnosis_artifact_path": controller.diagnosis_artifact_path,
|
|
293
|
-
"$diagnosis_review_artifact_path": controller.diagnosis_review_artifact_path,
|
|
294
|
-
"$review_artifact_path": controller.review_artifact_path,
|
|
359
|
+
"$diagnosis_artifact_path": controller.diagnosis_artifact_path ?? "",
|
|
360
|
+
"$diagnosis_review_artifact_path": controller.diagnosis_review_artifact_path ?? "",
|
|
361
|
+
"$review_artifact_path": controller.review_artifact_path ?? "",
|
|
295
362
|
};
|
|
296
363
|
let rendered = text;
|
|
297
364
|
for (const [token, value] of Object.entries(replacements)) {
|
|
@@ -381,9 +448,10 @@ function workflowSkillMapEntry(skillName, dirName, plan) {
|
|
|
381
448
|
produces,
|
|
382
449
|
};
|
|
383
450
|
}
|
|
384
|
-
export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
451
|
+
export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
|
|
385
452
|
const lines = [];
|
|
386
453
|
const skillName = `dna-${toKebabCase(plan.name)}`;
|
|
454
|
+
const surface = options?.surface ?? "claude_code";
|
|
387
455
|
assertSafeGeneratedName(skillName, "skill name");
|
|
388
456
|
// Frontmatter
|
|
389
457
|
lines.push("---");
|
|
@@ -492,8 +560,10 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
492
560
|
lines.push("</Advisory_Context>");
|
|
493
561
|
lines.push("");
|
|
494
562
|
}
|
|
495
|
-
// Steps —
|
|
563
|
+
// Steps — agent-owned steps are delegated; human-owned steps stay in the main session.
|
|
496
564
|
const usedRoles = new Set(plan.steps.map((s) => s.role));
|
|
565
|
+
const hasHumanSteps = plan.steps.some((step) => isHumanRole(roles[step.role]));
|
|
566
|
+
const hasInteractiveSteps = plan.steps.some((step) => isInteractiveRole(roles[step.role]));
|
|
497
567
|
// Pre-compute handoff targets: steps that are pointed to by another step's handoff_to
|
|
498
568
|
const handoffTargets = new Set();
|
|
499
569
|
for (const step of plan.steps) {
|
|
@@ -508,6 +578,8 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
508
578
|
stepNumber++;
|
|
509
579
|
const step = plan.steps.find((s) => s.id === stepId);
|
|
510
580
|
const agentType = `dna-${toKebabCase(step.role)}`;
|
|
581
|
+
const humanOwned = isHumanRole(roles[step.role]);
|
|
582
|
+
const interactiveOwned = isInteractiveRole(roles[step.role]);
|
|
511
583
|
const runIf = step.run_if ? `\n **Condition**: ${step.run_if}` : "";
|
|
512
584
|
const optional = step.optional ? " (optional)" : "";
|
|
513
585
|
// Build agent prompt: anti-recursion + role identity + task
|
|
@@ -536,14 +608,34 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
536
608
|
lines.push(`${stepNumber}. **${step.id}**${optional}`);
|
|
537
609
|
lines.push(` ${step.description}${runIf}`);
|
|
538
610
|
lines.push("");
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
611
|
+
if (humanOwned) {
|
|
612
|
+
lines.push(" **Human-owned gate (`model: human`)**");
|
|
613
|
+
lines.push(` Ask the user in the main session: ${step.prompt || step.description}`);
|
|
614
|
+
lines.push(" Do not delegate this decision, infer approval, or continue from silence. Continue only after an explicit human response.");
|
|
615
|
+
}
|
|
616
|
+
else if (interactiveOwned) {
|
|
617
|
+
lines.push(" **Main-session external-agent step (`model: interactive`)**");
|
|
618
|
+
lines.push(` Perform directly in the main session: ${step.prompt || step.description}`);
|
|
619
|
+
lines.push(" This step may interview the user or edit the candidate YAML inside its declared scope. Do not delegate away the conversation context.");
|
|
620
|
+
}
|
|
621
|
+
else if (surface === "codex_cli") {
|
|
622
|
+
lines.push(" Delegate to a Codex native subagent with this role contract; do not discard the role boundaries:");
|
|
623
|
+
lines.push(" ```text");
|
|
624
|
+
lines.push(` role: ${agentType}`);
|
|
625
|
+
lines.push(` prompt: ${agentPrompt}`);
|
|
626
|
+
lines.push(" ```");
|
|
627
|
+
lines.push(" Wait for the subagent to complete and read its output before proceeding.");
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
lines.push(` Execute with Agent tool — DO NOT perform this work yourself:`);
|
|
631
|
+
lines.push(` \`\`\``);
|
|
632
|
+
lines.push(` Agent(`);
|
|
633
|
+
lines.push(` subagent_type="${agentType}",`);
|
|
634
|
+
lines.push(` prompt="${agentPrompt}"`);
|
|
635
|
+
lines.push(` )`);
|
|
636
|
+
lines.push(` \`\`\``);
|
|
637
|
+
lines.push(` Wait for agent to complete. Read the output before proceeding.`);
|
|
638
|
+
}
|
|
547
639
|
if (step.handoff?.produces && step.handoff.produces.length > 0) {
|
|
548
640
|
const artifacts = step.handoff.produces.map(p => p.description).join(", ");
|
|
549
641
|
lines.push(` Verify produced artifacts: ${artifacts}`);
|
|
@@ -565,14 +657,39 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
565
657
|
}
|
|
566
658
|
lines.push("</Steps>");
|
|
567
659
|
lines.push("");
|
|
568
|
-
|
|
660
|
+
if (plan.retry.max_retries > 0) {
|
|
661
|
+
lines.push("<Retry_Policy>");
|
|
662
|
+
lines.push(`- max_retries: ${plan.retry.max_retries}`);
|
|
663
|
+
lines.push(`- retry_from: ${plan.retry.retry_from ?? "first_failed_step"}`);
|
|
664
|
+
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.");
|
|
666
|
+
lines.push("- Repeat only the retry slice and its dependent steps; do not continue to save, adoption, or sync while validation is red.");
|
|
667
|
+
lines.push("- Stop and report the remaining diagnostics when the retry budget is exhausted.");
|
|
668
|
+
lines.push("</Retry_Policy>");
|
|
669
|
+
lines.push("");
|
|
670
|
+
}
|
|
671
|
+
// Execution Policy — force delegation while preserving explicit human gates.
|
|
569
672
|
lines.push("<Execution_Policy>");
|
|
570
|
-
|
|
673
|
+
if (surface === "codex_cli") {
|
|
674
|
+
lines.push("- **CRITICAL**: Delegate each delegated-agent step to a Codex native subagent using the specified role contract. Do not collapse distinct roles into an unreviewed single pass.");
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
lines.push(hasHumanSteps || hasInteractiveSteps
|
|
678
|
+
? "- **CRITICAL**: Each delegated-agent step MUST be executed by spawning an Agent using the Agent tool with the specified subagent_type. DO NOT perform delegated-agent work directly in the main session."
|
|
679
|
+
: "- **CRITICAL**: Each step MUST be executed by spawning an Agent using the Agent tool with the specified subagent_type. DO NOT perform any step's work directly in the main session.");
|
|
680
|
+
}
|
|
571
681
|
lines.push("- Use the task tracker to track each step only as pending/in_progress/completed; use deleted only for skipped dependent tasks.");
|
|
572
|
-
lines.push(
|
|
682
|
+
lines.push(hasHumanSteps
|
|
683
|
+
? "- Proceed automatically between non-human steps, but stop at every `model: human` step and require an explicit user response before continuing."
|
|
684
|
+
: "- After completing each step, immediately proceed to the next — do not stop, summarize, or wait for confirmation.");
|
|
685
|
+
if (hasInteractiveSteps) {
|
|
686
|
+
lines.push("- Execute `model: interactive` steps in the main session so interview answers and candidate edits remain in the user-visible conversation.");
|
|
687
|
+
}
|
|
573
688
|
lines.push("- If a step cannot complete, do not use a failed status. Keep the current task in_progress or record the blocker in the task description/output, delete skipped dependent tasks, and continue only to non-dependent steps.");
|
|
574
|
-
lines.push(
|
|
575
|
-
|
|
689
|
+
lines.push(hasHumanSteps
|
|
690
|
+
? "- Never auto-approve, delegate, or synthesize a human-owned decision."
|
|
691
|
+
: "- Do not ask the user for permission between steps — the workflow is pre-approved.");
|
|
692
|
+
lines.push("- Wait for each delegated agent to complete and read its output before proceeding to the next step.");
|
|
576
693
|
lines.push("</Execution_Policy>");
|
|
577
694
|
lines.push("");
|
|
578
695
|
// Tool_Usage — only if roles have permissions or scope
|
|
@@ -585,9 +702,15 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
585
702
|
const denied = role.tool_permissions?.deny?.join(", ") || "none";
|
|
586
703
|
const writeScope = role.scope?.write?.join(", ") || "none";
|
|
587
704
|
toolUsageLines.push(`### ${roleName}`);
|
|
705
|
+
toolUsageLines.push(`- Owner: ${isHumanRole(role) ? "human" : isInteractiveRole(role) ? "main_session_external_agent" : "delegated_agent"}`);
|
|
706
|
+
toolUsageLines.push(`- Description: ${role.description}`);
|
|
588
707
|
toolUsageLines.push(`- Allowed: ${allowed}`);
|
|
589
708
|
toolUsageLines.push(`- Denied: ${denied}`);
|
|
590
709
|
toolUsageLines.push(`- Write scope: ${writeScope}`);
|
|
710
|
+
for (const instruction of role.instructions ?? [])
|
|
711
|
+
toolUsageLines.push(`- Instruction: ${instruction}`);
|
|
712
|
+
for (const criterion of role.success_criteria ?? [])
|
|
713
|
+
toolUsageLines.push(`- Success criterion: ${criterion}`);
|
|
591
714
|
}
|
|
592
715
|
if (toolUsageLines.length > 0) {
|
|
593
716
|
lines.push("<Tool_Usage>");
|
|
@@ -631,6 +754,8 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
631
754
|
for (const cp of step.checkpoints) {
|
|
632
755
|
const action = cp.action || "block";
|
|
633
756
|
lines.push(`- ${cp.assert}: ${cp.message} (${action})`);
|
|
757
|
+
if (cp.command)
|
|
758
|
+
lines.push(` command: ${cp.command}`);
|
|
634
759
|
}
|
|
635
760
|
}
|
|
636
761
|
lines.push("</Checkpoints>");
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { SyncOutputArtifactId } from "./output-artifact-registry.js";
|
|
2
|
+
import type { PlannedArtifact, SyncTarget } from "./sync-target-plan.js";
|
|
3
|
+
export declare const SYNC_ARTIFACT_REGISTRY_PATH = ".dna/sync/registry.json";
|
|
4
|
+
export declare const SYNC_ARTIFACT_REGISTRY_SCHEMA = "intentdna.sync_artifact_registry.v1";
|
|
5
|
+
export type SyncArtifactOwnership = PlannedArtifact["ownership"] | "unknown";
|
|
6
|
+
export type SyncArtifactStatus = "ok" | "missing" | "drifted" | "untracked";
|
|
7
|
+
export interface SyncArtifactRegistryTemplate {
|
|
8
|
+
sourceDnaIds: string[];
|
|
9
|
+
configFiles: string[];
|
|
10
|
+
compiledIrHash?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SyncArtifactRegistryEntry {
|
|
13
|
+
artifact: SyncOutputArtifactId;
|
|
14
|
+
kind: string;
|
|
15
|
+
path: string;
|
|
16
|
+
relativePath: string;
|
|
17
|
+
target: SyncTarget | "markdown" | "generic";
|
|
18
|
+
adapter?: SyncTarget;
|
|
19
|
+
installMode?: string;
|
|
20
|
+
ownership: SyncArtifactOwnership;
|
|
21
|
+
trackedByLock: boolean;
|
|
22
|
+
sha256?: string;
|
|
23
|
+
missingAtSync?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface SyncArtifactRegistry {
|
|
26
|
+
schema: typeof SYNC_ARTIFACT_REGISTRY_SCHEMA;
|
|
27
|
+
syncedAt: string;
|
|
28
|
+
projectDir: string;
|
|
29
|
+
dnaVersion?: string;
|
|
30
|
+
mode: "write";
|
|
31
|
+
template: SyncArtifactRegistryTemplate;
|
|
32
|
+
artifacts: SyncArtifactRegistryEntry[];
|
|
33
|
+
}
|
|
34
|
+
export interface SyncArtifactRegistryInputEntry {
|
|
35
|
+
artifact: SyncOutputArtifactId;
|
|
36
|
+
path: string;
|
|
37
|
+
kind?: string;
|
|
38
|
+
target?: SyncArtifactRegistryEntry["target"];
|
|
39
|
+
adapter?: SyncTarget;
|
|
40
|
+
installMode?: string;
|
|
41
|
+
ownership?: SyncArtifactOwnership;
|
|
42
|
+
trackedByLock?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface WriteSyncArtifactRegistryOptions {
|
|
45
|
+
projectDir: string;
|
|
46
|
+
dnaVersion?: string;
|
|
47
|
+
template: SyncArtifactRegistryTemplate;
|
|
48
|
+
artifacts: SyncArtifactRegistryInputEntry[];
|
|
49
|
+
}
|
|
50
|
+
export interface SyncArtifactInspection extends SyncArtifactRegistryEntry {
|
|
51
|
+
status: SyncArtifactStatus;
|
|
52
|
+
currentSha256?: string;
|
|
53
|
+
reason: string;
|
|
54
|
+
}
|
|
55
|
+
export interface SyncArtifactRegistryInspection {
|
|
56
|
+
registry: SyncArtifactRegistry | null;
|
|
57
|
+
registryPath: string;
|
|
58
|
+
artifacts: SyncArtifactInspection[];
|
|
59
|
+
totals: {
|
|
60
|
+
artifacts: number;
|
|
61
|
+
ok: number;
|
|
62
|
+
missing: number;
|
|
63
|
+
drifted: number;
|
|
64
|
+
untracked: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface RemoveSyncArtifactsOptions {
|
|
68
|
+
projectDir?: string;
|
|
69
|
+
dryRun?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface RemoveSyncArtifactsResult {
|
|
72
|
+
schema_version: "intentdna.sync_remove_result.v1";
|
|
73
|
+
mode: "sync_registry_remove";
|
|
74
|
+
registryPath: string;
|
|
75
|
+
removed: string[];
|
|
76
|
+
skipped: Array<{
|
|
77
|
+
path: string;
|
|
78
|
+
reason: string;
|
|
79
|
+
}>;
|
|
80
|
+
preservation: {
|
|
81
|
+
note: string;
|
|
82
|
+
kept: Array<{
|
|
83
|
+
path: string;
|
|
84
|
+
reason: string;
|
|
85
|
+
explanation: string;
|
|
86
|
+
}>;
|
|
87
|
+
nextCommands: string[];
|
|
88
|
+
};
|
|
89
|
+
dryRun: boolean;
|
|
90
|
+
totals: {
|
|
91
|
+
removable: number;
|
|
92
|
+
skipped: number;
|
|
93
|
+
};
|
|
94
|
+
boundary: string;
|
|
95
|
+
}
|
|
96
|
+
export declare function writeSyncArtifactRegistry(options: WriteSyncArtifactRegistryOptions): Promise<SyncArtifactRegistry>;
|
|
97
|
+
export declare function readSyncArtifactRegistry(projectDir?: string): Promise<SyncArtifactRegistry | null>;
|
|
98
|
+
export declare function inspectSyncArtifactRegistry(projectDir?: string): Promise<SyncArtifactRegistryInspection>;
|
|
99
|
+
export declare function removeSyncArtifacts(options?: RemoveSyncArtifactsOptions): Promise<RemoveSyncArtifactsResult>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, relative, resolve } from "node:path";
|
|
4
|
+
export const SYNC_ARTIFACT_REGISTRY_PATH = ".dna/sync/registry.json";
|
|
5
|
+
export const SYNC_ARTIFACT_REGISTRY_SCHEMA = "intentdna.sync_artifact_registry.v1";
|
|
6
|
+
function fileSha256(content) {
|
|
7
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
8
|
+
}
|
|
9
|
+
async function computeFileHash(path) {
|
|
10
|
+
try {
|
|
11
|
+
return fileSha256(await readFile(path, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function registryPath(projectDir) {
|
|
18
|
+
return resolve(projectDir, SYNC_ARTIFACT_REGISTRY_PATH);
|
|
19
|
+
}
|
|
20
|
+
async function canonicalPath(path) {
|
|
21
|
+
return realpath(path).catch(() => resolve(path));
|
|
22
|
+
}
|
|
23
|
+
async function normalizeEntry(projectDir, entry) {
|
|
24
|
+
const canonicalProjectDir = await canonicalPath(projectDir);
|
|
25
|
+
const absolutePath = resolve(projectDir, entry.path);
|
|
26
|
+
const canonicalArtifactPath = await canonicalPath(absolutePath);
|
|
27
|
+
return {
|
|
28
|
+
artifact: entry.artifact,
|
|
29
|
+
kind: entry.kind ?? entry.artifact,
|
|
30
|
+
path: canonicalArtifactPath,
|
|
31
|
+
relativePath: relative(canonicalProjectDir, canonicalArtifactPath) || ".",
|
|
32
|
+
target: entry.target ?? entry.adapter ?? "generic",
|
|
33
|
+
adapter: entry.adapter,
|
|
34
|
+
installMode: entry.installMode,
|
|
35
|
+
ownership: entry.ownership ?? "unknown",
|
|
36
|
+
trackedByLock: entry.trackedByLock ?? true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function uniqueArtifacts(artifacts) {
|
|
40
|
+
const byKey = new Map();
|
|
41
|
+
for (const artifact of artifacts) {
|
|
42
|
+
byKey.set(`${artifact.artifact}:${artifact.path}:${artifact.kind}`, artifact);
|
|
43
|
+
}
|
|
44
|
+
return [...byKey.values()].sort((a, b) => a.target.localeCompare(b.target) ||
|
|
45
|
+
a.relativePath.localeCompare(b.relativePath) ||
|
|
46
|
+
a.kind.localeCompare(b.kind));
|
|
47
|
+
}
|
|
48
|
+
export async function writeSyncArtifactRegistry(options) {
|
|
49
|
+
const projectDir = await canonicalPath(options.projectDir);
|
|
50
|
+
const artifacts = uniqueArtifacts(await Promise.all(options.artifacts.map((entry) => normalizeEntry(projectDir, entry))));
|
|
51
|
+
const withHashes = [];
|
|
52
|
+
for (const artifact of artifacts) {
|
|
53
|
+
const sha256 = await computeFileHash(artifact.path);
|
|
54
|
+
withHashes.push(sha256 ? { ...artifact, sha256 } : { ...artifact, missingAtSync: true });
|
|
55
|
+
}
|
|
56
|
+
const registry = {
|
|
57
|
+
schema: SYNC_ARTIFACT_REGISTRY_SCHEMA,
|
|
58
|
+
syncedAt: new Date().toISOString(),
|
|
59
|
+
projectDir,
|
|
60
|
+
dnaVersion: options.dnaVersion,
|
|
61
|
+
mode: "write",
|
|
62
|
+
template: options.template,
|
|
63
|
+
artifacts: withHashes,
|
|
64
|
+
};
|
|
65
|
+
const path = registryPath(options.projectDir);
|
|
66
|
+
await mkdir(dirname(path), { recursive: true });
|
|
67
|
+
await writeFile(path, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
68
|
+
return registry;
|
|
69
|
+
}
|
|
70
|
+
export async function readSyncArtifactRegistry(projectDir = process.cwd()) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await readFile(registryPath(projectDir), "utf-8");
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (parsed.schema !== SYNC_ARTIFACT_REGISTRY_SCHEMA || !Array.isArray(parsed.artifacts))
|
|
75
|
+
return null;
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function inspectEntry(entry) {
|
|
83
|
+
if (!entry.sha256) {
|
|
84
|
+
const exists = await stat(entry.path).then(() => true, () => false);
|
|
85
|
+
return {
|
|
86
|
+
...entry,
|
|
87
|
+
status: exists ? "untracked" : "missing",
|
|
88
|
+
reason: exists ? "No sync-time hash recorded for this artifact." : "Artifact did not exist when inspected.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const currentSha256 = await computeFileHash(entry.path);
|
|
92
|
+
if (!currentSha256) {
|
|
93
|
+
return { ...entry, status: "missing", reason: "Artifact path is missing." };
|
|
94
|
+
}
|
|
95
|
+
if (currentSha256 !== entry.sha256) {
|
|
96
|
+
return { ...entry, status: "drifted", currentSha256, reason: "Artifact hash differs from last sync." };
|
|
97
|
+
}
|
|
98
|
+
return { ...entry, status: "ok", currentSha256, reason: "Artifact matches last sync." };
|
|
99
|
+
}
|
|
100
|
+
export async function inspectSyncArtifactRegistry(projectDir = process.cwd()) {
|
|
101
|
+
const registry = await readSyncArtifactRegistry(projectDir);
|
|
102
|
+
const artifacts = registry ? await Promise.all(registry.artifacts.map(inspectEntry)) : [];
|
|
103
|
+
const totals = {
|
|
104
|
+
artifacts: artifacts.length,
|
|
105
|
+
ok: artifacts.filter((entry) => entry.status === "ok").length,
|
|
106
|
+
missing: artifacts.filter((entry) => entry.status === "missing").length,
|
|
107
|
+
drifted: artifacts.filter((entry) => entry.status === "drifted").length,
|
|
108
|
+
untracked: artifacts.filter((entry) => entry.status === "untracked").length,
|
|
109
|
+
};
|
|
110
|
+
return { registry, registryPath: registryPath(projectDir), artifacts, totals };
|
|
111
|
+
}
|
|
112
|
+
function isSafeToRemove(entry) {
|
|
113
|
+
if (entry.status !== "ok")
|
|
114
|
+
return false;
|
|
115
|
+
if (!entry.sha256 || entry.currentSha256 !== entry.sha256)
|
|
116
|
+
return false;
|
|
117
|
+
return entry.ownership === "intentdna_owned" || entry.ownership === "diagnostic";
|
|
118
|
+
}
|
|
119
|
+
function removalSkipExplanation(entry) {
|
|
120
|
+
if (entry.status === "drifted") {
|
|
121
|
+
return "Preserved because the file changed after the last sync; review the diff or rerun dna sync before cleanup.";
|
|
122
|
+
}
|
|
123
|
+
if (entry.status === "missing") {
|
|
124
|
+
return "No file was deleted because the registry path is already missing.";
|
|
125
|
+
}
|
|
126
|
+
if (entry.status === "untracked") {
|
|
127
|
+
return "Preserved because no sync-time hash proves IntentDNA still owns the current file.";
|
|
128
|
+
}
|
|
129
|
+
if (entry.ownership === "merge_preserve_user") {
|
|
130
|
+
return "Preserved because the artifact is a merge-preserve user configuration surface.";
|
|
131
|
+
}
|
|
132
|
+
if (entry.ownership === "owned_block") {
|
|
133
|
+
return "Preserved because IntentDNA owns only an injected block inside this user file.";
|
|
134
|
+
}
|
|
135
|
+
if (entry.ownership === "unknown") {
|
|
136
|
+
return "Preserved because ownership is unknown.";
|
|
137
|
+
}
|
|
138
|
+
return "Preserved because this artifact is outside the safe remove set.";
|
|
139
|
+
}
|
|
140
|
+
export async function removeSyncArtifacts(options = {}) {
|
|
141
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
142
|
+
const inspection = await inspectSyncArtifactRegistry(projectDir);
|
|
143
|
+
const removed = [];
|
|
144
|
+
const skipped = [];
|
|
145
|
+
const kept = [];
|
|
146
|
+
for (const artifact of inspection.artifacts) {
|
|
147
|
+
if (!isSafeToRemove(artifact)) {
|
|
148
|
+
const reason = `${artifact.ownership}/${artifact.status}`;
|
|
149
|
+
skipped.push({ path: artifact.path, reason });
|
|
150
|
+
kept.push({ path: artifact.path, reason, explanation: removalSkipExplanation(artifact) });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!options.dryRun)
|
|
154
|
+
await rm(artifact.path, { recursive: true, force: true });
|
|
155
|
+
removed.push(artifact.path);
|
|
156
|
+
}
|
|
157
|
+
if (inspection.registry && !options.dryRun && skipped.length === 0) {
|
|
158
|
+
await rm(inspection.registryPath, { force: true });
|
|
159
|
+
removed.push(inspection.registryPath);
|
|
160
|
+
}
|
|
161
|
+
else if (inspection.registry && !options.dryRun) {
|
|
162
|
+
const keptPaths = new Set(skipped.map((entry) => entry.path));
|
|
163
|
+
const preservedRegistry = {
|
|
164
|
+
...inspection.registry,
|
|
165
|
+
artifacts: inspection.registry.artifacts.filter((artifact) => keptPaths.has(artifact.path)),
|
|
166
|
+
};
|
|
167
|
+
await writeFile(inspection.registryPath, JSON.stringify(preservedRegistry, null, 2) + "\n", "utf-8");
|
|
168
|
+
}
|
|
169
|
+
else if (inspection.registry && skipped.length === 0) {
|
|
170
|
+
removed.push(inspection.registryPath);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
schema_version: "intentdna.sync_remove_result.v1",
|
|
174
|
+
mode: "sync_registry_remove",
|
|
175
|
+
registryPath: inspection.registryPath,
|
|
176
|
+
removed,
|
|
177
|
+
skipped,
|
|
178
|
+
preservation: {
|
|
179
|
+
note: "Safe remove preserves user-owned, merge-preserve, owned-block, drifted, missing, untracked, and unknown-ownership registry entries.",
|
|
180
|
+
kept,
|
|
181
|
+
nextCommands: [
|
|
182
|
+
"dna sync status",
|
|
183
|
+
"dna sync doctor",
|
|
184
|
+
"dna sync remove --dry-run",
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
dryRun: options.dryRun === true,
|
|
188
|
+
totals: {
|
|
189
|
+
removable: removed.length,
|
|
190
|
+
skipped: skipped.length,
|
|
191
|
+
},
|
|
192
|
+
boundary: "remove only deletes hash-matching IntentDNA-owned/diagnostic artifacts from the sync registry; merge-preserve/user-owned files are skipped",
|
|
193
|
+
};
|
|
194
|
+
}
|