@tea-agent/loop-agent 0.22.0 → 0.24.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 +42 -108
- package/CHANGELOG.md +85 -0
- package/README.md +8 -5
- package/bin/agent-worker.js +0 -0
- package/dist/application/context-usage/skill-resolution-stats.js +263 -0
- package/dist/application/dag/generate-task-dag.js +17 -3
- package/dist/cli/command-definitions.js +8 -7
- package/dist/cli/program.js +17 -15
- package/dist/commands/doctor.js +269 -18
- package/dist/commands/init.js +101 -86
- package/dist/commands/stats.js +40 -11
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +486 -3
- package/dist/worker/console/app-data.js +6 -0
- package/dist/worker/console/chat/artifact-card.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +495 -0
- package/dist/worker/console/chat/chat-ui-policy.js +25 -0
- package/dist/worker/console/chat/composer-draft-store.js +45 -0
- package/dist/worker/console/chat/context-panel.js +54 -0
- package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
- package/dist/worker/console/chat/explore-tools.js +299 -0
- package/dist/worker/console/chat/human-gate-card.js +37 -0
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/interview-adapter.js +136 -0
- package/dist/worker/console/chat/model-resolver.js +106 -0
- package/dist/worker/console/chat/operation-card.js +23 -0
- package/dist/worker/console/chat/pi-console-config.js +158 -0
- package/dist/worker/console/chat/pi-runtime.js +1143 -0
- package/dist/worker/console/chat/repo-browser.js +140 -0
- package/dist/worker/console/chat/repo-walk.js +116 -0
- package/dist/worker/console/chat/resource-loader.js +67 -0
- package/dist/worker/console/chat/routes.js +1646 -0
- package/dist/worker/console/chat/runtime-context.js +24 -0
- package/dist/worker/console/chat/runtime-selection.js +37 -0
- package/dist/worker/console/chat/session-store.js +437 -0
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +125 -0
- package/dist/worker/console/chat/tools.js +195 -0
- package/dist/worker/console/chat/usage.js +37 -0
- package/dist/worker/console/chat/workspace-landing.js +56 -0
- package/dist/worker/console/dag-confirmation.js +42 -8
- package/dist/worker/console/human-gate-token.js +130 -0
- package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
- package/dist/worker/console/operation-runner.js +6 -2
- package/dist/worker/console/operation-sse.js +26 -0
- package/dist/worker/console/operator-actions.js +420 -7
- package/dist/worker/console/server.js +68 -1
- package/dist/worker/console/static/assets/index-BTbrEHnO.css +1 -0
- package/dist/worker/console/static/assets/index-D9qLevoP.js +27 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/feature/profile-schema.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -5
- package/dist/workflows/dag/backend-test-result-contract.js +229 -0
- package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
- package/dist/workflows/dag/init-hybrid.js +71 -22
- package/dist/workflows/dag/node-execution.js +38 -1
- package/dist/workflows/dag/output-protocol.js +89 -0
- package/dist/workflows/dag/prompt.js +35 -1
- package/dist/workflows/dag/recovery-recommendation.js +45 -0
- package/dist/workflows/dag/report.js +28 -1
- package/dist/workflows/dag/rerun-task.js +1 -1
- package/dist/workflows/dag/scheduler.js +9 -0
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +55 -0
- package/docs/README.md +73 -156
- package/docs/architecture/README.md +7 -6
- package/docs/architecture/dag-execution.md +2 -2
- package/docs/architecture/evolution.md +16 -14
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/governance/README.md +15 -0
- package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
- package/docs/init-surface.manifest.json +22 -4
- package/docs/operations/README.md +12 -0
- package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
- package/docs/skills/vetted-skill-registry.md +23 -3
- package/docs/templates/README.md +55 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/progress-log.md +9 -2
- package/harness.json +4 -4
- package/package.json +5 -5
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/grill-with-docs/SKILL.md +44 -52
- package/skills/grill-with-docs/adr-format.md +37 -26
- package/skills/grill-with-docs/context-format.md +18 -26
- package/skills/loop-agent/SKILL.md +28 -112
- package/skills/loop-agent/references/command-reference.md +9 -3
- package/skills/loop-agent/references/harness-policy.md +3 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/task-workflow.md +2 -0
- package/skills/systematic-debugging/SKILL.md +20 -4
- package/skills/test-driven-development/SKILL.md +10 -3
- package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
- package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
- /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
- /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
|
@@ -9,7 +9,7 @@ import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
|
9
9
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
10
10
|
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
11
11
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
12
|
-
import { buildProtocolRetryInstruction, validateOutputProtocol, } from "./output-protocol.js";
|
|
12
|
+
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, validateOutputProtocol, } from "./output-protocol.js";
|
|
13
13
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
14
14
|
import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
|
|
15
15
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
@@ -483,6 +483,43 @@ export async function executeDagNode(input) {
|
|
|
483
483
|
await input.persistState();
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
|
+
// AC2: after retries are exhausted on a canonical review verdict protocol
|
|
487
|
+
// node, attempt one deterministic normalization of the full terminal output.
|
|
488
|
+
// This ONLY re-wraps output so the first non-empty line is the single
|
|
489
|
+
// supported verdict; it never changes the verdict value, and missing /
|
|
490
|
+
// duplicated / conflicting / illegal verdicts remain protocol-invalid.
|
|
491
|
+
// Shell, write-guard, timeout, and model failures are never normalized.
|
|
492
|
+
if (terminalResult &&
|
|
493
|
+
!terminalResult.ok &&
|
|
494
|
+
terminalResult.failureCategory === "protocol-invalid" &&
|
|
495
|
+
task.outputProtocol) {
|
|
496
|
+
const fullText = terminalResult.assistantText && terminalResult.assistantText.trim().length > 0
|
|
497
|
+
? terminalResult.assistantText
|
|
498
|
+
: terminalResult.stdout ?? "";
|
|
499
|
+
const normalized = normalizeReviewVerdictAfterRetries(task.outputProtocol, fullText);
|
|
500
|
+
if (normalized.ok) {
|
|
501
|
+
const normalizedText = normalized.normalizedOutput;
|
|
502
|
+
terminalResult = {
|
|
503
|
+
...terminalResult,
|
|
504
|
+
ok: true,
|
|
505
|
+
failureCategory: undefined,
|
|
506
|
+
assistantText: normalizedText,
|
|
507
|
+
stdout: normalizedText,
|
|
508
|
+
stderr: [
|
|
509
|
+
terminalResult.stderr,
|
|
510
|
+
`verdict normalized after retries exhausted: ${normalized.verdictLine}`,
|
|
511
|
+
]
|
|
512
|
+
.filter(Boolean)
|
|
513
|
+
.join("\n"),
|
|
514
|
+
};
|
|
515
|
+
// Sync the observable node fields so downstream gates and reports see the
|
|
516
|
+
// normalized first-line verdict while the original evidence is preserved.
|
|
517
|
+
node.assistantText = normalizedText;
|
|
518
|
+
node.stdout = normalizedText;
|
|
519
|
+
node.failureCategory = undefined;
|
|
520
|
+
node.stderr = terminalResult.stderr;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
486
523
|
const result = terminalResult;
|
|
487
524
|
const started = Date.now();
|
|
488
525
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
2
3
|
/**
|
|
3
4
|
* Machine-readable output protocol for safe read-only Pi nodes.
|
|
4
5
|
*
|
|
@@ -74,3 +75,91 @@ export function buildProtocolRetryInstruction(protocol, reason) {
|
|
|
74
75
|
"</retry_instruction>",
|
|
75
76
|
].join("\n");
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* AC2: deterministic review-verdict normalization applied ONLY after retries
|
|
80
|
+
* are exhausted on a review verdict protocol node.
|
|
81
|
+
*
|
|
82
|
+
* The canonical review verdict protocol accepts exactly one of
|
|
83
|
+
* {@link REVIEW_VERDICT_OUTPUT_PROTOCOL}.validLines. This helper scans the
|
|
84
|
+
* FULL output (assistantText + stdout style text) for verdict lines using the
|
|
85
|
+
* same whole-line Markdown-emphasis normalization the verdict gate uses.
|
|
86
|
+
*
|
|
87
|
+
* Normalization succeeds if and only if the full output contains exactly one
|
|
88
|
+
* supported, unambiguous verdict line. It produces a normalized output whose
|
|
89
|
+
* first non-empty line is that verdict, preserving the original evidence text
|
|
90
|
+
* afterwards. It never changes the verdict value.
|
|
91
|
+
*
|
|
92
|
+
* It fails (returns ok:false) for:
|
|
93
|
+
* - no verdict line anywhere;
|
|
94
|
+
* - multiple verdict lines, even if identical (duplicated protocol ambiguity);
|
|
95
|
+
* - conflicting verdict lines;
|
|
96
|
+
* - any unsupported/illegal verdict value (e.g. `VERDICT: approve`);
|
|
97
|
+
* - malformed lines that are not a supported verdict after normalization;
|
|
98
|
+
* - natural-language approval prose without a real verdict token.
|
|
99
|
+
*
|
|
100
|
+
* This helper only operates on the canonical review verdict protocol; any
|
|
101
|
+
* other protocol is left untouched (ok:false) so decision envelopes, shell
|
|
102
|
+
* output, and structured protocols are never loosely tolerated.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeReviewVerdictAfterRetries(protocol, text) {
|
|
105
|
+
const isReviewVerdictProtocol = protocol.type === REVIEW_VERDICT_OUTPUT_PROTOCOL.type &&
|
|
106
|
+
Array.from(protocol.validLines).every((line, index) => line === REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines[index]) &&
|
|
107
|
+
protocol.validLines.length ===
|
|
108
|
+
REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines.length;
|
|
109
|
+
if (!isReviewVerdictProtocol) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
reason: "deterministic verdict normalization only applies to the canonical review verdict protocol",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const supported = new Set(REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines);
|
|
116
|
+
const candidates = [];
|
|
117
|
+
let seenIllegalVerdictPrefixedLine = false;
|
|
118
|
+
for (const rawLine of String(text).split(/\r?\n/)) {
|
|
119
|
+
const normalized = normalizeVerdictCandidateLine(rawLine);
|
|
120
|
+
if (!normalized)
|
|
121
|
+
continue;
|
|
122
|
+
// Detect any line that looks like a verdict token (VERDICT: ...) but is
|
|
123
|
+
// not a supported value, so a single valid + single illegal line cannot
|
|
124
|
+
// be silently normalized to the valid one.
|
|
125
|
+
if (/^VERDICT:/.test(normalized) && !supported.has(normalized)) {
|
|
126
|
+
seenIllegalVerdictPrefixedLine = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (supported.has(normalized)) {
|
|
130
|
+
candidates.push(normalized);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (seenIllegalVerdictPrefixedLine) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "output contains an illegal or unsupported VERDICT value; cannot normalize",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (candidates.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
reason: "no supported verdict line found in full output; cannot normalize",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const unique = new Set(candidates);
|
|
146
|
+
if (unique.size !== 1 || candidates.length !== 1) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
reason: "output contains multiple or conflicting verdict lines; normalization requires exactly one supported verdict",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const verdictLine = candidates[0];
|
|
153
|
+
const preservedEvidence = String(text)
|
|
154
|
+
.split(/\r?\n/)
|
|
155
|
+
.filter((line) => {
|
|
156
|
+
const normalized = normalizeVerdictCandidateLine(line);
|
|
157
|
+
return !normalized || !supported.has(normalized);
|
|
158
|
+
})
|
|
159
|
+
.join("\n")
|
|
160
|
+
.trim();
|
|
161
|
+
const normalizedOutput = preservedEvidence
|
|
162
|
+
? `${verdictLine}\n${preservedEvidence}`
|
|
163
|
+
: verdictLine;
|
|
164
|
+
return { ok: true, verdictLine, normalizedOutput };
|
|
165
|
+
}
|
|
@@ -58,13 +58,47 @@ export function formatOutputLanguageBlock(language = DEFAULT_DAG_OUTPUT_LANGUAGE
|
|
|
58
58
|
}
|
|
59
59
|
export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHARS) {
|
|
60
60
|
const sections = [];
|
|
61
|
+
const toleratedErrors = new Set(task.failureAwareDependsOn ?? []);
|
|
61
62
|
for (const depId of task.depends_on) {
|
|
62
63
|
const record = upstream[depId];
|
|
63
64
|
const stdout = record?.stdout?.trim() ? record.stdout : "";
|
|
64
65
|
const assistantText = record?.assistantText?.trim()
|
|
65
66
|
? record.assistantText
|
|
66
67
|
: "";
|
|
67
|
-
const
|
|
68
|
+
const stderr = record?.stderr?.trim() ? record.stderr : "";
|
|
69
|
+
const upstreamText = stdout || assistantText || stderr;
|
|
70
|
+
// AC1: a failure-aware recovery/diagnosis/closeout node may consume an
|
|
71
|
+
// upstream ERROR as settled failure evidence. Surface that evidence
|
|
72
|
+
// explicitly so the node can read the failure without the upstream being
|
|
73
|
+
// rewritten to FINISHED.
|
|
74
|
+
if (record?.status === "ERROR" &&
|
|
75
|
+
toleratedErrors.has(depId) &&
|
|
76
|
+
upstreamText) {
|
|
77
|
+
const { preview, truncated } = formatStdoutPreview(upstreamText, maxChars);
|
|
78
|
+
let section = `## Upstream failure evidence: ${depId} (status=ERROR${record.failureCategory ? `, failureCategory=${record.failureCategory}` : ""})\n${preview}`;
|
|
79
|
+
const artifactKind = stdout
|
|
80
|
+
? "stdout"
|
|
81
|
+
: assistantText
|
|
82
|
+
? "assistant"
|
|
83
|
+
: "stderr";
|
|
84
|
+
const artifactPath = artifactKind === "stdout"
|
|
85
|
+
? record.stdoutArtifactPath
|
|
86
|
+
: artifactKind === "assistant"
|
|
87
|
+
? record.assistantArtifactPath
|
|
88
|
+
: undefined;
|
|
89
|
+
if (truncated && artifactPath) {
|
|
90
|
+
section += formatUpstreamArtifactPointerMap(record, artifactKind === "stdout" ? "stdout" : "assistant");
|
|
91
|
+
}
|
|
92
|
+
else if (truncated && record.nodeRecordPath) {
|
|
93
|
+
section += [
|
|
94
|
+
"",
|
|
95
|
+
"Full failure record (read-only runner evidence — use read tool to fetch; do not edit):",
|
|
96
|
+
`- node record: ${record.nodeRecordPath}`,
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
sections.push(section);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
68
102
|
if (!record || record.status !== "FINISHED" || !upstreamText)
|
|
69
103
|
continue;
|
|
70
104
|
const artifactKind = stdout ? "stdout" : "assistant";
|
|
@@ -8,6 +8,51 @@ export const DAG_RECOVERY_ACTIONS = [
|
|
|
8
8
|
"inspect-upstream",
|
|
9
9
|
"unknown",
|
|
10
10
|
];
|
|
11
|
+
const FOLLOW_UP_BY_RAW_CATEGORY = {
|
|
12
|
+
"nonzero-exit": {
|
|
13
|
+
nextStepCategory: "repair",
|
|
14
|
+
reason: "Deterministic command exited nonzero; inspect stdout/stderr and exit code, then repair code, command, or environment before rerun.",
|
|
15
|
+
},
|
|
16
|
+
"decision-envelope-invalid": {
|
|
17
|
+
nextStepCategory: "revision",
|
|
18
|
+
reason: "Decision gate output violated the envelope protocol; revise the decision node prompt/output and do not infer approval.",
|
|
19
|
+
},
|
|
20
|
+
"protocol-invalid": {
|
|
21
|
+
nextStepCategory: "revision",
|
|
22
|
+
reason: "Model output violated the required protocol; if deterministic verdict normalization cannot prove exactly one supported verdict, revise/retry the protocol-producing node rather than inferring approval.",
|
|
23
|
+
},
|
|
24
|
+
"write-guard": {
|
|
25
|
+
nextStepCategory: "repair",
|
|
26
|
+
reason: "Write boundary was violated; narrow writeSet or revert out-of-bound changes before rerun. Write-guard failures must not be converted to success.",
|
|
27
|
+
},
|
|
28
|
+
timeout: {
|
|
29
|
+
nextStepCategory: "retry",
|
|
30
|
+
reason: "Execution exceeded its activity/wall-clock budget; inspect readiness/resource diagnostics and scoped timeout budget, then retry once the readiness issue is fixed.",
|
|
31
|
+
},
|
|
32
|
+
"controller-interrupted": {
|
|
33
|
+
nextStepCategory: "restart",
|
|
34
|
+
reason: "Controller was interrupted; reconcile run state from persisted DAG facts, then restart or resume the run.",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const UNKNOWN_FOLLOW_UP = {
|
|
38
|
+
nextStepCategory: "diagnosis",
|
|
39
|
+
reason: "Failure category is unavailable or unrecognized; run diagnosis/report before selecting repair/retry/restart.",
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic mapping from a raw failure category to a stable next-step
|
|
43
|
+
* category and reason. Unknown/missing categories route to diagnosis.
|
|
44
|
+
*
|
|
45
|
+
* This is a routing hint only; callers must not use it to change a DAG run's
|
|
46
|
+
* terminal status or to convert a genuine failure into success.
|
|
47
|
+
*/
|
|
48
|
+
export function recommendFollowUpForFailureCategory(rawFailureCategory) {
|
|
49
|
+
if (!rawFailureCategory)
|
|
50
|
+
return { ...UNKNOWN_FOLLOW_UP };
|
|
51
|
+
const direct = FOLLOW_UP_BY_RAW_CATEGORY[rawFailureCategory];
|
|
52
|
+
if (direct)
|
|
53
|
+
return { ...direct };
|
|
54
|
+
return { ...UNKNOWN_FOLLOW_UP };
|
|
55
|
+
}
|
|
11
56
|
const REPORT_DERIVED_ONLY = "Derived planning hint only; dag report does not execute retry or resume.";
|
|
12
57
|
const TRANSIENT_EXECUTOR_RAW = new Set([
|
|
13
58
|
"network",
|
|
@@ -8,7 +8,7 @@ import { dagRunDirExists, getDagRunDir, locateDagRun, readDagRunSpec, readDagRun
|
|
|
8
8
|
export const DAG_CLOSEOUT_DRAFT_DISCLAIMER = "> **Advisory only.** Derived from completed run facts. Canonical source remains `dag report --json` and `.harness/dag-runs/completed/<run-id>/`. Do not treat this draft as authoritative.";
|
|
9
9
|
import { dagNormalizedFailureCategorySchema, normalizeDagFailureCategory, } from "./failure-category.js";
|
|
10
10
|
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
11
|
-
import { DAG_RECOVERY_ACTIONS, planDagRecovery, } from "./recovery-recommendation.js";
|
|
11
|
+
import { DAG_RECOVERY_ACTIONS, planDagRecovery, recommendFollowUpForFailureCategory, } from "./recovery-recommendation.js";
|
|
12
12
|
import { dagNodeExecutorSchema, dagNodeStatusSchema, LEGACY_TOP_LEVEL_MODELS_ERROR, parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
13
13
|
import { checkRuntimeContractCompatible, DAG_CONTROLLER_CAPABILITIES, } from "./runtime-contract.js";
|
|
14
14
|
export const DAG_REPORT_SCHEMA_VERSION = 1;
|
|
@@ -141,6 +141,10 @@ const dagNodeReportRowSchema = z
|
|
|
141
141
|
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
142
142
|
productLineFailureCategory: dagProductLineFailureCategorySchema.optional(),
|
|
143
143
|
recommendedFollowUp: z.string().optional(),
|
|
144
|
+
recommendedFollowUpCategory: z
|
|
145
|
+
.enum(["repair", "revision", "retry", "restart", "diagnosis"])
|
|
146
|
+
.optional(),
|
|
147
|
+
recommendedFollowUpReason: z.string().optional(),
|
|
144
148
|
recoveryRecommendation: dagRecoveryRecommendationSchema.optional(),
|
|
145
149
|
backend: z.enum(["sdk", "cli"]).optional(),
|
|
146
150
|
sdkAttempted: z.boolean().optional(),
|
|
@@ -166,6 +170,10 @@ const dagRunReportEntrySchema = z
|
|
|
166
170
|
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
167
171
|
productLineFailureCategory: dagProductLineFailureCategorySchema.optional(),
|
|
168
172
|
recommendedFollowUp: z.string().optional(),
|
|
173
|
+
recommendedFollowUpCategory: z
|
|
174
|
+
.enum(["repair", "revision", "retry", "restart", "diagnosis"])
|
|
175
|
+
.optional(),
|
|
176
|
+
recommendedFollowUpReason: z.string().optional(),
|
|
169
177
|
recoveryRecommendation: dagRecoveryRecommendationSchema.optional(),
|
|
170
178
|
primaryFailure: dagReportPrimaryFailureSchema,
|
|
171
179
|
primaryRecovery: dagRecoveryRecommendationSchema,
|
|
@@ -434,6 +442,9 @@ export async function buildDagRunReportEntry(input) {
|
|
|
434
442
|
nodeId,
|
|
435
443
|
executor: node.executor,
|
|
436
444
|
});
|
|
445
|
+
const followUp = node.status === "ERROR" || node.status === "SKIPPED"
|
|
446
|
+
? recommendFollowUpForFailureCategory(node.failureCategory)
|
|
447
|
+
: undefined;
|
|
437
448
|
nodes.push({
|
|
438
449
|
nodeId,
|
|
439
450
|
rank,
|
|
@@ -448,6 +459,12 @@ export async function buildDagRunReportEntry(input) {
|
|
|
448
459
|
failureCategory: node.failureCategory,
|
|
449
460
|
normalizedFailureCategory,
|
|
450
461
|
...failureRouting,
|
|
462
|
+
...(followUp
|
|
463
|
+
? {
|
|
464
|
+
recommendedFollowUpCategory: followUp.nextStepCategory,
|
|
465
|
+
recommendedFollowUpReason: followUp.reason,
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
451
468
|
recoveryRecommendation: planDagRecovery({
|
|
452
469
|
status: node.status,
|
|
453
470
|
normalizedFailureCategory,
|
|
@@ -513,6 +530,10 @@ export async function buildDagRunReportEntry(input) {
|
|
|
513
530
|
normalizedFailureCategory: runRecoverySource.normalizedFailureCategory,
|
|
514
531
|
nodeId: "nodeId" in runRecoverySource ? runRecoverySource.nodeId : undefined,
|
|
515
532
|
});
|
|
533
|
+
const runFollowUp = input.state.status === "failed" ||
|
|
534
|
+
input.state.status === "partial_failed"
|
|
535
|
+
? recommendFollowUpForFailureCategory(input.state.failureCategory)
|
|
536
|
+
: undefined;
|
|
516
537
|
const partialEntry = {
|
|
517
538
|
runId: input.state.runId,
|
|
518
539
|
title: input.state.title,
|
|
@@ -524,6 +545,12 @@ export async function buildDagRunReportEntry(input) {
|
|
|
524
545
|
failureCategory: input.state.failureCategory,
|
|
525
546
|
normalizedFailureCategory,
|
|
526
547
|
...runFailureRouting,
|
|
548
|
+
...(runFollowUp
|
|
549
|
+
? {
|
|
550
|
+
recommendedFollowUpCategory: runFollowUp.nextStepCategory,
|
|
551
|
+
recommendedFollowUpReason: runFollowUp.reason,
|
|
552
|
+
}
|
|
553
|
+
: {}),
|
|
527
554
|
recoveryRecommendation,
|
|
528
555
|
pausedByNodeId: input.state.pausedByNodeId,
|
|
529
556
|
pauseReason: input.state.pauseReason,
|
|
@@ -193,7 +193,7 @@ function blockedResult(input) {
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
function extractRunSummary(generateResult) {
|
|
196
|
-
if (generateResult.mode === "generate+validate")
|
|
196
|
+
if (generateResult.mode === "generate+basic-validate")
|
|
197
197
|
return undefined;
|
|
198
198
|
return generateResult.run;
|
|
199
199
|
}
|
|
@@ -17,6 +17,7 @@ function dependencyReadiness(task, nodes) {
|
|
|
17
17
|
if (task.depends_on.length === 0)
|
|
18
18
|
return "run";
|
|
19
19
|
const softConditionJoin = task.dependsPolicy === "all-or-condition-skip";
|
|
20
|
+
const toleratedErrors = new Set(task.failureAwareDependsOn ?? []);
|
|
20
21
|
let hasFinished = false;
|
|
21
22
|
let hasPendingUpstream = false;
|
|
22
23
|
let hasHardBlock = false;
|
|
@@ -35,6 +36,14 @@ function dependencyReadiness(task, nodes) {
|
|
|
35
36
|
continue;
|
|
36
37
|
}
|
|
37
38
|
if (dep.status === "ERROR") {
|
|
39
|
+
// AC1: an explicit read-only recovery/diagnosis/closeout node may
|
|
40
|
+
// consume a declared upstream ERROR as settled failure evidence. This
|
|
41
|
+
// does not rewrite the upstream status; it only allows the recovery
|
|
42
|
+
// node to become runnable so it can read the failure evidence.
|
|
43
|
+
if (toleratedErrors.has(depId)) {
|
|
44
|
+
hasFinished = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
38
47
|
hasHardBlock = true;
|
|
39
48
|
continue;
|
|
40
49
|
}
|
|
@@ -27,6 +27,10 @@ export const dagShellVerifyEvidenceSchema = z.object({
|
|
|
27
27
|
commandCount: z.number().int().nonnegative(),
|
|
28
28
|
commandLabels: z.array(z.string()).default([]),
|
|
29
29
|
commandTexts: z.array(z.string()).default([]),
|
|
30
|
+
/** Effective timeout applied independently to each shell command. */
|
|
31
|
+
commandTimeoutMs: z.number().int().positive().optional(),
|
|
32
|
+
/** Worst-case serial budget: commandCount * commandTimeoutMs. */
|
|
33
|
+
totalTimeoutBudgetMs: z.number().int().nonnegative().optional(),
|
|
30
34
|
finalFullRequired: z.boolean().optional(),
|
|
31
35
|
});
|
|
32
36
|
export const dagRepairArtifactGateSchema = z.object({
|
|
@@ -424,6 +428,14 @@ export const dagTaskSchema = z.object({
|
|
|
424
428
|
* every dep is FINISHED or soft condition-skip and at least one is FINISHED (OR-join)
|
|
425
429
|
*/
|
|
426
430
|
dependsPolicy: z.enum(["all", "all-or-condition-skip"]).optional(),
|
|
431
|
+
/**
|
|
432
|
+
* Explicit opt-in: upstream node ids whose ERROR terminal state this
|
|
433
|
+
* read-only node may consume as settled failure evidence. Validation
|
|
434
|
+
* restricts this to read-only Pi recovery/diagnosis/closeout nodes; writers,
|
|
435
|
+
* shell verifiers, hard gates, and dynamic/decision nodes remain fail-closed.
|
|
436
|
+
* Every id must also appear in depends_on.
|
|
437
|
+
*/
|
|
438
|
+
failureAwareDependsOn: z.array(z.string()).optional(),
|
|
427
439
|
/** Execute only when this fail-closed condition resolves true after dependencies settle. */
|
|
428
440
|
runIf: z.string().min(1).optional(),
|
|
429
441
|
complexity: dagComplexitySchema,
|
|
@@ -614,6 +614,60 @@ function validateDecisionGateTaskConfig(task, issues) {
|
|
|
614
614
|
});
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* AC1: failure-aware dependencies are an explicit, narrow opt-in. Only
|
|
619
|
+
* read-only Pi nodes acting as recovery/diagnosis/supervision/closeout
|
|
620
|
+
* (reviewer, verifier, supervisor, closeout roles) may tolerate an upstream
|
|
621
|
+
* ERROR. Writers, shell verifiers, hard/decision gates, dynamic nodes, and any
|
|
622
|
+
* node with write capability must remain fail-closed.
|
|
623
|
+
*/
|
|
624
|
+
const FAILURE_AWARE_SAFE_ROLES = new Set([
|
|
625
|
+
"reviewer",
|
|
626
|
+
"verifier",
|
|
627
|
+
"supervisor",
|
|
628
|
+
"closeout",
|
|
629
|
+
]);
|
|
630
|
+
function validateFailureAwareDependsOn(task, spec, issues) {
|
|
631
|
+
const tolerated = task.failureAwareDependsOn;
|
|
632
|
+
if (!tolerated || tolerated.length === 0)
|
|
633
|
+
return;
|
|
634
|
+
const isReadOnlyPiRecoveryNode = task.executor === "pi" &&
|
|
635
|
+
FAILURE_AWARE_SAFE_ROLES.has(task.role ?? "") &&
|
|
636
|
+
task.toolProfile !== "write" &&
|
|
637
|
+
(task.writePolicy === undefined ||
|
|
638
|
+
task.writePolicy === "read-only" ||
|
|
639
|
+
task.writePolicy === "none") &&
|
|
640
|
+
(task.writeSet ?? []).length === 0 &&
|
|
641
|
+
!task.decisionGate?.enabled &&
|
|
642
|
+
!task.dynamicExpansion &&
|
|
643
|
+
!task.dynamicReduction &&
|
|
644
|
+
!task.dynamicCondition &&
|
|
645
|
+
!task.dynamicLoopUntil;
|
|
646
|
+
if (!isReadOnlyPiRecoveryNode) {
|
|
647
|
+
issues.push({
|
|
648
|
+
type: "invalid-failure-aware-dependency",
|
|
649
|
+
message: `task ${task.id} declares failureAwareDependsOn but failure-aware dependencies are allowed only for read-only recovery/diagnosis/supervision/closeout-equivalent Pi nodes (reviewer, verifier, supervisor, closeout); writer and shell verification nodes may not tolerate upstream ERROR`,
|
|
650
|
+
});
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const dependsOn = new Set(task.depends_on);
|
|
654
|
+
const knownIds = new Set(spec.tasks.map((entry) => entry.id));
|
|
655
|
+
for (const upstreamId of tolerated) {
|
|
656
|
+
if (!knownIds.has(upstreamId)) {
|
|
657
|
+
issues.push({
|
|
658
|
+
type: "invalid-failure-aware-dependency",
|
|
659
|
+
message: `task ${task.id} failureAwareDependsOn references unknown task id ${upstreamId}`,
|
|
660
|
+
});
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (!dependsOn.has(upstreamId)) {
|
|
664
|
+
issues.push({
|
|
665
|
+
type: "invalid-failure-aware-dependency",
|
|
666
|
+
message: `task ${task.id} failure-aware dependency ${upstreamId} must be listed in depends_on`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
617
671
|
export function collectExecutorModelWarnings(spec, expectedMatrices = DEFAULT_DAG_EXECUTOR_MODELS) {
|
|
618
672
|
const warnings = [];
|
|
619
673
|
const executors = Object.keys(DEFAULT_DAG_EXECUTOR_MODELS);
|
|
@@ -704,6 +758,7 @@ export function validateDagSpec(spec) {
|
|
|
704
758
|
validateRetryPolicyTaskConfig(task, issues);
|
|
705
759
|
validateOutputProtocolTaskConfig(task, issues);
|
|
706
760
|
validateProjectGovernanceTaskConfig(task, spec, issues);
|
|
761
|
+
validateFailureAwareDependsOn(task, spec, issues);
|
|
707
762
|
}
|
|
708
763
|
validateSameRankWriteSetConflicts(spec, ranks, issues);
|
|
709
764
|
validateSameRankAgentAttributionRisks(spec, ranks, issues);
|