@tea-agent/loop-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { summarizeDecisionEnvelopeEvidenceCompleteness, } from "./decision-evidence.js";
|
|
5
|
+
import { locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
6
|
+
export const DECISION_ENVELOPE_FENCE_INFO = "DECISION_ENVELOPE_JSON";
|
|
7
|
+
const decisionGateTypeSchema = z.enum([
|
|
8
|
+
"plan-gate",
|
|
9
|
+
"side-effect-gate",
|
|
10
|
+
"acceptance-gate",
|
|
11
|
+
"closeout-gate",
|
|
12
|
+
]);
|
|
13
|
+
const decisionScopeSchema = z.enum([
|
|
14
|
+
"node",
|
|
15
|
+
"rank",
|
|
16
|
+
"dag-run",
|
|
17
|
+
"task",
|
|
18
|
+
"repo",
|
|
19
|
+
]);
|
|
20
|
+
const decisionValueSchema = z.enum([
|
|
21
|
+
"auto-approve",
|
|
22
|
+
"approve-with-constraints",
|
|
23
|
+
"request-revision",
|
|
24
|
+
"run-more-verification",
|
|
25
|
+
"split-followup",
|
|
26
|
+
"reject",
|
|
27
|
+
"escalate-to-human",
|
|
28
|
+
"pause-wait-external",
|
|
29
|
+
]);
|
|
30
|
+
const nextActionSchema = z.enum([
|
|
31
|
+
"continue",
|
|
32
|
+
"rerun-implement",
|
|
33
|
+
"rerun-verify",
|
|
34
|
+
"run-targeted-check",
|
|
35
|
+
"split-followup",
|
|
36
|
+
"pause-and-ask",
|
|
37
|
+
"abort",
|
|
38
|
+
]);
|
|
39
|
+
const riskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
|
|
40
|
+
const evidenceStatusSchema = z.enum([
|
|
41
|
+
"verified",
|
|
42
|
+
"partial",
|
|
43
|
+
"self-reported",
|
|
44
|
+
"conflicting",
|
|
45
|
+
"missing",
|
|
46
|
+
]);
|
|
47
|
+
const humanEscalationOptionSchema = z.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
label: z.string().min(1),
|
|
50
|
+
risk: z.string().min(1),
|
|
51
|
+
reason: z.string().min(1),
|
|
52
|
+
});
|
|
53
|
+
const humanEscalationSchema = z.object({
|
|
54
|
+
question: z.string().min(1),
|
|
55
|
+
recommendedOption: z.string().min(1),
|
|
56
|
+
options: z.array(humanEscalationOptionSchema).min(1),
|
|
57
|
+
});
|
|
58
|
+
const evidenceItemSchema = z.object({
|
|
59
|
+
path: z.string().min(1),
|
|
60
|
+
kind: z.string().min(1),
|
|
61
|
+
status: evidenceStatusSchema,
|
|
62
|
+
summary: z.string().min(1),
|
|
63
|
+
});
|
|
64
|
+
export const decisionEnvelopeSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
schemaVersion: z.literal(1),
|
|
67
|
+
gateType: decisionGateTypeSchema,
|
|
68
|
+
decisionScope: decisionScopeSchema,
|
|
69
|
+
decision: decisionValueSchema,
|
|
70
|
+
confidence: z.number().min(0).max(1),
|
|
71
|
+
riskLevel: riskLevelSchema,
|
|
72
|
+
requiresHuman: z.boolean(),
|
|
73
|
+
nextAction: nextActionSchema,
|
|
74
|
+
policyVersion: z.string().min(1),
|
|
75
|
+
policyChecks: z.object({
|
|
76
|
+
mustEscalateFlags: z.array(z.string().min(1)),
|
|
77
|
+
evidenceComplete: z.boolean(),
|
|
78
|
+
allowedAutoApprove: z.boolean(),
|
|
79
|
+
}),
|
|
80
|
+
rationale: z.array(z.string().min(1)).min(1),
|
|
81
|
+
evidence: z.array(evidenceItemSchema).min(1),
|
|
82
|
+
blockingFindings: z.array(z.string().min(1)),
|
|
83
|
+
requiredRevisions: z.array(z.string().min(1)),
|
|
84
|
+
riskFlags: z.array(z.string().min(1)),
|
|
85
|
+
humanEscalation: humanEscalationSchema.nullable(),
|
|
86
|
+
audit: z
|
|
87
|
+
.object({
|
|
88
|
+
runId: z.string().min(1),
|
|
89
|
+
nodeId: z.string().min(1),
|
|
90
|
+
model: z.string().min(1),
|
|
91
|
+
sourceHash: z.string().optional(),
|
|
92
|
+
})
|
|
93
|
+
.passthrough(),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
const DECISION_ENVELOPE_FENCE_RE = /```DECISION_ENVELOPE_JSON[ \t]*\r?\n([\s\S]*?)\r?\n```/g;
|
|
97
|
+
export function extractDecisionEnvelopeFencedBlocks(text) {
|
|
98
|
+
const blocks = [];
|
|
99
|
+
if (!text)
|
|
100
|
+
return blocks;
|
|
101
|
+
const re = new RegExp(DECISION_ENVELOPE_FENCE_RE.source, DECISION_ENVELOPE_FENCE_RE.flags);
|
|
102
|
+
for (const match of text.matchAll(re)) {
|
|
103
|
+
const body = match[1]?.trim();
|
|
104
|
+
if (body)
|
|
105
|
+
blocks.push(body);
|
|
106
|
+
}
|
|
107
|
+
return blocks;
|
|
108
|
+
}
|
|
109
|
+
export function parseDecisionEnvelopeJson(rawJson) {
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(rawJson);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
errors: [
|
|
118
|
+
`malformed JSON in ${DECISION_ENVELOPE_FENCE_INFO} block: ${error instanceof Error ? error.message : String(error)}`,
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const schemaResult = decisionEnvelopeSchema.safeParse(parsed);
|
|
123
|
+
if (!schemaResult.success) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
errors: schemaResult.error.issues.map((issue) => `schema: ${issue.path.join(".") || "(root)"}: ${issue.message}`),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const semanticErrors = validateDecisionEnvelopeSemantics(schemaResult.data);
|
|
130
|
+
if (semanticErrors.length > 0) {
|
|
131
|
+
return { ok: false, errors: semanticErrors };
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, envelope: schemaResult.data };
|
|
134
|
+
}
|
|
135
|
+
export function validateDecisionEnvelopeSemantics(envelope) {
|
|
136
|
+
const errors = [];
|
|
137
|
+
if ((envelope.decision === "auto-approve" ||
|
|
138
|
+
envelope.decision === "approve-with-constraints") &&
|
|
139
|
+
envelope.requiresHuman) {
|
|
140
|
+
errors.push(`${envelope.decision} must not set requiresHuman=true`);
|
|
141
|
+
}
|
|
142
|
+
if ((envelope.decision === "escalate-to-human" ||
|
|
143
|
+
envelope.decision === "pause-wait-external" ||
|
|
144
|
+
envelope.nextAction === "pause-and-ask") &&
|
|
145
|
+
(!envelope.requiresHuman || envelope.humanEscalation === null)) {
|
|
146
|
+
errors.push("escalate-to-human, pause-wait-external, or nextAction=pause-and-ask requires requiresHuman=true and humanEscalation object");
|
|
147
|
+
}
|
|
148
|
+
if (envelope.requiresHuman && envelope.nextAction !== "pause-and-ask") {
|
|
149
|
+
errors.push("requiresHuman=true requires nextAction=pause-and-ask");
|
|
150
|
+
}
|
|
151
|
+
if (!envelope.requiresHuman && envelope.humanEscalation !== null) {
|
|
152
|
+
errors.push("requiresHuman=false must not include a blocking humanEscalation question");
|
|
153
|
+
}
|
|
154
|
+
if (envelope.humanEscalation) {
|
|
155
|
+
const optionIds = new Set(envelope.humanEscalation.options.map((option) => option.id));
|
|
156
|
+
if (!optionIds.has(envelope.humanEscalation.recommendedOption)) {
|
|
157
|
+
errors.push("humanEscalation.recommendedOption must match one of options[].id");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if ((envelope.riskLevel === "high" || envelope.riskLevel === "critical") &&
|
|
161
|
+
envelope.decision === "auto-approve") {
|
|
162
|
+
const verifiedEvidence = envelope.evidence.filter((item) => item.status === "verified");
|
|
163
|
+
if (!envelope.policyChecks.allowedAutoApprove ||
|
|
164
|
+
verifiedEvidence.length === 0) {
|
|
165
|
+
errors.push("high/critical risk must not auto-approve unless policyChecks.allowedAutoApprove is true and verified evidence exists");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return errors;
|
|
169
|
+
}
|
|
170
|
+
export function parseDecisionEnvelopeFromText(text, source) {
|
|
171
|
+
const blocks = extractDecisionEnvelopeFencedBlocks(text);
|
|
172
|
+
if (blocks.length === 0) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
errors: [`missing ${DECISION_ENVELOPE_FENCE_INFO} fenced block`],
|
|
176
|
+
source,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (blocks.length > 1) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
errors: [
|
|
183
|
+
`expected exactly one ${DECISION_ENVELOPE_FENCE_INFO} fenced block, found ${blocks.length}`,
|
|
184
|
+
],
|
|
185
|
+
source,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const parsed = parseDecisionEnvelopeJson(blocks[0]);
|
|
189
|
+
if (!parsed.ok) {
|
|
190
|
+
return { ok: false, errors: parsed.errors, source };
|
|
191
|
+
}
|
|
192
|
+
return { ok: true, envelope: parsed.envelope, source };
|
|
193
|
+
}
|
|
194
|
+
export async function readOptionalTextFile(filePath) {
|
|
195
|
+
try {
|
|
196
|
+
return await readFile(filePath, "utf-8");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function parseDecisionEnvelopeForNode(input) {
|
|
203
|
+
const primary = input.assistantText?.trim()
|
|
204
|
+
? parseDecisionEnvelopeFromText(input.assistantText, "assistantText")
|
|
205
|
+
: undefined;
|
|
206
|
+
if (primary?.ok) {
|
|
207
|
+
return primary;
|
|
208
|
+
}
|
|
209
|
+
if (primary &&
|
|
210
|
+
primary.errors.some((error) => error.startsWith("expected exactly one"))) {
|
|
211
|
+
return primary;
|
|
212
|
+
}
|
|
213
|
+
if (input.resultSummaryMarkdown?.trim()) {
|
|
214
|
+
const fallback = parseDecisionEnvelopeFromText(input.resultSummaryMarkdown, "resultSummaryFallback");
|
|
215
|
+
if (fallback.ok) {
|
|
216
|
+
return fallback;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (primary) {
|
|
220
|
+
return primary;
|
|
221
|
+
}
|
|
222
|
+
if (input.resultSummaryMarkdown?.trim()) {
|
|
223
|
+
return parseDecisionEnvelopeFromText(input.resultSummaryMarkdown, "resultSummaryFallback");
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
errors: [`missing ${DECISION_ENVELOPE_FENCE_INFO} fenced block`],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function toDecisionEnvelopeNodeRecord(result) {
|
|
231
|
+
if (!result.ok) {
|
|
232
|
+
return {
|
|
233
|
+
parseOk: false,
|
|
234
|
+
errors: result.errors,
|
|
235
|
+
source: result.source,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
parseOk: true,
|
|
240
|
+
decision: result.envelope.decision,
|
|
241
|
+
requiresHuman: result.envelope.requiresHuman,
|
|
242
|
+
nextAction: result.envelope.nextAction,
|
|
243
|
+
riskLevel: result.envelope.riskLevel,
|
|
244
|
+
source: result.source,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export async function writeDecisionEnvelopeArtifacts(nodeArtifactsDir, result) {
|
|
248
|
+
await mkdir(nodeArtifactsDir, { recursive: true });
|
|
249
|
+
if (result.ok) {
|
|
250
|
+
const envelopePath = path.join(nodeArtifactsDir, "decision.envelope.json");
|
|
251
|
+
await writeFile(envelopePath, `${JSON.stringify(result.envelope, null, 2)}\n`, "utf-8");
|
|
252
|
+
return { envelopePath };
|
|
253
|
+
}
|
|
254
|
+
const validationPath = path.join(nodeArtifactsDir, "decision.envelope.validation.json");
|
|
255
|
+
await writeFile(validationPath, `${JSON.stringify({
|
|
256
|
+
ok: false,
|
|
257
|
+
source: result.source ?? null,
|
|
258
|
+
errors: result.errors,
|
|
259
|
+
}, null, 2)}\n`, "utf-8");
|
|
260
|
+
return { validationPath };
|
|
261
|
+
}
|
|
262
|
+
export function isDecisionGateEnabled(task) {
|
|
263
|
+
return task.decisionGate?.enabled === true;
|
|
264
|
+
}
|
|
265
|
+
export function isPauseOnHumanDecisionGate(task) {
|
|
266
|
+
return (isDecisionGateEnabled(task) && task.decisionGate?.mode === "pause-on-human");
|
|
267
|
+
}
|
|
268
|
+
export function shouldPauseOnHumanEscalation(task, nodeRecord) {
|
|
269
|
+
return (isPauseOnHumanDecisionGate(task) &&
|
|
270
|
+
nodeRecord?.parseOk === true &&
|
|
271
|
+
nodeRecord.requiresHuman === true);
|
|
272
|
+
}
|
|
273
|
+
export async function writeHumanEscalationArtifacts(input) {
|
|
274
|
+
await mkdir(input.nodeArtifactsDir, { recursive: true });
|
|
275
|
+
const jsonPath = path.join(input.nodeArtifactsDir, "human-escalation.json");
|
|
276
|
+
const markdownPath = path.join(input.nodeArtifactsDir, "human-escalation.md");
|
|
277
|
+
const payload = {
|
|
278
|
+
schemaVersion: 1,
|
|
279
|
+
runId: input.runId,
|
|
280
|
+
nodeId: input.nodeId,
|
|
281
|
+
decision: input.envelope.decision,
|
|
282
|
+
requiresHuman: input.envelope.requiresHuman,
|
|
283
|
+
nextAction: input.envelope.nextAction,
|
|
284
|
+
riskLevel: input.envelope.riskLevel,
|
|
285
|
+
humanEscalation: input.envelope.humanEscalation,
|
|
286
|
+
pausedAt: input.pausedAt,
|
|
287
|
+
};
|
|
288
|
+
await writeFile(jsonPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
289
|
+
const escalation = input.envelope.humanEscalation;
|
|
290
|
+
const markdownLines = [
|
|
291
|
+
"# Human escalation",
|
|
292
|
+
"",
|
|
293
|
+
`- **Run**: \`${input.runId}\``,
|
|
294
|
+
`- **Node**: \`${input.nodeId}\``,
|
|
295
|
+
`- **Decision**: \`${input.envelope.decision}\``,
|
|
296
|
+
`- **Risk**: \`${input.envelope.riskLevel}\``,
|
|
297
|
+
`- **Paused at**: ${input.pausedAt}`,
|
|
298
|
+
"",
|
|
299
|
+
escalation ? `## Question\n\n${escalation.question}` : "## Question\n\n(none)",
|
|
300
|
+
];
|
|
301
|
+
if (escalation) {
|
|
302
|
+
markdownLines.push("", `Recommended option: \`${escalation.recommendedOption}\``, "", "## Options");
|
|
303
|
+
for (const option of escalation.options) {
|
|
304
|
+
markdownLines.push("", `### ${option.label} (\`${option.id}\`)`, `- Risk: ${option.risk}`, `- Reason: ${option.reason}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
await writeFile(markdownPath, `${markdownLines.join("\n")}\n`, "utf-8");
|
|
308
|
+
return { jsonPath, markdownPath };
|
|
309
|
+
}
|
|
310
|
+
export async function recordDecisionEnvelopeForNode(input) {
|
|
311
|
+
if (!isDecisionGateEnabled(input.task)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const resultSummaryMarkdown = await readOptionalTextFile(path.join(input.runDir, input.nodeId, "result.summary.md"));
|
|
315
|
+
const parseResult = await parseDecisionEnvelopeForNode({
|
|
316
|
+
assistantText: input.assistantText,
|
|
317
|
+
resultSummaryMarkdown,
|
|
318
|
+
});
|
|
319
|
+
const nodeRecord = toDecisionEnvelopeNodeRecord(parseResult);
|
|
320
|
+
await writeDecisionEnvelopeArtifacts(path.join(input.runDir, input.nodeId), parseResult);
|
|
321
|
+
return {
|
|
322
|
+
nodeRecord,
|
|
323
|
+
envelope: parseResult.ok ? parseResult.envelope : undefined,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export function parseDagDecisionCommandArgs(mode, args) {
|
|
327
|
+
if (args.length === 0) {
|
|
328
|
+
throw new Error(`usage: dag decision ${mode} --run-id <id> [--node-id <node-id>]`);
|
|
329
|
+
}
|
|
330
|
+
let runId;
|
|
331
|
+
let nodeId;
|
|
332
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
333
|
+
const arg = args[i];
|
|
334
|
+
if (arg === "--run-id") {
|
|
335
|
+
runId = args[++i];
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (arg.startsWith("--run-id=")) {
|
|
339
|
+
runId = arg.slice("--run-id=".length);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (arg === "--node-id") {
|
|
343
|
+
nodeId = args[++i];
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (arg.startsWith("--node-id=")) {
|
|
347
|
+
nodeId = arg.slice("--node-id=".length);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (arg.startsWith("-")) {
|
|
351
|
+
throw new Error(`unknown dag decision ${mode} flag: ${arg}`);
|
|
352
|
+
}
|
|
353
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
354
|
+
}
|
|
355
|
+
if (!runId) {
|
|
356
|
+
throw new Error(`usage: dag decision ${mode} --run-id <id> [--node-id <node-id>]`);
|
|
357
|
+
}
|
|
358
|
+
return { runId, nodeId };
|
|
359
|
+
}
|
|
360
|
+
function buildDecisionEnvelopeInspectNodeSummary(envelope) {
|
|
361
|
+
return {
|
|
362
|
+
decision: envelope.decision,
|
|
363
|
+
requiresHuman: envelope.requiresHuman,
|
|
364
|
+
nextAction: envelope.nextAction,
|
|
365
|
+
riskLevel: envelope.riskLevel,
|
|
366
|
+
gateType: envelope.gateType,
|
|
367
|
+
confidence: envelope.confidence,
|
|
368
|
+
blockingFindingsCount: envelope.blockingFindings.length,
|
|
369
|
+
requiredRevisionsCount: envelope.requiredRevisions.length,
|
|
370
|
+
riskFlags: envelope.riskFlags,
|
|
371
|
+
hasHumanEscalation: envelope.humanEscalation !== null,
|
|
372
|
+
evidence: summarizeDecisionEnvelopeEvidenceCompleteness(envelope),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function pathExists(filePath) {
|
|
376
|
+
try {
|
|
377
|
+
await access(filePath);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function resolvePersistedDecisionArtifact(nodeDir) {
|
|
385
|
+
if (await pathExists(path.join(nodeDir, "decision.envelope.json"))) {
|
|
386
|
+
return "decision.envelope.json";
|
|
387
|
+
}
|
|
388
|
+
if (await pathExists(path.join(nodeDir, "decision.envelope.validation.json"))) {
|
|
389
|
+
return "decision.envelope.validation.json";
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
async function listDecisionGateNodeIds(input) {
|
|
394
|
+
const ids = new Set();
|
|
395
|
+
for (const task of input.specTasks) {
|
|
396
|
+
if (isDecisionGateEnabled(task)) {
|
|
397
|
+
ids.add(task.id);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const [nodeId, node] of Object.entries(input.state.nodes)) {
|
|
401
|
+
if (node.decisionEnvelope !== undefined) {
|
|
402
|
+
ids.add(nodeId);
|
|
403
|
+
}
|
|
404
|
+
const nodeDir = path.join(input.runDir, nodeId);
|
|
405
|
+
if (await resolvePersistedDecisionArtifact(nodeDir)) {
|
|
406
|
+
ids.add(nodeId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return [...ids].sort();
|
|
410
|
+
}
|
|
411
|
+
export async function inspectDecisionEnvelopeNode(input) {
|
|
412
|
+
const nodeDir = path.join(input.runDir, input.nodeId);
|
|
413
|
+
const assistantText = input.nodeRecord?.assistantText;
|
|
414
|
+
const resultSummaryMarkdown = await readOptionalTextFile(path.join(nodeDir, "result.summary.md"));
|
|
415
|
+
const parseResult = await parseDecisionEnvelopeForNode({
|
|
416
|
+
assistantText,
|
|
417
|
+
resultSummaryMarkdown,
|
|
418
|
+
});
|
|
419
|
+
const persistedArtifact = await resolvePersistedDecisionArtifact(nodeDir);
|
|
420
|
+
const summary = parseResult.ok
|
|
421
|
+
? buildDecisionEnvelopeInspectNodeSummary(parseResult.envelope)
|
|
422
|
+
: {
|
|
423
|
+
decision: input.nodeRecord?.decisionEnvelope?.decision,
|
|
424
|
+
requiresHuman: input.nodeRecord?.decisionEnvelope?.requiresHuman,
|
|
425
|
+
nextAction: input.nodeRecord?.decisionEnvelope?.nextAction,
|
|
426
|
+
riskLevel: input.nodeRecord?.decisionEnvelope?.riskLevel,
|
|
427
|
+
};
|
|
428
|
+
return {
|
|
429
|
+
nodeId: input.nodeId,
|
|
430
|
+
parseOk: parseResult.ok,
|
|
431
|
+
source: parseResult.ok ? parseResult.source : parseResult.source,
|
|
432
|
+
persistedArtifact,
|
|
433
|
+
inputs: {
|
|
434
|
+
assistantText: Boolean(assistantText?.trim()),
|
|
435
|
+
resultSummaryMarkdown: Boolean(resultSummaryMarkdown?.trim()),
|
|
436
|
+
},
|
|
437
|
+
recordSummary: input.nodeRecord?.decisionEnvelope,
|
|
438
|
+
summary,
|
|
439
|
+
errors: parseResult.ok ? undefined : parseResult.errors,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
export async function buildDecisionEnvelopeInspectReport(repoRoot, args, mode) {
|
|
443
|
+
const located = await locateDagRun(repoRoot, args.runId);
|
|
444
|
+
if (!located) {
|
|
445
|
+
throw new Error(`dag run not found: ${args.runId}`);
|
|
446
|
+
}
|
|
447
|
+
const { lifecycle, runDir } = located;
|
|
448
|
+
const state = await readDagRunState(runDir);
|
|
449
|
+
const spec = await readDagRunSpec(runDir);
|
|
450
|
+
let nodeIds = await listDecisionGateNodeIds({
|
|
451
|
+
runDir,
|
|
452
|
+
specTasks: spec.tasks,
|
|
453
|
+
state,
|
|
454
|
+
});
|
|
455
|
+
if (args.nodeId) {
|
|
456
|
+
if (!nodeIds.includes(args.nodeId)) {
|
|
457
|
+
const nodeDir = path.join(runDir, args.nodeId);
|
|
458
|
+
const hasArtifacts = Boolean(state.nodes[args.nodeId]) ||
|
|
459
|
+
(await resolvePersistedDecisionArtifact(nodeDir)) !== null;
|
|
460
|
+
if (!hasArtifacts) {
|
|
461
|
+
throw new Error(`decision gate node not found for run ${args.runId}: ${args.nodeId}`);
|
|
462
|
+
}
|
|
463
|
+
nodeIds = [args.nodeId];
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
nodeIds = [args.nodeId];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (nodeIds.length === 0) {
|
|
470
|
+
throw new Error(`no decision gate nodes found for run ${args.runId}`);
|
|
471
|
+
}
|
|
472
|
+
const nodes = [];
|
|
473
|
+
for (const nodeId of nodeIds) {
|
|
474
|
+
nodes.push(await inspectDecisionEnvelopeNode({
|
|
475
|
+
runDir,
|
|
476
|
+
nodeId,
|
|
477
|
+
nodeRecord: state.nodes[nodeId],
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
mode,
|
|
482
|
+
runId: args.runId,
|
|
483
|
+
lifecycle,
|
|
484
|
+
dryRun: true,
|
|
485
|
+
advisoryOnly: true,
|
|
486
|
+
valid: nodes.every((node) => node.parseOk),
|
|
487
|
+
nodes,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
export async function runDagDecisionInspect(repoRoot, rawArgs) {
|
|
491
|
+
const parsed = parseDagDecisionCommandArgs("inspect", rawArgs);
|
|
492
|
+
const report = await buildDecisionEnvelopeInspectReport(repoRoot, parsed, "inspect");
|
|
493
|
+
console.log(JSON.stringify(report, null, 2));
|
|
494
|
+
}
|
|
495
|
+
export async function runDagDecisionValidate(repoRoot, rawArgs) {
|
|
496
|
+
const parsed = parseDagDecisionCommandArgs("validate", rawArgs);
|
|
497
|
+
const report = await buildDecisionEnvelopeInspectReport(repoRoot, parsed, "validate");
|
|
498
|
+
console.log(JSON.stringify(report, null, 2));
|
|
499
|
+
if (!report.valid) {
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export const DAG_DECISION_GATE_EVIDENCE_KIND = "dag-report-derived";
|
|
2
|
+
export const DAG_DECISION_GATE_EVIDENCE_STATUS = "verified";
|
|
3
|
+
export const DAG_DECISION_GATE_ADVISORY_NOTICE = "advisory only, not an execution directive";
|
|
4
|
+
function isFailedRunStatus(status) {
|
|
5
|
+
return status === "failed" || status === "partial_failed";
|
|
6
|
+
}
|
|
7
|
+
function isActionableNodeForRecovery(node) {
|
|
8
|
+
return (node.status === "ERROR" ||
|
|
9
|
+
node.status === "SKIPPED" ||
|
|
10
|
+
(node.failureCategory !== undefined && node.failureCategory !== "success"));
|
|
11
|
+
}
|
|
12
|
+
function findPrimaryActionableNode(nodes) {
|
|
13
|
+
return (nodes.find((node) => node.status === "ERROR") ??
|
|
14
|
+
nodes.find(isActionableNodeForRecovery));
|
|
15
|
+
}
|
|
16
|
+
function resolvePrimaryFailureNode(run) {
|
|
17
|
+
if (run.pausedByNodeId) {
|
|
18
|
+
return run.nodes.find((node) => node.nodeId === run.pausedByNodeId);
|
|
19
|
+
}
|
|
20
|
+
if (isFailedRunStatus(run.status)) {
|
|
21
|
+
return findPrimaryActionableNode(run.nodes);
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function resolvePrimaryRecovery(run) {
|
|
26
|
+
if (run.recoveryRecommendation) {
|
|
27
|
+
return run.recoveryRecommendation;
|
|
28
|
+
}
|
|
29
|
+
const primaryNode = resolvePrimaryFailureNode(run);
|
|
30
|
+
if (primaryNode?.recoveryRecommendation) {
|
|
31
|
+
return primaryNode.recoveryRecommendation;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
action: "none",
|
|
35
|
+
summary: "No recovery needed.",
|
|
36
|
+
reason: `${run.runId} finished without a derived recovery recommendation.`,
|
|
37
|
+
humanRequired: false,
|
|
38
|
+
autoRetryEligible: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildPrimaryFailure(run, primaryNode) {
|
|
42
|
+
if (primaryNode) {
|
|
43
|
+
return {
|
|
44
|
+
scope: "node",
|
|
45
|
+
nodeId: primaryNode.nodeId,
|
|
46
|
+
nodeStatus: primaryNode.status,
|
|
47
|
+
failureCategory: primaryNode.failureCategory,
|
|
48
|
+
normalizedFailureCategory: primaryNode.normalizedFailureCategory,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
scope: "run",
|
|
53
|
+
failureCategory: run.failureCategory,
|
|
54
|
+
normalizedFailureCategory: run.normalizedFailureCategory,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function collectDownstreamSkippedNodes(run) {
|
|
58
|
+
return run.downstreamSkippedNodes.map((node) => ({
|
|
59
|
+
nodeId: node.nodeId,
|
|
60
|
+
failureCategory: node.failureCategory,
|
|
61
|
+
normalizedFailureCategory: node.normalizedFailureCategory,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
function dagRunEvidencePath(run) {
|
|
65
|
+
return `.harness/dag-runs/${run.lifecycle}/${run.runId}/state.json`;
|
|
66
|
+
}
|
|
67
|
+
function formatEvidenceSummary(input) {
|
|
68
|
+
const { primaryFailure, primaryRecovery } = input;
|
|
69
|
+
const rawCategory = primaryFailure.failureCategory ?? primaryFailure.nodeStatus ?? "unknown";
|
|
70
|
+
const normalizedCategory = primaryFailure.normalizedFailureCategory ?? "unknown";
|
|
71
|
+
const nodeSuffix = primaryFailure.nodeId
|
|
72
|
+
? ` primaryFailureNodeId=${primaryFailure.nodeId}`
|
|
73
|
+
: "";
|
|
74
|
+
return [
|
|
75
|
+
`recoveryRecommendation action=${primaryRecovery.action}`,
|
|
76
|
+
`failureCategory=${rawCategory}`,
|
|
77
|
+
`normalizedFailureCategory=${normalizedCategory}`,
|
|
78
|
+
`humanRequired=${primaryRecovery.humanRequired}`,
|
|
79
|
+
`autoRetryEligible=${primaryRecovery.autoRetryEligible}`,
|
|
80
|
+
`reason=${primaryRecovery.reason}`,
|
|
81
|
+
`${DAG_DECISION_GATE_ADVISORY_NOTICE}${nodeSuffix}`,
|
|
82
|
+
].join(" ");
|
|
83
|
+
}
|
|
84
|
+
/** Read-only envelope evidence counters for inspect/validate dry-run output. */
|
|
85
|
+
export function summarizeDecisionEnvelopeEvidenceCompleteness(envelope) {
|
|
86
|
+
const counts = {
|
|
87
|
+
verified: 0,
|
|
88
|
+
partial: 0,
|
|
89
|
+
selfReported: 0,
|
|
90
|
+
conflicting: 0,
|
|
91
|
+
missing: 0,
|
|
92
|
+
};
|
|
93
|
+
for (const item of envelope.evidence) {
|
|
94
|
+
switch (item.status) {
|
|
95
|
+
case "verified":
|
|
96
|
+
counts.verified += 1;
|
|
97
|
+
break;
|
|
98
|
+
case "partial":
|
|
99
|
+
counts.partial += 1;
|
|
100
|
+
break;
|
|
101
|
+
case "self-reported":
|
|
102
|
+
counts.selfReported += 1;
|
|
103
|
+
break;
|
|
104
|
+
case "conflicting":
|
|
105
|
+
counts.conflicting += 1;
|
|
106
|
+
break;
|
|
107
|
+
case "missing":
|
|
108
|
+
counts.missing += 1;
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
evidenceCount: envelope.evidence.length,
|
|
116
|
+
verifiedEvidenceCount: counts.verified,
|
|
117
|
+
partialEvidenceCount: counts.partial,
|
|
118
|
+
selfReportedEvidenceCount: counts.selfReported,
|
|
119
|
+
conflictingEvidenceCount: counts.conflicting,
|
|
120
|
+
missingEvidenceCount: counts.missing,
|
|
121
|
+
evidenceComplete: envelope.policyChecks.evidenceComplete,
|
|
122
|
+
allowedAutoApprove: envelope.policyChecks.allowedAutoApprove,
|
|
123
|
+
mustEscalateFlags: envelope.policyChecks.mustEscalateFlags,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read-only summary of dag-report facts for Decision Gate prompts.
|
|
128
|
+
* Does not mutate run state or execute retry/resume.
|
|
129
|
+
*/
|
|
130
|
+
export function buildDagDecisionGateEvidence(run) {
|
|
131
|
+
const primaryNode = resolvePrimaryFailureNode(run);
|
|
132
|
+
const primaryFailure = buildPrimaryFailure(run, primaryNode);
|
|
133
|
+
const primaryRecovery = resolvePrimaryRecovery(run);
|
|
134
|
+
return {
|
|
135
|
+
runId: run.runId,
|
|
136
|
+
lifecycle: run.lifecycle,
|
|
137
|
+
status: run.status,
|
|
138
|
+
failureCategory: run.failureCategory,
|
|
139
|
+
normalizedFailureCategory: run.normalizedFailureCategory,
|
|
140
|
+
primaryFailure,
|
|
141
|
+
primaryRecovery,
|
|
142
|
+
downstreamSkippedNodes: collectDownstreamSkippedNodes(run),
|
|
143
|
+
evidence: [
|
|
144
|
+
{
|
|
145
|
+
path: dagRunEvidencePath(run),
|
|
146
|
+
kind: DAG_DECISION_GATE_EVIDENCE_KIND,
|
|
147
|
+
status: DAG_DECISION_GATE_EVIDENCE_STATUS,
|
|
148
|
+
summary: formatEvidenceSummary({ primaryFailure, primaryRecovery }),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
advisoryOnly: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./decision-envelope.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
|
|
2
|
+
import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
|
|
3
|
+
import { executeDagShellNode } from "../../executors/shell-executor.js";
|
|
4
|
+
import { executeDagStaticNode } from "../../executors/dag-static-executor.js";
|
|
5
|
+
function requireModel(input) {
|
|
6
|
+
if (!input.model) {
|
|
7
|
+
throw new Error(`executor ${input.task.executor} requires a resolved model`);
|
|
8
|
+
}
|
|
9
|
+
return input;
|
|
10
|
+
}
|
|
11
|
+
export const DEFAULT_DAG_EXECUTOR_REGISTRY = {
|
|
12
|
+
cursor: (input, meta) => executeDagCursorNode(requireModel(input), meta),
|
|
13
|
+
pi: (input, meta) => executeDagPiNode(requireModel(input), meta),
|
|
14
|
+
shell: executeDagShellNode,
|
|
15
|
+
static: executeDagStaticNode,
|
|
16
|
+
};
|
|
17
|
+
export function resolveDagExecutorHandler(executor, registry = DEFAULT_DAG_EXECUTOR_REGISTRY) {
|
|
18
|
+
return registry[executor];
|
|
19
|
+
}
|
|
20
|
+
export function createDagNodeExecutor(meta, registry = DEFAULT_DAG_EXECUTOR_REGISTRY) {
|
|
21
|
+
return async (input) => {
|
|
22
|
+
const handler = resolveDagExecutorHandler(input.task.executor, registry);
|
|
23
|
+
return handler(input, meta);
|
|
24
|
+
};
|
|
25
|
+
}
|