@tea-agent/loop-agent 0.31.0 → 0.32.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 +1 -1
- package/CHANGELOG.md +60 -0
- package/README.md +1 -1
- package/dist/executors/dag-pi-executor.js +69 -2
- package/dist/executors/pi-sdk-executor.js +61 -0
- package/dist/executors/shell-executor.js +136 -79
- package/dist/shared/operator/capabilities.js +22 -1
- package/dist/shared/operator/command-lifecycle.js +94 -0
- package/dist/shared/operator/index.js +1 -0
- package/dist/worker/cli.js +10 -24
- package/dist/worker/console/doctor.js +11 -4
- package/dist/worker/console/observe-health-match.js +18 -15
- package/dist/worker/console/operator-actions.js +2 -1
- package/dist/worker/feature/acceptance-policy.js +227 -0
- package/dist/worker/feature/decision-loader.js +1 -1
- package/dist/worker/feature/next-action.js +56 -6
- package/dist/worker/feature/profile-schema.js +3 -0
- package/dist/worker/feature/reducer.js +1 -0
- package/dist/worker/feature/review.js +72 -8
- package/dist/worker/feature/scaffold.js +14 -0
- package/dist/worker/loop-agent/controller-protocol.js +143 -0
- package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
- package/dist/worker/materialize/harness-task-lineage.js +220 -0
- package/dist/worker/materialize/harness-task-materializer.js +350 -80
- package/dist/worker/observability/progress-composite.js +1 -0
- package/dist/worker/observability/read-model.js +66 -19
- package/dist/worker/pool/attempt-identity.js +41 -0
- package/dist/worker/pool/attempt-lease.js +184 -0
- package/dist/worker/pool/attempt-transition.js +210 -0
- package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
- package/dist/worker/pool/begin-attempt.js +35 -0
- package/dist/worker/pool/failure-routing.js +49 -0
- package/dist/worker/pool/recovery-decision.js +163 -0
- package/dist/worker/pool/run-owner-store.js +126 -0
- package/dist/worker/pool/run-store.js +32 -46
- package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
- package/dist/worker/pool/state-projection.js +57 -0
- package/dist/worker/run-task/run-task.js +31 -4
- package/dist/worker/runner/run-ready.js +64 -14
- package/dist/worker/runner/single-task-attempt.js +42 -13
- package/dist/worker/task-graph/acceptance-schema.js +3 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +162 -7
- package/dist/workflows/dag/backend-test-result-contract.js +105 -67
- package/dist/workflows/dag/backend-test-scenario-param.js +92 -30
- package/dist/workflows/dag/backend-test-writer-completeness.js +55 -0
- package/dist/workflows/dag/init-hybrid.js +44 -47
- package/dist/workflows/dag/rerun-task.js +86 -0
- package/docs/README.md +3 -1
- package/docs/architecture/evolution.md +1 -1
- package/docs/operations/README.md +1 -0
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-worker-production-readiness-checklist.md +45 -0
- package/docs/templates/backend-test-dag.json +40 -60
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +1 -1
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +11 -0
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { OPERATOR_ERROR_CODES_V1, OPERATOR_OUTCOMES_V1 } from "./registry.js";
|
|
3
|
+
import { WORKER_ADAPTER_PROTOCOL_RANGE } from "../../worker/loop-agent/controller-protocol.js";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const PACKAGE_VERSION = require("../../../package.json").version ?? "0.0.0";
|
|
6
|
+
/** Keep aligned with WORKER_CONTROLLER_REQUIREMENTS_V1 in controller-protocol.ts */
|
|
7
|
+
const CONTROLLER_PROTOCOL_CAPABILITIES = [
|
|
8
|
+
"task-advance-managed-contract-v2",
|
|
9
|
+
"worker-association-v1",
|
|
10
|
+
"events-jsonl-v1",
|
|
11
|
+
"document-index-closure-v1",
|
|
12
|
+
"worker-attempt-owner-v1",
|
|
13
|
+
"worker-attempt-revision-v1",
|
|
14
|
+
"recovery-decision-v2",
|
|
15
|
+
];
|
|
2
16
|
const officialAction = (action, cli, kind, coverage) => ({
|
|
3
17
|
action,
|
|
4
18
|
cli,
|
|
@@ -1348,6 +1362,13 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1348
1362
|
canonicalizerVersion: 1,
|
|
1349
1363
|
taskConfigSchemaVersion: 1,
|
|
1350
1364
|
},
|
|
1365
|
+
controllerProtocol: {
|
|
1366
|
+
schemaVersion: 1,
|
|
1367
|
+
controllerProtocolVersion: 1,
|
|
1368
|
+
workerAdapterProtocolRange: WORKER_ADAPTER_PROTOCOL_RANGE,
|
|
1369
|
+
capabilities: CONTROLLER_PROTOCOL_CAPABILITIES,
|
|
1370
|
+
packageVersion: PACKAGE_VERSION,
|
|
1371
|
+
},
|
|
1351
1372
|
actions,
|
|
1352
1373
|
};
|
|
1353
1374
|
}
|
|
@@ -2215,7 +2236,7 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
2215
2236
|
coverage: "excluded",
|
|
2216
2237
|
action: null,
|
|
2217
2238
|
source: "agent-worker",
|
|
2218
|
-
exclusionReason: "
|
|
2239
|
+
exclusionReason: "Removed (Wave 5): OBSERVE_SERVE_REMOVED + exit 2; use agent-worker console + /inspect/.",
|
|
2219
2240
|
},
|
|
2220
2241
|
{
|
|
2221
2242
|
command: "agent-worker observe snapshot",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public command lifecycle machine facts (Phase 7 Track B).
|
|
3
|
+
* Implementation remains in CLI modules; this registry is the shared tombstone /
|
|
4
|
+
* deprecation source for capabilities, drift checks, and operator envelopes.
|
|
5
|
+
*/
|
|
6
|
+
const COMMAND_LIFECYCLE_REGISTRY = {
|
|
7
|
+
"agent-worker observe serve": {
|
|
8
|
+
command: "agent-worker observe serve",
|
|
9
|
+
product: "agent-worker",
|
|
10
|
+
lifecycle: "removed",
|
|
11
|
+
replacement: "agent-worker console",
|
|
12
|
+
removedVersion: "0.19.0",
|
|
13
|
+
diagnosticCode: "OBSERVE_SERVE_REMOVED",
|
|
14
|
+
removedExitCode: 2,
|
|
15
|
+
docsSummary: "Independent observe HTTP server removed; use unified Operator Console /inspect/.",
|
|
16
|
+
operatorCategory: "observe",
|
|
17
|
+
},
|
|
18
|
+
"agent-worker console": {
|
|
19
|
+
command: "agent-worker console",
|
|
20
|
+
product: "agent-worker",
|
|
21
|
+
lifecycle: "official",
|
|
22
|
+
introducedVersion: "0.19.0",
|
|
23
|
+
docsSummary: "Official unified Operator Console (Operate + Inspect on default loopback :8790).",
|
|
24
|
+
operatorCategory: "console",
|
|
25
|
+
},
|
|
26
|
+
"agent-worker feature review": {
|
|
27
|
+
command: "agent-worker feature review",
|
|
28
|
+
product: "agent-worker",
|
|
29
|
+
lifecycle: "official",
|
|
30
|
+
docsSummary: "Project Feature readiness, acceptance gaps, and next action.",
|
|
31
|
+
operatorCategory: "feature",
|
|
32
|
+
},
|
|
33
|
+
"agent-worker feature advance": {
|
|
34
|
+
command: "agent-worker feature advance",
|
|
35
|
+
product: "agent-worker",
|
|
36
|
+
lifecycle: "official",
|
|
37
|
+
docsSummary: "ADR 0007 orchestration: verify-final → delivery → closeout.",
|
|
38
|
+
operatorCategory: "feature",
|
|
39
|
+
},
|
|
40
|
+
"agent-worker feature verify-final": {
|
|
41
|
+
command: "agent-worker feature verify-final",
|
|
42
|
+
product: "agent-worker",
|
|
43
|
+
lifecycle: "official",
|
|
44
|
+
docsSummary: "Canonical Final Verification writer (ADR 0007); only authority for coverage facts.",
|
|
45
|
+
operatorCategory: "feature",
|
|
46
|
+
},
|
|
47
|
+
"agent-worker batch run-ready": {
|
|
48
|
+
command: "agent-worker batch run-ready",
|
|
49
|
+
product: "agent-worker",
|
|
50
|
+
lifecycle: "official",
|
|
51
|
+
docsSummary: "Execute Ready Feature tasks with Task Pool attempt lease.",
|
|
52
|
+
operatorCategory: "batch",
|
|
53
|
+
},
|
|
54
|
+
"agent-worker task reconcile": {
|
|
55
|
+
command: "agent-worker task reconcile",
|
|
56
|
+
product: "agent-worker",
|
|
57
|
+
lifecycle: "official",
|
|
58
|
+
docsSummary: "Reconcile partial/orphan Worker attempt facts without deleting evidence.",
|
|
59
|
+
operatorCategory: "task",
|
|
60
|
+
},
|
|
61
|
+
"agent-worker pool doctor": {
|
|
62
|
+
command: "agent-worker pool doctor",
|
|
63
|
+
product: "agent-worker",
|
|
64
|
+
lifecycle: "official",
|
|
65
|
+
docsSummary: "Diagnose Task Pool / lease / ownership consistency.",
|
|
66
|
+
operatorCategory: "pool",
|
|
67
|
+
},
|
|
68
|
+
"loop-agent operator capabilities": {
|
|
69
|
+
command: "loop-agent operator capabilities",
|
|
70
|
+
product: "loop-agent",
|
|
71
|
+
lifecycle: "official",
|
|
72
|
+
docsSummary: "Publish operator + controllerProtocol envelope for Console/Worker negotiation.",
|
|
73
|
+
operatorCategory: "operator",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export function listCommandLifecycleEntries() {
|
|
77
|
+
return Object.values(COMMAND_LIFECYCLE_REGISTRY);
|
|
78
|
+
}
|
|
79
|
+
export function normalizeCommandLifecycleKey(command) {
|
|
80
|
+
return command.trim().replace(/\s+/g, " ");
|
|
81
|
+
}
|
|
82
|
+
export function resolveCommandLifecycle(command) {
|
|
83
|
+
const key = normalizeCommandLifecycleKey(command);
|
|
84
|
+
return COMMAND_LIFECYCLE_REGISTRY[key];
|
|
85
|
+
}
|
|
86
|
+
export function isRemovedCommand(command) {
|
|
87
|
+
return resolveCommandLifecycle(command)?.lifecycle === "removed";
|
|
88
|
+
}
|
|
89
|
+
export function listOfficialCommands(product) {
|
|
90
|
+
return listCommandLifecycleEntries()
|
|
91
|
+
.filter((entry) => entry.lifecycle === "official" &&
|
|
92
|
+
(product === undefined || entry.product === product))
|
|
93
|
+
.map((entry) => entry.command);
|
|
94
|
+
}
|
package/dist/worker/cli.js
CHANGED
|
@@ -13,7 +13,6 @@ import { createProgressReporter } from "./progress-reporter.js";
|
|
|
13
13
|
import { createCompositeProgressReporter } from "./observability/progress-composite.js";
|
|
14
14
|
import { createRoutedWorkerEventStore } from "./observability/event-store.js";
|
|
15
15
|
import { buildGlobalSnapshot } from "./observability/read-model.js";
|
|
16
|
-
import { createObserveServer } from "./observe/server.js";
|
|
17
16
|
import { diagnoseTaskPoolStates, formatDoctorHuman, loadOperatorMapping, } from "./pool/doctor.js";
|
|
18
17
|
import { migrateTaskPoolStates } from "./pool/migrate-state.js";
|
|
19
18
|
import { markTaskPoolFailed, reconcileTaskPoolAbandon, } from "./pool/reconcile.js";
|
|
@@ -690,29 +689,16 @@ export function buildAgentWorkerProgram() {
|
|
|
690
689
|
});
|
|
691
690
|
observe
|
|
692
691
|
.command("serve")
|
|
693
|
-
.
|
|
694
|
-
.option("--port <port>", "HTTP port", "8787")
|
|
695
|
-
.option("--host <host>", "Bind host (
|
|
696
|
-
.option("--debug", "
|
|
697
|
-
.description("
|
|
698
|
-
.action(
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
port: Number.parseInt(options.port, 10),
|
|
704
|
-
debug: options.debug === true,
|
|
705
|
-
});
|
|
706
|
-
process.stderr.write("[OBSERVE_SERVE_DEPRECATED] agent-worker observe serve 作为独立看板入口将在兼容窗口后下线。\n" +
|
|
707
|
-
"请改用:agent-worker console serve --repo <repo> --port 8790\n" +
|
|
708
|
-
"统一 Operator Console 已包含原 Observe 的只读检视能力:http://127.0.0.1:8790/\n");
|
|
709
|
-
process.stdout.write(`${server.url}\n`);
|
|
710
|
-
await new Promise((resolve) => {
|
|
711
|
-
const shutdown = () => resolve();
|
|
712
|
-
process.once("SIGINT", shutdown);
|
|
713
|
-
process.once("SIGTERM", shutdown);
|
|
714
|
-
});
|
|
715
|
-
await server.close();
|
|
692
|
+
.option("--repo <repo-root>", "Target repo root (ignored; command removed)")
|
|
693
|
+
.option("--port <port>", "HTTP port (ignored; command removed)", "8787")
|
|
694
|
+
.option("--host <host>", "Bind host (ignored; command removed)", "127.0.0.1")
|
|
695
|
+
.option("--debug", "Ignored; command removed")
|
|
696
|
+
.description("Removed: independent observe HTTP server (use agent-worker console + /inspect/)")
|
|
697
|
+
.action(() => {
|
|
698
|
+
process.stderr.write("[OBSERVE_SERVE_REMOVED] agent-worker observe serve 已下线。\n" +
|
|
699
|
+
"请改用:agent-worker console --repo <repo> --port 8790\n" +
|
|
700
|
+
"只读检视:http://127.0.0.1:8790/inspect/\n");
|
|
701
|
+
process.exitCode = 2;
|
|
716
702
|
});
|
|
717
703
|
observe
|
|
718
704
|
.command("snapshot")
|
|
@@ -161,12 +161,19 @@ export async function runConsoleDoctor(options) {
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
const inspect = await probeInspect(options.consoleBaseUrl, options.fetchImpl ?? fetch);
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// Wave 5: do not probe retired independent observe serve (8787).
|
|
165
|
+
// Optional observeBaseUrl only accepted when it is the same origin as Console.
|
|
166
|
+
const consoleOrigin = options.consoleBaseUrl?.replace(/\/+$/, "");
|
|
167
|
+
const observeOrigin = options.observeBaseUrl?.replace(/\/+$/, "");
|
|
168
|
+
const observe = observeOrigin &&
|
|
169
|
+
consoleOrigin &&
|
|
170
|
+
observeOrigin === consoleOrigin
|
|
171
|
+
? await probeObserve(observeOrigin, fingerprint, options.fetchImpl ?? fetch)
|
|
166
172
|
: {
|
|
167
173
|
status: "unavailable",
|
|
168
174
|
url: `${DEFAULT_OBSERVE_BASE_URL}/api/health`,
|
|
169
|
-
message: "
|
|
175
|
+
message: "独立 observe serve 已下线;健康以统一 Operator Console / Inspect 为准",
|
|
176
|
+
startCommand: OBSERVE_START_COMMAND,
|
|
170
177
|
};
|
|
171
178
|
const pi = await probePiReadiness({
|
|
172
179
|
override: options.piReadinessProbe,
|
|
@@ -204,7 +211,7 @@ export function formatConsoleDoctorHuman(report) {
|
|
|
204
211
|
];
|
|
205
212
|
if (report.observe.status === "unavailable" ||
|
|
206
213
|
report.observe.status === "mismatch") {
|
|
207
|
-
lines.push(`observe.startCommand: ${report.observe.startCommand ?? OBSERVE_START_COMMAND}`);
|
|
214
|
+
lines.push(`observe.startCommand: ${report.observe.startCommand ?? OBSERVE_START_COMMAND} (use console + /inspect/)`);
|
|
208
215
|
}
|
|
209
216
|
if (report.siblingLoopAgentBin) {
|
|
210
217
|
lines.push(`siblingLoopAgentBin: ${report.siblingLoopAgentBin}`);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Observe
|
|
3
|
-
*
|
|
2
|
+
* Observe link capability mapping + retired cross-process health-match stubs.
|
|
3
|
+
* Wave 5: independent observe serve (8787) is removed; doctor uses Console surface.
|
|
4
|
+
* capabilityForObserveTarget remains for Inspect deep-links / night-jobs.
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
-
export const
|
|
6
|
+
/** @deprecated Wave 5 — observe serve removed; use agent-worker console */
|
|
7
|
+
export const OBSERVE_START_COMMAND = "agent-worker console --repo . --port 8790";
|
|
8
|
+
/** @deprecated Wave 5 — default unified Console origin (not independent Observe) */
|
|
9
|
+
export const DEFAULT_OBSERVE_BASE_URL = "http://127.0.0.1:8790";
|
|
7
10
|
export function capabilityForObserveTarget(target) {
|
|
8
11
|
switch (target.kind) {
|
|
9
12
|
case "dag":
|
|
@@ -25,8 +28,8 @@ export function capabilityForObserveTarget(target) {
|
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
|
-
* Classify
|
|
29
|
-
* Pure function — no I/O.
|
|
31
|
+
* Classify Console/Inspect /api/health body against local repo fingerprint + capability.
|
|
32
|
+
* Pure function — no I/O. Prefer unified Console origin (8790), not retired 8787 Observe.
|
|
30
33
|
*/
|
|
31
34
|
export function classifyObserveHealth(input) {
|
|
32
35
|
const url = input.healthUrl ?? `${DEFAULT_OBSERVE_BASE_URL}/api/health`;
|
|
@@ -36,7 +39,7 @@ export function classifyObserveHealth(input) {
|
|
|
36
39
|
return {
|
|
37
40
|
status: "unavailable",
|
|
38
41
|
url,
|
|
39
|
-
message: "
|
|
42
|
+
message: "Operator surface health body missing or not JSON object",
|
|
40
43
|
startCommand,
|
|
41
44
|
body,
|
|
42
45
|
localFingerprint: input.localFingerprint,
|
|
@@ -48,7 +51,7 @@ export function classifyObserveHealth(input) {
|
|
|
48
51
|
return {
|
|
49
52
|
status: "unavailable",
|
|
50
53
|
url,
|
|
51
|
-
message: "
|
|
54
|
+
message: "Operator surface health ok !== true",
|
|
52
55
|
startCommand,
|
|
53
56
|
body,
|
|
54
57
|
localFingerprint: input.localFingerprint,
|
|
@@ -60,7 +63,7 @@ export function classifyObserveHealth(input) {
|
|
|
60
63
|
return {
|
|
61
64
|
status: "mismatch",
|
|
62
65
|
url,
|
|
63
|
-
message: `
|
|
66
|
+
message: `Operator surface schemaVersion mismatch (expected 1, got ${String(health.schemaVersion)})`,
|
|
64
67
|
startCommand,
|
|
65
68
|
body,
|
|
66
69
|
localFingerprint: input.localFingerprint,
|
|
@@ -75,7 +78,7 @@ export function classifyObserveHealth(input) {
|
|
|
75
78
|
return {
|
|
76
79
|
status: "mismatch",
|
|
77
80
|
url,
|
|
78
|
-
message: "
|
|
81
|
+
message: "Operator surface repoFingerprint does not match local worktree",
|
|
79
82
|
startCommand,
|
|
80
83
|
body,
|
|
81
84
|
localFingerprint: input.localFingerprint,
|
|
@@ -91,7 +94,7 @@ export function classifyObserveHealth(input) {
|
|
|
91
94
|
return {
|
|
92
95
|
status: "mismatch",
|
|
93
96
|
url,
|
|
94
|
-
message: `
|
|
97
|
+
message: `Operator surface missing route capability: ${input.requiredCapability}`,
|
|
95
98
|
startCommand,
|
|
96
99
|
body,
|
|
97
100
|
localFingerprint: input.localFingerprint,
|
|
@@ -104,7 +107,7 @@ export function classifyObserveHealth(input) {
|
|
|
104
107
|
return {
|
|
105
108
|
status: "match",
|
|
106
109
|
url,
|
|
107
|
-
message: "
|
|
110
|
+
message: "Operator surface health matches local repo and required capability",
|
|
108
111
|
startCommand,
|
|
109
112
|
body,
|
|
110
113
|
localFingerprint: input.localFingerprint,
|
|
@@ -117,7 +120,7 @@ export function classifyObserveHealth(input) {
|
|
|
117
120
|
return {
|
|
118
121
|
status: "match",
|
|
119
122
|
url,
|
|
120
|
-
message: "
|
|
123
|
+
message: "Operator surface health matches local repo",
|
|
121
124
|
startCommand,
|
|
122
125
|
body,
|
|
123
126
|
localFingerprint: input.localFingerprint,
|
|
@@ -126,8 +129,8 @@ export function classifyObserveHealth(input) {
|
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
/**
|
|
129
|
-
* Probe
|
|
130
|
-
* Network failure → unavailable
|
|
132
|
+
* Probe /api/health and classify against local fingerprint.
|
|
133
|
+
* Network failure → unavailable. Prefer Console origin; independent Observe serve is removed.
|
|
131
134
|
*/
|
|
132
135
|
export async function probeAndClassifyObserve(input) {
|
|
133
136
|
const base = input.baseUrl.replace(/\/+$/, "");
|
|
@@ -537,7 +537,8 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
537
537
|
}
|
|
538
538
|
case "observeLink": {
|
|
539
539
|
const consoleOrigin = ctx.getConsoleOrigin?.();
|
|
540
|
-
|
|
540
|
+
// Wave 5: prefer unified Console origin; do not fall back to retired 8787 Observe.
|
|
541
|
+
const baseUrl = consoleOrigin ?? ctx.observeBaseUrl ?? DEFAULT_OBSERVE_BASE_URL;
|
|
541
542
|
const dagRunId = str(p.dagRunId);
|
|
542
543
|
const taskId = str(p.taskId);
|
|
543
544
|
const featureId = str(p.featureId);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* ADR 0007: only `agent-worker feature verify-final` is the canonical writer for
|
|
4
|
+
* Feature Verification Bundle, QA pass evidence, and Final Verification Record.
|
|
5
|
+
* Task Pool Done, DAG verify-shell receipts, and packet FINAL-VERIFY-* smoke
|
|
6
|
+
* nodes are inputs only — they must never be treated as coverage writers.
|
|
7
|
+
*/
|
|
8
|
+
export const CANONICAL_FINAL_VERIFICATION_AUTHORITY = "feature-verify-final";
|
|
9
|
+
export const DEFAULT_VERIFY_FINAL_COMMAND = "agent-worker feature verify-final";
|
|
10
|
+
const deliverySequenceSchema = z.array(z.enum(["verify-final", "delivery", "closeout"]));
|
|
11
|
+
export const acceptancePolicySchema = z
|
|
12
|
+
.object({
|
|
13
|
+
schema_version: z.literal(1),
|
|
14
|
+
authority: z.string().min(1).optional(),
|
|
15
|
+
independent_qa_required: z.boolean().optional(),
|
|
16
|
+
coverage_writer: z.string().min(1).optional(),
|
|
17
|
+
final_verification: z
|
|
18
|
+
.object({
|
|
19
|
+
command: z.string().min(1),
|
|
20
|
+
})
|
|
21
|
+
.strict()
|
|
22
|
+
.optional(),
|
|
23
|
+
delivery_sequence: deliverySequenceSchema.optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
const DEFAULT_POLICY = {
|
|
27
|
+
schema_version: 1,
|
|
28
|
+
authority: CANONICAL_FINAL_VERIFICATION_AUTHORITY,
|
|
29
|
+
independent_qa_required: true,
|
|
30
|
+
coverage_writer: CANONICAL_FINAL_VERIFICATION_AUTHORITY,
|
|
31
|
+
final_verification: {
|
|
32
|
+
command: DEFAULT_VERIFY_FINAL_COMMAND,
|
|
33
|
+
},
|
|
34
|
+
delivery_sequence: ["verify-final", "delivery", "closeout"],
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Parse optional `acceptance_policy` from a Feature Packet object (typically
|
|
38
|
+
* `feature.yaml`). Unknown fields are rejected; missing fields receive ADR 0007
|
|
39
|
+
* defaults without inventing covered evidence.
|
|
40
|
+
*/
|
|
41
|
+
export function parseAcceptancePolicy(packet) {
|
|
42
|
+
if (!packet || typeof packet !== "object")
|
|
43
|
+
return { ...DEFAULT_POLICY };
|
|
44
|
+
const raw = packet.acceptance_policy;
|
|
45
|
+
if (raw === undefined || raw === null)
|
|
46
|
+
return { ...DEFAULT_POLICY };
|
|
47
|
+
const parsed = acceptancePolicySchema.safeParse(raw);
|
|
48
|
+
if (!parsed.success)
|
|
49
|
+
return { ...DEFAULT_POLICY };
|
|
50
|
+
return normalizeAcceptancePolicy(parsed.data);
|
|
51
|
+
}
|
|
52
|
+
/** Apply canonical defaults while preserving explicit packet overrides. */
|
|
53
|
+
export function normalizeAcceptancePolicy(policy) {
|
|
54
|
+
return {
|
|
55
|
+
schema_version: 1,
|
|
56
|
+
authority: policy.authority?.trim() || DEFAULT_POLICY.authority,
|
|
57
|
+
independent_qa_required: policy.independent_qa_required ?? DEFAULT_POLICY.independent_qa_required,
|
|
58
|
+
coverage_writer: policy.coverage_writer?.trim() || DEFAULT_POLICY.coverage_writer,
|
|
59
|
+
final_verification: {
|
|
60
|
+
command: policy.final_verification?.command?.trim() ||
|
|
61
|
+
DEFAULT_POLICY.final_verification.command,
|
|
62
|
+
},
|
|
63
|
+
delivery_sequence: policy.delivery_sequence && policy.delivery_sequence.length > 0
|
|
64
|
+
? [...policy.delivery_sequence]
|
|
65
|
+
: [...DEFAULT_POLICY.delivery_sequence],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Extract additive policy fields that may exist on raw acceptance YAML items. */
|
|
69
|
+
export function extractAcceptanceItemPolicyFields(rawItem) {
|
|
70
|
+
if (!rawItem || typeof rawItem !== "object") {
|
|
71
|
+
return { requiredEvidenceKinds: [], expectedTaskRefs: [] };
|
|
72
|
+
}
|
|
73
|
+
const verification = rawItem.verification;
|
|
74
|
+
if (!verification || typeof verification !== "object") {
|
|
75
|
+
return { requiredEvidenceKinds: [], expectedTaskRefs: [] };
|
|
76
|
+
}
|
|
77
|
+
const record = verification;
|
|
78
|
+
const authority = typeof record.authority === "string" && record.authority.trim().length > 0
|
|
79
|
+
? record.authority.trim()
|
|
80
|
+
: undefined;
|
|
81
|
+
const requiredEvidenceKinds = readStringArray(record.required_evidence_kinds ?? record.required_evidence);
|
|
82
|
+
const expectedTaskRefs = readStringArray(record.expected_task_refs);
|
|
83
|
+
return { authority, requiredEvidenceKinds, expectedTaskRefs };
|
|
84
|
+
}
|
|
85
|
+
export function indexRawAcceptanceItems(rawAcceptance) {
|
|
86
|
+
const result = new Map();
|
|
87
|
+
if (!rawAcceptance || typeof rawAcceptance !== "object")
|
|
88
|
+
return result;
|
|
89
|
+
const items = rawAcceptance.acceptance;
|
|
90
|
+
if (!Array.isArray(items))
|
|
91
|
+
return result;
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
if (!item || typeof item !== "object")
|
|
94
|
+
continue;
|
|
95
|
+
const id = item.id;
|
|
96
|
+
if (typeof id === "string" && id.length > 0)
|
|
97
|
+
result.set(id, item);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
export function buildAcceptanceGaps(input) {
|
|
102
|
+
const gaps = [];
|
|
103
|
+
const verifyFinalCommand = buildVerifyFinalCommand(input.policy, input.featureDir, input.repoRoot);
|
|
104
|
+
const allTasksDone = input.tasks.length > 0 &&
|
|
105
|
+
input.tasks.every((task) => task.status === "Done");
|
|
106
|
+
for (const item of input.acceptanceItems) {
|
|
107
|
+
if (item.priority !== "must")
|
|
108
|
+
continue;
|
|
109
|
+
const fact = input.coverage.find((entry) => entry.acId === item.id);
|
|
110
|
+
if (!fact || fact.status === "covered" || fact.status === "waived")
|
|
111
|
+
continue;
|
|
112
|
+
const rawItem = input.rawByAcId?.get(item.id);
|
|
113
|
+
const policyFields = extractAcceptanceItemPolicyFields(rawItem);
|
|
114
|
+
const authority = resolveItemAuthority(input.policy, policyFields);
|
|
115
|
+
const blockers = collectBlockers({
|
|
116
|
+
policy: input.policy,
|
|
117
|
+
policyFields,
|
|
118
|
+
item,
|
|
119
|
+
authority,
|
|
120
|
+
});
|
|
121
|
+
const status = normalizeGapStatus(fact.status);
|
|
122
|
+
const missingEvidenceKinds = resolveMissingEvidenceKinds({
|
|
123
|
+
policyFields,
|
|
124
|
+
item,
|
|
125
|
+
fact,
|
|
126
|
+
status,
|
|
127
|
+
});
|
|
128
|
+
gaps.push({
|
|
129
|
+
acId: item.id,
|
|
130
|
+
status,
|
|
131
|
+
missingEvidenceKinds,
|
|
132
|
+
expectedTaskRefs: item.verification.expected_task_refs,
|
|
133
|
+
authority,
|
|
134
|
+
nextCommand: projectGapNextCommand({
|
|
135
|
+
status,
|
|
136
|
+
blockers,
|
|
137
|
+
allTasksDone,
|
|
138
|
+
verifyFinalCommand,
|
|
139
|
+
featureDir: input.featureDir,
|
|
140
|
+
repoRoot: input.repoRoot,
|
|
141
|
+
blockedBy: fact.blockedBy,
|
|
142
|
+
}),
|
|
143
|
+
blockers,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return gaps;
|
|
147
|
+
}
|
|
148
|
+
function resolveItemAuthority(policy, policyFields) {
|
|
149
|
+
return policyFields.authority ?? policy.authority ?? CANONICAL_FINAL_VERIFICATION_AUTHORITY;
|
|
150
|
+
}
|
|
151
|
+
function collectBlockers(input) {
|
|
152
|
+
const blockers = [];
|
|
153
|
+
if (!input.policyFields.authority && input.policy.independent_qa_required) {
|
|
154
|
+
blockers.push("missing authority: required AC has no verification.authority");
|
|
155
|
+
}
|
|
156
|
+
if (input.policyFields.expectedTaskRefs.length === 0 &&
|
|
157
|
+
(input.policyFields.requiredEvidenceKinds.length === 0 &&
|
|
158
|
+
(input.item.verification.implementation_task_refs?.length ?? 0) === 0 &&
|
|
159
|
+
(input.item.verification.verification_task_refs?.length ?? 0) === 0)) {
|
|
160
|
+
blockers.push("missing producer: no expected_task_refs or dual-coverage refs");
|
|
161
|
+
}
|
|
162
|
+
if (input.policy.independent_qa_required &&
|
|
163
|
+
input.policyFields.requiredEvidenceKinds.length === 0 &&
|
|
164
|
+
(input.item.verification.required_evidence?.length ?? 0) === 0 &&
|
|
165
|
+
input.authority === CANONICAL_FINAL_VERIFICATION_AUTHORITY) {
|
|
166
|
+
blockers.push("missing evidence kinds: declare verification.required_evidence_kinds or required_evidence");
|
|
167
|
+
}
|
|
168
|
+
if (input.authority !== CANONICAL_FINAL_VERIFICATION_AUTHORITY &&
|
|
169
|
+
input.policy.coverage_writer === CANONICAL_FINAL_VERIFICATION_AUTHORITY) {
|
|
170
|
+
blockers.push(`non-canonical authority ${input.authority}; ADR 0007 requires ${CANONICAL_FINAL_VERIFICATION_AUTHORITY}`);
|
|
171
|
+
}
|
|
172
|
+
return blockers;
|
|
173
|
+
}
|
|
174
|
+
function resolveMissingEvidenceKinds(input) {
|
|
175
|
+
if (input.status === "blocked")
|
|
176
|
+
return [];
|
|
177
|
+
const declared = [
|
|
178
|
+
...input.policyFields.requiredEvidenceKinds,
|
|
179
|
+
...(input.item.verification.required_evidence ?? []),
|
|
180
|
+
];
|
|
181
|
+
const unique = [...new Set(declared)];
|
|
182
|
+
if (unique.length > 0 && input.fact.evidence.length === 0)
|
|
183
|
+
return unique;
|
|
184
|
+
if (unique.length === 0 && input.status !== "missing") {
|
|
185
|
+
return ["canonical-qa-evidence"];
|
|
186
|
+
}
|
|
187
|
+
return unique;
|
|
188
|
+
}
|
|
189
|
+
function normalizeGapStatus(status) {
|
|
190
|
+
if (status === "awaiting-verification")
|
|
191
|
+
return "awaiting-verification";
|
|
192
|
+
if (status === "blocked")
|
|
193
|
+
return "blocked";
|
|
194
|
+
if (status === "partial")
|
|
195
|
+
return "partial";
|
|
196
|
+
return "missing";
|
|
197
|
+
}
|
|
198
|
+
function projectGapNextCommand(input) {
|
|
199
|
+
if (input.status === "blocked") {
|
|
200
|
+
if (input.blockedBy.length > 0) {
|
|
201
|
+
const taskId = input.blockedBy[0];
|
|
202
|
+
return `agent-worker feature review --feature-dir ${quote(input.featureDir)} --repo ${quote(input.repoRoot)}`;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!input.allTasksDone) {
|
|
207
|
+
return `agent-worker batch run-ready --feature-dir ${quote(input.featureDir)} --repo ${quote(input.repoRoot)}`;
|
|
208
|
+
}
|
|
209
|
+
if (input.status === "partial" ||
|
|
210
|
+
input.status === "awaiting-verification" ||
|
|
211
|
+
input.status === "missing") {
|
|
212
|
+
return input.verifyFinalCommand;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function buildVerifyFinalCommand(policy, featureDir, repoRoot) {
|
|
217
|
+
const base = policy.final_verification?.command?.trim() || DEFAULT_VERIFY_FINAL_COMMAND;
|
|
218
|
+
return `${base} --feature-dir ${quote(featureDir)} --repo ${quote(repoRoot)}`;
|
|
219
|
+
}
|
|
220
|
+
function readStringArray(value) {
|
|
221
|
+
if (!Array.isArray(value))
|
|
222
|
+
return [];
|
|
223
|
+
return value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
224
|
+
}
|
|
225
|
+
function quote(value) {
|
|
226
|
+
return JSON.stringify(value);
|
|
227
|
+
}
|
|
@@ -27,7 +27,7 @@ export async function loadFeatureDecisionModels(repoRoot) {
|
|
|
27
27
|
function degradeExisting(feature, warnings) {
|
|
28
28
|
return { ...feature, status: "needs_action", statusLabel: "需处理", blockingItems: [...warnings.map((warning) => ({ type: "projection_warning", message: warning, evidence: [] })), ...feature.blockingItems], nextAction: { kind: "repair_projection", label: "修复损坏的 Feature / Task Pool facts 后重新审阅" }, projectionWarnings: [...new Set([...feature.projectionWarnings, ...warnings])] };
|
|
29
29
|
}
|
|
30
|
-
function degradedFeature(featureId, warning) { return { schemaVersion: 1, featureId, status: "needs_action", statusLabel: "需处理", summary: { tasksTotal: 0, tasksSucceeded: 0, tasksFailed: 0, tasksReady: 0, tasksBlocked: 0, requiredAcTotal: 0, requiredAcCovered: 0 }, riskSummary: { high: 1, medium: 0, low: 0 }, blockingItems: [{ type: "projection_warning", message: warning, evidence: [] }], followUps: { pending: [], actionCards: [], resolvedFailureTaskIds: [] }, tasks: [], acceptanceCoverage: [], nextAction: { kind: "repair_projection", label: "修复损坏事实后重新审阅" }, alternativeActions: [], evidence: { morningReport: null, observeSnapshot: null, delivery: null, closeout: null }, projectionWarnings: [warning] }; }
|
|
30
|
+
function degradedFeature(featureId, warning) { return { schemaVersion: 1, featureId, status: "needs_action", statusLabel: "需处理", summary: { tasksTotal: 0, tasksSucceeded: 0, tasksFailed: 0, tasksReady: 0, tasksBlocked: 0, requiredAcTotal: 0, requiredAcCovered: 0 }, riskSummary: { high: 1, medium: 0, low: 0 }, blockingItems: [{ type: "projection_warning", message: warning, evidence: [] }], followUps: { pending: [], actionCards: [], resolvedFailureTaskIds: [] }, tasks: [], acceptanceCoverage: [], acceptanceGaps: [], nextAction: { kind: "repair_projection", label: "修复损坏事实后重新审阅" }, alternativeActions: [], evidence: { morningReport: null, observeSnapshot: null, delivery: null, closeout: null }, projectionWarnings: [warning] }; }
|
|
31
31
|
async function auditTaskPoolFacts(repoRoot) {
|
|
32
32
|
const warnings = [];
|
|
33
33
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveWorkerRecoveryDecision } from "../pool/recovery-decision.js";
|
|
1
2
|
export function projectNextAction(input, status) {
|
|
2
3
|
const featureArgs = `--feature-dir ${quote(input.featureDir)} --repo ${quote(input.repoRoot)}`;
|
|
3
4
|
const resolvedFailures = new Set(input.resolvedFailureTaskIds ?? []);
|
|
@@ -31,20 +32,59 @@ export function projectNextAction(input, status) {
|
|
|
31
32
|
}
|
|
32
33
|
const failed = input.tasks.find((task) => task.status === "Failed" && !resolvedFailures.has(task.taskId));
|
|
33
34
|
if (failed) {
|
|
34
|
-
|
|
35
|
+
const decision = resolveWorkerRecoveryDecision({
|
|
36
|
+
state: {
|
|
37
|
+
status: failed.status ?? "Failed",
|
|
38
|
+
...(failed.workerRunId ? { workerRunId: failed.workerRunId } : {}),
|
|
39
|
+
...(failed.failureCategory
|
|
40
|
+
? {
|
|
41
|
+
failure: {
|
|
42
|
+
category: failed.failureCategory,
|
|
43
|
+
recommendedFollowUpKind: "feature-review",
|
|
44
|
+
derivedFollowUpTaskId: `${failed.taskId}-review`,
|
|
45
|
+
source: "fallback",
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
: {}),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (decision.recommendedAction === "retry") {
|
|
35
52
|
return {
|
|
36
53
|
kind: "retry_task",
|
|
37
|
-
label:
|
|
54
|
+
label: `重试失败任务 ${failed.taskId}`,
|
|
38
55
|
command: `agent-worker task retry ${quote(failed.taskId)} --feature-id ${quote(input.featureId)} --repo ${quote(input.repoRoot)}`,
|
|
39
56
|
};
|
|
40
57
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
if (decision.recommendedAction === "decide") {
|
|
59
|
+
return {
|
|
60
|
+
kind: "handle_failure",
|
|
61
|
+
label: `根据失败证据处理 ${failed.taskId};ProductBug 可生成并人工批准 Follow-up`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
45
64
|
}
|
|
46
65
|
const blocked = input.tasks.find((task) => ["Blocked", "HumanReview", "Abandoned"].includes(task.status ?? ""));
|
|
47
66
|
if (blocked) {
|
|
67
|
+
const decision = resolveWorkerRecoveryDecision({
|
|
68
|
+
state: {
|
|
69
|
+
status: blocked.status ?? "Blocked",
|
|
70
|
+
...(blocked.failureCategory
|
|
71
|
+
? {
|
|
72
|
+
failure: {
|
|
73
|
+
category: blocked.failureCategory,
|
|
74
|
+
recommendedFollowUpKind: "feature-review",
|
|
75
|
+
derivedFollowUpTaskId: `${blocked.taskId}-review`,
|
|
76
|
+
source: "fallback",
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
if (decision.recommendedAction === "revise-contract") {
|
|
83
|
+
return {
|
|
84
|
+
kind: "revise_contract",
|
|
85
|
+
label: `修订 ${blocked.taskId} 的 contract/spec 后再 materialize`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
48
88
|
return {
|
|
49
89
|
kind: "resolve_human_gate",
|
|
50
90
|
label: `处理 ${blocked.taskId} 的人工 Gate 或阻塞原因`,
|
|
@@ -65,6 +105,16 @@ export function projectNextAction(input, status) {
|
|
|
65
105
|
command: `agent-worker batch run-ready ${featureArgs}`,
|
|
66
106
|
};
|
|
67
107
|
}
|
|
108
|
+
if (status === "awaiting_qa" || status === "deliverable") {
|
|
109
|
+
const gap = input.acceptanceGaps?.find((entry) => entry.nextCommand);
|
|
110
|
+
if (gap?.nextCommand) {
|
|
111
|
+
return {
|
|
112
|
+
kind: status === "deliverable" ? "advance_delivery" : "complete_qa",
|
|
113
|
+
label: `补齐 ${gap.acId} 验收缺口(${gap.status})`,
|
|
114
|
+
command: gap.nextCommand,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
68
118
|
if (status === "awaiting_qa") {
|
|
69
119
|
const finalish = input.tasks.find((task) => /(final[-_]?(verify|verification))|closeout/i.test(task.taskId));
|
|
70
120
|
if (finalish && (finalish.status === "Done" || finalish.status === "Ready")) {
|