@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.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/CHANGELOG.md +28 -5
- package/dist/commands/init.js +16 -2
- package/dist/executors/dag-pi-executor.js +74 -13
- package/dist/executors/pi-executor.js +25 -7
- package/dist/executors/pi-prompt-transport.js +198 -0
- package/dist/executors/pi-sdk-executor.js +1 -1
- package/dist/executors/process-tree.js +33 -0
- package/dist/executors/shell-executor.js +332 -24
- package/dist/executors/shell-verification.js +4 -2
- package/dist/infrastructure/harness/task-store.js +31 -0
- package/dist/shared/operator/capabilities.js +6 -0
- package/dist/verification/maven/cache.js +142 -0
- package/dist/verification/maven/index.js +120 -0
- package/dist/verification/maven/plan-commands.js +421 -0
- package/dist/verification/maven/pom-static.js +136 -0
- package/dist/verification/maven/scope-resolve.js +153 -0
- package/dist/verification/maven/stale.js +130 -0
- package/dist/verification/maven/types.js +23 -0
- package/dist/verification/maven/workspace-graph.js +322 -0
- package/dist/worker/cli.js +236 -21
- package/dist/worker/delivery/final-verification.js +12 -0
- package/dist/worker/delivery/git-transaction.js +32 -7
- package/dist/worker/delivery/package.js +9 -5
- package/dist/worker/delivery/verification-bundle.js +26 -5
- package/dist/worker/feature/advance.js +301 -0
- package/dist/worker/feature/doctor.js +223 -0
- package/dist/worker/feature/next-action.js +11 -3
- package/dist/worker/feature/scaffold.js +798 -0
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/worker/outcomes/projector.js +5 -1
- package/dist/worker/runner/run-ready.js +41 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +202 -9
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/failure-category.js +5 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
- package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
- package/dist/workflows/dag/init-hybrid.js +220 -32
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/reconcile-run.js +24 -0
- package/dist/workflows/dag/types.js +46 -0
- package/dist/workflows/dag/validate.js +19 -2
- package/docs/architecture/dag-execution.md +5 -1
- package/docs/architecture/runtime-boundaries.md +11 -1
- package/docs/architecture/worker-and-feature.md +2 -0
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/product-line/README.md +17 -1
- package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
- package/docs/templates/product-line/scaffold-samples/README.md +36 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
- package/examples/l5-report-coms-process-definition.html +322 -0
- package/package.json +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +84 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/loop-agent/SKILL.md +8 -8
- package/skills/loop-agent/references/command-reference.md +14 -2
- package/skills/loop-agent/references/harness-policy.md +22 -0
- package/skills/loop-agent/references/task-workflow.md +5 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { truncateOutput } from "../shared/output-truncation.js";
|
|
7
|
+
import { processTreeSpawnOptions, terminateProcessTree } from "./process-tree.js";
|
|
7
8
|
import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdictGateShellCommand, } from "./shell-presets.js";
|
|
8
9
|
import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
|
|
9
10
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
10
11
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
11
|
-
import { materializeFrontendTestResult, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
12
|
+
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
12
13
|
import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
|
|
13
14
|
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
14
15
|
import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
|
|
@@ -17,14 +18,16 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
17
18
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
18
19
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
19
20
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
20
|
-
import { materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
21
|
-
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
21
|
+
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
22
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
23
|
+
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
22
24
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
23
25
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
24
26
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
25
27
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
26
28
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
27
29
|
import { readProjectGovernanceContext } from "../workflows/dag/project-governance-context.js";
|
|
30
|
+
import { assertMavenPlanFresh, MavenPlanStaleError, } from "../verification/maven/index.js";
|
|
28
31
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
29
32
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
30
33
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
@@ -46,6 +49,54 @@ function resolveBashExecutable() {
|
|
|
46
49
|
envCandidate ??
|
|
47
50
|
"bash");
|
|
48
51
|
}
|
|
52
|
+
/** POSIX-style runDir for bash `${HARNESS_DAG_RUN_DIR}` expansions on Windows. */
|
|
53
|
+
function normalizeHarnessDagRunDir(runDir) {
|
|
54
|
+
return runDir.replaceAll(path.sep, "/");
|
|
55
|
+
}
|
|
56
|
+
const BACKEND_TEST_EXECUTE_DIAGNOSTICS_REL = "reports/backend-test-execute-diagnostics.md";
|
|
57
|
+
/**
|
|
58
|
+
* Write diagnostics *before* any bash spawn so Windows STATUS_DLL_INIT_FAILED
|
|
59
|
+
* (0xC0000142) / empty-shell-output failures still leave an auditable file.
|
|
60
|
+
* Shell steps append under `## shell-steps`; they must not overwrite this head.
|
|
61
|
+
*/
|
|
62
|
+
async function writeBackendTestExecuteDiagnosticsPreSpawn(input) {
|
|
63
|
+
const harnessDagRunDir = normalizeHarnessDagRunDir(input.runDir);
|
|
64
|
+
if (!harnessDagRunDir.trim()) {
|
|
65
|
+
throw new Error("HARNESS_DAG_RUN_DIR would be empty; refusing backend-test execute (would write under /reports)");
|
|
66
|
+
}
|
|
67
|
+
const reportsDir = path.join(input.runDir, "reports");
|
|
68
|
+
await mkdir(reportsDir, { recursive: true });
|
|
69
|
+
const diagnosticsPath = path.join(reportsDir, "backend-test-execute-diagnostics.md");
|
|
70
|
+
const lines = [
|
|
71
|
+
"# Backend Test Execute Diagnostics",
|
|
72
|
+
"",
|
|
73
|
+
"## pre-spawn",
|
|
74
|
+
"",
|
|
75
|
+
`- phase: pre-spawn`,
|
|
76
|
+
`- startedAt: ${new Date().toISOString()}`,
|
|
77
|
+
`- cwd: ${input.cwd}`,
|
|
78
|
+
`- runId: ${input.runId}`,
|
|
79
|
+
`- runDir: ${input.runDir}`,
|
|
80
|
+
`- HARNESS_DAG_RUN_DIR: ${harnessDagRunDir}`,
|
|
81
|
+
`- bashExecutable: ${resolveBashExecutable()}`,
|
|
82
|
+
`- platform: ${process.platform}`,
|
|
83
|
+
`- mappedScriptCount: ${input.mappedScripts.length}`,
|
|
84
|
+
"- mappedScripts:",
|
|
85
|
+
...(input.mappedScripts.length > 0
|
|
86
|
+
? input.mappedScripts.map((script) => ` - ${script}`)
|
|
87
|
+
: [" - <none>"]),
|
|
88
|
+
`- commandPlanCount: ${input.commandPlan.length}`,
|
|
89
|
+
"- commandPlan:",
|
|
90
|
+
...input.commandPlan.map((label, index) => ` - ${index + 1}. ${label}`),
|
|
91
|
+
"",
|
|
92
|
+
"## shell-steps",
|
|
93
|
+
"",
|
|
94
|
+
"_Shell commands append below. If this section stays empty, bash never ran successfully._",
|
|
95
|
+
"",
|
|
96
|
+
];
|
|
97
|
+
await writeFile(diagnosticsPath, `${lines.join("\n")}\n`, "utf8");
|
|
98
|
+
return { diagnosticsPath, harnessDagRunDir };
|
|
99
|
+
}
|
|
49
100
|
function isWithinRoot(root, candidate) {
|
|
50
101
|
const relative = path.relative(root, candidate);
|
|
51
102
|
return (relative === "" ||
|
|
@@ -114,6 +165,7 @@ export async function executeShellCommand(input) {
|
|
|
114
165
|
const child = spawn(resolveBashExecutable(), ["-c", input.command], {
|
|
115
166
|
cwd: input.cwd,
|
|
116
167
|
env: buildShellProcessEnv(input.envAllowlist, injectedEnv),
|
|
168
|
+
...processTreeSpawnOptions(),
|
|
117
169
|
stdio: ["ignore", "pipe", "pipe"],
|
|
118
170
|
});
|
|
119
171
|
let stdout = createBoundedOutput();
|
|
@@ -187,10 +239,10 @@ export async function executeShellCommand(input) {
|
|
|
187
239
|
if (input.timeoutMs > 0) {
|
|
188
240
|
timeoutHandle = setTimeout(() => {
|
|
189
241
|
timedOut = true;
|
|
190
|
-
child
|
|
242
|
+
terminateProcessTree(child, "SIGTERM");
|
|
191
243
|
sigkillHandle = setTimeout(() => {
|
|
192
244
|
if (child.exitCode === null) {
|
|
193
|
-
child
|
|
245
|
+
terminateProcessTree(child, "SIGKILL");
|
|
194
246
|
}
|
|
195
247
|
}, 5_000);
|
|
196
248
|
}, input.timeoutMs);
|
|
@@ -385,35 +437,164 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
385
437
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
386
438
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
387
439
|
const pytestTargets = mappedScripts.map(shellQuote).join(" ");
|
|
388
|
-
|
|
440
|
+
// Split into short bash -c commands (aligned with markdown-environment).
|
|
441
|
+
// A single ultra-long compound command has been observed on Windows to
|
|
442
|
+
// exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
|
|
443
|
+
// stderr and no diagnostics file. Keep pytest exactly once.
|
|
444
|
+
const commandPlan = [
|
|
445
|
+
"shell-bootstrap",
|
|
446
|
+
"python-identity",
|
|
447
|
+
"pytest-version",
|
|
448
|
+
"pytest-run",
|
|
449
|
+
];
|
|
450
|
+
let preSpawnOk = false;
|
|
451
|
+
try {
|
|
452
|
+
await writeBackendTestExecuteDiagnosticsPreSpawn({
|
|
453
|
+
runDir: meta.runDir,
|
|
454
|
+
runId: meta.runId,
|
|
455
|
+
cwd: input.cwd,
|
|
456
|
+
mappedScripts,
|
|
457
|
+
commandPlan: [...commandPlan],
|
|
458
|
+
});
|
|
459
|
+
preSpawnOk = true;
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
stdout: "",
|
|
466
|
+
stderr: [
|
|
467
|
+
"backend-test markdown-execute-html shell failed",
|
|
468
|
+
"failureCategory=invalid-output",
|
|
469
|
+
`mappedScripts=${mappedScripts.join(",") || "<none>"}`,
|
|
470
|
+
`preSpawnDiagnostics=failed`,
|
|
471
|
+
`diagnostics=${BACKEND_TEST_EXECUTE_DIAGNOSTICS_REL}`,
|
|
472
|
+
message,
|
|
473
|
+
].join("\n"),
|
|
474
|
+
failureCategory: "invalid-output",
|
|
475
|
+
durationMs: Date.now() - started,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// Append-only shell snippets; never overwrite the JS pre-spawn head.
|
|
479
|
+
const shellBootstrapCommand = [
|
|
480
|
+
'if [ -z "${HARNESS_DAG_RUN_DIR:-}" ]; then echo "HARNESS_DAG_RUN_DIR is empty; refusing to write diagnostics under /reports" >&2; exit 2; fi',
|
|
389
481
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
482
|
+
'DIAG_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-execute-diagnostics.md"',
|
|
483
|
+
'echo "STEP=shell-bootstrap"',
|
|
484
|
+
'echo "- shellBootstrapAt: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" >> "${DIAG_FILE}"',
|
|
485
|
+
'echo "- shellPwd: $(pwd)" >> "${DIAG_FILE}"',
|
|
486
|
+
'echo "- shellHARNESS_DAG_RUN_DIR: ${HARNESS_DAG_RUN_DIR}" >> "${DIAG_FILE}"',
|
|
487
|
+
'echo "- shell: ${BASH:-bash} (${BASH_VERSION:-unknown})" >> "${DIAG_FILE}"',
|
|
488
|
+
].join("; ");
|
|
489
|
+
const pythonIdentityCommand = [
|
|
490
|
+
'if [ -z "${HARNESS_DAG_RUN_DIR:-}" ]; then echo "HARNESS_DAG_RUN_DIR is empty" >&2; exit 2; fi',
|
|
491
|
+
'DIAG_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-execute-diagnostics.md"',
|
|
492
|
+
'echo "STEP=resolve-python"',
|
|
390
493
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
391
|
-
'
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
494
|
+
'echo "- command_v_python: $(command -v python || true)" >> "${DIAG_FILE}"',
|
|
495
|
+
'echo "- command_v_python3: $(command -v python3 || true)" >> "${DIAG_FILE}"',
|
|
496
|
+
'echo "- PYTHON_BIN: ${PYTHON_BIN:-<empty>}" >> "${DIAG_FILE}"',
|
|
497
|
+
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
498
|
+
'echo "STEP=python-identity"',
|
|
499
|
+
'if ! PYTHONUNBUFFERED=1 "${PYTHON_BIN}" -c "import sys; print(sys.executable); print(sys.version.splitlines()[0])" >> "${DIAG_FILE}" 2>&1; then echo "STEP=python-identity-failed" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
500
|
+
].join("; ");
|
|
501
|
+
const pytestVersionCommand = [
|
|
502
|
+
'if [ -z "${HARNESS_DAG_RUN_DIR:-}" ]; then echo "HARNESS_DAG_RUN_DIR is empty" >&2; exit 2; fi',
|
|
503
|
+
'DIAG_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-execute-diagnostics.md"',
|
|
504
|
+
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
505
|
+
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
506
|
+
'echo "STEP=pytest-version"',
|
|
507
|
+
'if ! PYTHONUNBUFFERED=1 "${PYTHON_BIN}" -m pytest --version >> "${DIAG_FILE}" 2>&1; then echo "STEP=pytest-version-failed" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
508
|
+
].join("; ");
|
|
509
|
+
// Single pytest invocation; exit 0/1 + non-empty html are reportable.
|
|
510
|
+
const pytestRunCommand = [
|
|
511
|
+
'if [ -z "${HARNESS_DAG_RUN_DIR:-}" ]; then echo "HARNESS_DAG_RUN_DIR is empty" >&2; exit 2; fi',
|
|
512
|
+
'DIAG_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-execute-diagnostics.md"',
|
|
513
|
+
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
514
|
+
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
515
|
+
'echo "STEP=pytest-run"',
|
|
516
|
+
`echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
517
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
398
518
|
"STATUS=$?",
|
|
399
519
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
520
|
+
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
521
|
+
'if [ -f "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ]; then echo "- pytestHtml: present ($(wc -c < "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" | tr -d " ") bytes)" >> "${DIAG_FILE}"; else echo "- pytestHtml: missing" >> "${DIAG_FILE}"; fi',
|
|
522
|
+
'echo "- finishedAt: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" >> "${DIAG_FILE}"',
|
|
523
|
+
'echo "STEP=pytest-finished status=${STATUS}"',
|
|
403
524
|
'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ]; then exit 0; fi',
|
|
525
|
+
'echo "STEP=pipeline-failed status=${STATUS} html=$([ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ] && echo present || echo missing)" >&2',
|
|
404
526
|
'exit "${STATUS}"',
|
|
405
527
|
].join("; ");
|
|
406
|
-
const
|
|
528
|
+
const pipelineCommands = [
|
|
529
|
+
shellBootstrapCommand,
|
|
530
|
+
pythonIdentityCommand,
|
|
531
|
+
pytestVersionCommand,
|
|
532
|
+
pytestRunCommand,
|
|
533
|
+
];
|
|
534
|
+
const results = await executePipelineCommands(input, meta, pipelineCommands);
|
|
407
535
|
if (!results.every((result) => result.ok)) {
|
|
408
|
-
const
|
|
409
|
-
|
|
536
|
+
const failureIndex = results.findIndex((result) => !result.ok);
|
|
537
|
+
const failure = results[failureIndex];
|
|
538
|
+
const combinedStdout = results.map((result) => result.stdout).join("\n");
|
|
539
|
+
let diagnosticsText = "";
|
|
540
|
+
try {
|
|
541
|
+
diagnosticsText = await readFile(path.join(meta.runDir, "reports", "backend-test-execute-diagnostics.md"), "utf8");
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
diagnosticsText = "";
|
|
545
|
+
}
|
|
546
|
+
const failedStep = failureIndex >= 0 && failureIndex < commandPlan.length
|
|
547
|
+
? commandPlan[failureIndex]
|
|
548
|
+
: "unknown";
|
|
549
|
+
const stderrSummary = [
|
|
550
|
+
"backend-test markdown-execute-html shell failed",
|
|
551
|
+
`failureCategory=${failure.failureCategory}`,
|
|
552
|
+
`exitCode=${failure.exitCode ?? "null"}`,
|
|
553
|
+
`durationMs=${failure.durationMs}`,
|
|
554
|
+
`failedCommandIndex=${failureIndex + 1}`,
|
|
555
|
+
`failedStep=${failedStep}`,
|
|
556
|
+
`commandPlanCount=${pipelineCommands.length}`,
|
|
557
|
+
`preSpawnDiagnostics=${preSpawnOk ? "written" : "failed"}`,
|
|
558
|
+
`mappedScripts=${mappedScripts.join(",") || "<none>"}`,
|
|
559
|
+
`shellStdoutEmpty=${combinedStdout.trim().length === 0}`,
|
|
560
|
+
`shellStderrEmpty=${failure.stderr.trim().length === 0}`,
|
|
561
|
+
failure.stdoutArtifactPath
|
|
562
|
+
? `commandStdoutArtifact=${failure.stdoutArtifactPath}`
|
|
563
|
+
: undefined,
|
|
564
|
+
failure.stderrArtifactPath
|
|
565
|
+
? `commandStderrArtifact=${failure.stderrArtifactPath}`
|
|
566
|
+
: undefined,
|
|
567
|
+
`diagnostics=${BACKEND_TEST_EXECUTE_DIAGNOSTICS_REL}`,
|
|
568
|
+
failure.stderr.trim() || "(shell stderr empty)",
|
|
569
|
+
diagnosticsText.trim()
|
|
570
|
+
? `--- diagnostics ---\n${diagnosticsText.trim()}`
|
|
571
|
+
: "(diagnostics file missing or empty)",
|
|
572
|
+
]
|
|
573
|
+
.filter((line) => Boolean(line))
|
|
574
|
+
.join("\n");
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
stdout: combinedStdout,
|
|
578
|
+
stderr: stderrSummary,
|
|
579
|
+
failureCategory: failure.failureCategory,
|
|
580
|
+
durationMs: Date.now() - started,
|
|
581
|
+
};
|
|
410
582
|
}
|
|
411
583
|
const reportsDir = path.join(meta.runDir, "reports");
|
|
412
584
|
const pytestHtmlContent = await readFile(path.join(reportsDir, "backend-test.html"), "utf8");
|
|
413
585
|
const pytestExitCode = Number.parseInt((await readFile(path.join(reportsDir, "backend-test-pytest-exit.txt"), "utf8")).trim(), 10);
|
|
414
|
-
if (![0, 1].includes(pytestExitCode))
|
|
586
|
+
if (![0, 1].includes(pytestExitCode)) {
|
|
415
587
|
throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
|
|
588
|
+
}
|
|
416
589
|
const parsed = parsePytestHtmlReport(pytestHtmlContent);
|
|
590
|
+
// Bind Result v1 from the native pytest-html report BEFORE overwriting with the
|
|
591
|
+
// styled renderer (which drops the data-jsonblob island).
|
|
592
|
+
const resultArtifact = await materializeBackendTestResultFromPytestHtml({
|
|
593
|
+
runDir: meta.runDir,
|
|
594
|
+
htmlRelativePath: "reports/backend-test.html",
|
|
595
|
+
htmlContent: pytestHtmlContent,
|
|
596
|
+
pytestExitCode,
|
|
597
|
+
});
|
|
417
598
|
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
418
599
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
419
600
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
@@ -426,11 +607,52 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
426
607
|
traceabilitySummary,
|
|
427
608
|
});
|
|
428
609
|
const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
|
|
429
|
-
const facts = renderBackendTestFacts({
|
|
610
|
+
const facts = renderBackendTestFacts({
|
|
611
|
+
parsed,
|
|
612
|
+
cases,
|
|
613
|
+
pytestExitCode,
|
|
614
|
+
htmlRelativePath: "reports/backend-test.html",
|
|
615
|
+
htmlContent,
|
|
616
|
+
caseValidationSummary,
|
|
617
|
+
traceabilitySummary,
|
|
618
|
+
});
|
|
430
619
|
const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
|
|
431
620
|
const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
|
|
621
|
+
// Deterministic L-5 dashboard: machine-computed metrics (not Pi-generated).
|
|
622
|
+
// manifest is produced by an upstream finalize node; tolerate its absence
|
|
623
|
+
// so a minimal DAG without manifest still gets a degraded dashboard.
|
|
624
|
+
let manifestForL5 = {};
|
|
625
|
+
try {
|
|
626
|
+
const manifestPath = path.join(meta.runDir, "contracts", "backend-test-case-manifest.json");
|
|
627
|
+
manifestForL5 = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// manifest missing: L5 AC/automation metrics degrade to unavailable.
|
|
631
|
+
}
|
|
632
|
+
const l5Metrics = computeL5ReportMetrics({
|
|
633
|
+
result: { passed: parsed.passed, failed: parsed.failed, error: parsed.errors, skipped: parsed.skipped },
|
|
634
|
+
manifest: manifestForL5,
|
|
635
|
+
coverage: null,
|
|
636
|
+
criticalRiskCount: parsed.failed + parsed.errors > 0 ? 1 : 0,
|
|
637
|
+
});
|
|
638
|
+
const failureSummaries = parsed.cases
|
|
639
|
+
.filter((c) => c.status !== "passed")
|
|
640
|
+
.map((c) => {
|
|
641
|
+
const id = c.name.match(/BE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}/)?.[0] ?? c.name;
|
|
642
|
+
return { caseId: id, title: c.name, message: c.message ?? c.status, script: `${c.classname.split("::")[0] ?? c.classname}` };
|
|
643
|
+
});
|
|
644
|
+
const l5Html = renderBackendTestL5Dashboard({
|
|
645
|
+
title: meta.spec.title,
|
|
646
|
+
objective: meta.spec.objective,
|
|
647
|
+
parsed,
|
|
648
|
+
metrics: l5Metrics,
|
|
649
|
+
caseValidationSummary,
|
|
650
|
+
traceabilitySummary,
|
|
651
|
+
failures: failureSummaries.length > 0 ? failureSummaries : undefined,
|
|
652
|
+
});
|
|
653
|
+
const l5Path = await writeRunReport(meta.runDir, "backend-test-l5-dashboard.html", l5Html);
|
|
432
654
|
const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
|
|
433
|
-
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, facts);
|
|
655
|
+
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `l5-dashboard=${l5Path}`, `result=${resultArtifact.path}`, facts);
|
|
434
656
|
}
|
|
435
657
|
else if (pipeline === "contracts") {
|
|
436
658
|
const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
|
|
@@ -889,6 +1111,38 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
889
1111
|
};
|
|
890
1112
|
}
|
|
891
1113
|
}
|
|
1114
|
+
async function executeFrontendTestEvidenceValidation(input) {
|
|
1115
|
+
const started = Date.now();
|
|
1116
|
+
try {
|
|
1117
|
+
const result = await validateFrontendCaseEvidence({ workspaceRoot: input.cwd });
|
|
1118
|
+
const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
|
|
1119
|
+
if (result.hardFail) {
|
|
1120
|
+
return {
|
|
1121
|
+
ok: false,
|
|
1122
|
+
stdout: "",
|
|
1123
|
+
stderr: `frontend-test evidence hard-fail: ${JSON.stringify(result.issues)}`,
|
|
1124
|
+
failureCategory: "nonzero-exit",
|
|
1125
|
+
durationMs: Date.now() - started,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
ok: true,
|
|
1130
|
+
stdout: output,
|
|
1131
|
+
stderr: "",
|
|
1132
|
+
failureCategory: "success",
|
|
1133
|
+
durationMs: Date.now() - started,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
return {
|
|
1138
|
+
ok: false,
|
|
1139
|
+
stdout: "",
|
|
1140
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
1141
|
+
failureCategory: "invalid-output",
|
|
1142
|
+
durationMs: Date.now() - started,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
892
1146
|
async function executeFrontendLintBaseline(input, meta) {
|
|
893
1147
|
const started = Date.now();
|
|
894
1148
|
const shell = input.task.shell;
|
|
@@ -989,6 +1243,9 @@ export async function executeDagShellNode(input, meta) {
|
|
|
989
1243
|
return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
|
|
990
1244
|
}
|
|
991
1245
|
}
|
|
1246
|
+
if (shell?.frontendTestEvidenceValidation) {
|
|
1247
|
+
return executeFrontendTestEvidenceValidation(input);
|
|
1248
|
+
}
|
|
992
1249
|
if (shell?.backendTestPipeline) {
|
|
993
1250
|
return executeBackendTestPipelineWithWriteGuard(input, meta);
|
|
994
1251
|
}
|
|
@@ -1260,10 +1517,61 @@ export async function executeDagShellNode(input, meta) {
|
|
|
1260
1517
|
if (!shell || commands.length === 0) {
|
|
1261
1518
|
throw new Error(`shell task ${input.task.id} requires shell.preset, shell.verdictGate, and/or non-empty shell.commands`);
|
|
1262
1519
|
}
|
|
1520
|
+
const started = Date.now();
|
|
1521
|
+
if (shell.mavenVerificationPlan) {
|
|
1522
|
+
try {
|
|
1523
|
+
assertMavenPlanFresh(input.cwd, shell.mavenVerificationPlan, { shellCommands: commands });
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
const message = error instanceof MavenPlanStaleError
|
|
1527
|
+
? error.message
|
|
1528
|
+
: error instanceof Error
|
|
1529
|
+
? error.message
|
|
1530
|
+
: String(error);
|
|
1531
|
+
const staleMessage = message.includes("verification-plan-stale")
|
|
1532
|
+
? message
|
|
1533
|
+
: `verification-plan-stale: ${message}`;
|
|
1534
|
+
const staleResults = commands.map((command) => ({
|
|
1535
|
+
command,
|
|
1536
|
+
cwd: input.cwd,
|
|
1537
|
+
durationMs: 0,
|
|
1538
|
+
exitCode: null,
|
|
1539
|
+
failureCategory: "verification-plan-stale",
|
|
1540
|
+
ok: false,
|
|
1541
|
+
stderr: staleMessage,
|
|
1542
|
+
stderrBytes: Buffer.byteLength(staleMessage),
|
|
1543
|
+
stderrTruncated: false,
|
|
1544
|
+
stdout: "",
|
|
1545
|
+
stdoutBytes: 0,
|
|
1546
|
+
stdoutTruncated: false,
|
|
1547
|
+
timedOut: false,
|
|
1548
|
+
}));
|
|
1549
|
+
await writeDagNodeTextArtifact(meta.runDir, input.task.id, "result.summary.md", buildShellResultSummaryMarkdown({
|
|
1550
|
+
nodeId: input.task.id,
|
|
1551
|
+
runId: meta.runId,
|
|
1552
|
+
rootCwd: input.cwd,
|
|
1553
|
+
results: staleResults,
|
|
1554
|
+
}));
|
|
1555
|
+
return {
|
|
1556
|
+
ok: false,
|
|
1557
|
+
stdout: "",
|
|
1558
|
+
stderr: `${staleMessage}\nRegenerate/validate the DAG before running Maven verification.`,
|
|
1559
|
+
failureCategory: "verification-plan-stale",
|
|
1560
|
+
durationMs: Date.now() - started,
|
|
1561
|
+
...{
|
|
1562
|
+
commandResults: staleResults.map((result) => ({
|
|
1563
|
+
ok: result.ok,
|
|
1564
|
+
exitCode: result.exitCode,
|
|
1565
|
+
failureCategory: result.failureCategory,
|
|
1566
|
+
command: result.command,
|
|
1567
|
+
})),
|
|
1568
|
+
},
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1263
1572
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
1264
1573
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
1265
1574
|
const results = [];
|
|
1266
|
-
const started = Date.now();
|
|
1267
1575
|
let beforeStatus;
|
|
1268
1576
|
try {
|
|
1269
1577
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { processTreeSpawnOptions, terminateProcessTree } from "./process-tree.js";
|
|
2
3
|
export const DEFAULT_VERIFY_TIMEOUT_MS = 1_800_000;
|
|
3
4
|
const VERIFY_ENV_ALLOWLIST = [
|
|
4
5
|
"HOME",
|
|
@@ -142,6 +143,7 @@ export async function executeCommand(command, defaultTimeoutMs = DEFAULT_VERIFY_
|
|
|
142
143
|
const child = spawn(binary, args, {
|
|
143
144
|
cwd: command.cwd,
|
|
144
145
|
env: command.env ?? process.env,
|
|
146
|
+
...processTreeSpawnOptions(),
|
|
145
147
|
stdio: ["ignore", "pipe", "pipe"],
|
|
146
148
|
});
|
|
147
149
|
let stdout = "";
|
|
@@ -184,10 +186,10 @@ export async function executeCommand(command, defaultTimeoutMs = DEFAULT_VERIFY_
|
|
|
184
186
|
if (timeoutMs > 0) {
|
|
185
187
|
timeoutHandle = setTimeout(() => {
|
|
186
188
|
timedOut = true;
|
|
187
|
-
child
|
|
189
|
+
terminateProcessTree(child, "SIGTERM");
|
|
188
190
|
sigkillHandle = setTimeout(() => {
|
|
189
191
|
if (child.exitCode === null) {
|
|
190
|
-
child
|
|
192
|
+
terminateProcessTree(child, "SIGKILL");
|
|
191
193
|
}
|
|
192
194
|
}, 5_000);
|
|
193
195
|
}, timeoutMs);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { taskConfigSchema } from "../../shared/types.js";
|
|
3
4
|
import { appendJsonlLineAtomic, writeJsonAtomic, writeTextAtomic, } from "./atomic-write.js";
|
|
4
5
|
function taskDir(repoRoot, taskId) {
|
|
5
6
|
return path.join(repoRoot, ".harness", "tasks", taskId);
|
|
@@ -10,6 +11,27 @@ function taskFile(repoRoot, taskId, filename) {
|
|
|
10
11
|
function taskWriteOptions(repoRoot) {
|
|
11
12
|
return { repoRoot };
|
|
12
13
|
}
|
|
14
|
+
function describeTaskConfigValidationError(error) {
|
|
15
|
+
if (error &&
|
|
16
|
+
typeof error === "object" &&
|
|
17
|
+
"issues" in error &&
|
|
18
|
+
Array.isArray(error.issues)) {
|
|
19
|
+
return error.issues
|
|
20
|
+
.map((issue) => {
|
|
21
|
+
if (!issue || typeof issue !== "object")
|
|
22
|
+
return String(issue);
|
|
23
|
+
const pathValue = "path" in issue && Array.isArray(issue.path)
|
|
24
|
+
? issue.path.map(String).join(".")
|
|
25
|
+
: "";
|
|
26
|
+
const message = "message" in issue && typeof issue.message === "string"
|
|
27
|
+
? issue.message
|
|
28
|
+
: String(issue);
|
|
29
|
+
return pathValue ? `${pathValue}: ${message}` : message;
|
|
30
|
+
})
|
|
31
|
+
.join("; ");
|
|
32
|
+
}
|
|
33
|
+
return error instanceof Error ? error.message : String(error);
|
|
34
|
+
}
|
|
13
35
|
export function resolveTaskContextFromDir(taskDir) {
|
|
14
36
|
const normalized = path.resolve(taskDir);
|
|
15
37
|
const taskId = path.basename(normalized);
|
|
@@ -37,6 +59,15 @@ export function resolveTaskStateContext(statePath) {
|
|
|
37
59
|
return { repoRoot, taskId };
|
|
38
60
|
}
|
|
39
61
|
export async function writeTaskConfig(repoRoot, taskId, config) {
|
|
62
|
+
try {
|
|
63
|
+
taskConfigSchema.parse(config);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error([
|
|
67
|
+
`invalid task config for ${taskId}: ${describeTaskConfigValidationError(error)}`,
|
|
68
|
+
"`referenceDocs` must use `{ path, name? }[]`; `verifyCommands` must use `{ label, command, timeoutMs? }[]`.",
|
|
69
|
+
].join(" "));
|
|
70
|
+
}
|
|
40
71
|
await writeJsonAtomic(taskFile(repoRoot, taskId, "task.json"), config, taskWriteOptions(repoRoot));
|
|
41
72
|
}
|
|
42
73
|
export async function writeWorkflowState(repoRoot, taskId, state) {
|
|
@@ -61,7 +61,10 @@ const OFFICIAL_ACTIONS = [
|
|
|
61
61
|
...["init", "status", "run", "record-round", "add-signal", "closeout"].map((leaf) => [`loop${leaf.replace(/(^|-)(.)/g, (_m, _d, c) => c.toUpperCase())}`, `loop-agent loop ${leaf}`, leaf === "status" ? "read" : "mutation", "advanced"]),
|
|
62
62
|
...["list", "inspect", "save", "run", "diff", "replay"].map((leaf) => [`workflow${leaf[0].toUpperCase()}${leaf.slice(1)}`, `loop-agent workflow ${leaf}`, ["list", "inspect", "diff"].includes(leaf) ? "read" : "mutation", "advanced"]),
|
|
63
63
|
["workerFeatureReview", "agent-worker feature review", "read", "model-callable"],
|
|
64
|
+
["workerFeatureScaffold", "agent-worker feature scaffold", "mutation", "advanced"],
|
|
64
65
|
["workerFeatureRun", "agent-worker feature run", "long-running", "human-gated-required"],
|
|
66
|
+
["workerFeatureAdvance", "agent-worker feature advance", "long-running", "human-gated-required"],
|
|
67
|
+
["workerFeatureDoctor", "agent-worker feature doctor", "read", "model-callable"],
|
|
65
68
|
["workerFeatureVerifyFinal", "agent-worker feature verify-final", "long-running", "human-gated-required"],
|
|
66
69
|
["workerFeatureDelivery", "agent-worker feature delivery", "mutation", "human-gated-required"],
|
|
67
70
|
["workerFeatureCloseout", "agent-worker feature closeout", "mutation", "human-gated-required"],
|
|
@@ -734,7 +737,10 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
734
737
|
{ command: "loop-agent stats", coverage: "model-callable", action: "stats", source: "loop-agent" },
|
|
735
738
|
{ command: "loop-agent stats context", coverage: "model-callable", action: "statsContext", source: "loop-agent" },
|
|
736
739
|
{ command: "agent-worker feature review", coverage: "model-callable", action: "workerFeatureReview", source: "agent-worker" },
|
|
740
|
+
{ command: "agent-worker feature scaffold", coverage: "advanced", action: "workerFeatureScaffold", source: "agent-worker" },
|
|
737
741
|
{ command: "agent-worker feature run", coverage: "human-gated-required", action: "workerFeatureRun", source: "agent-worker" },
|
|
742
|
+
{ command: "agent-worker feature advance", coverage: "human-gated-required", action: "workerFeatureAdvance", source: "agent-worker" },
|
|
743
|
+
{ command: "agent-worker feature doctor", coverage: "model-callable", action: "workerFeatureDoctor", source: "agent-worker" },
|
|
738
744
|
{ command: "agent-worker feature verify-final", coverage: "human-gated-required", action: "workerFeatureVerifyFinal", source: "agent-worker" },
|
|
739
745
|
{ command: "agent-worker feature delivery", coverage: "human-gated-required", action: "workerFeatureDelivery", source: "agent-worker" },
|
|
740
746
|
{ command: "agent-worker feature closeout", coverage: "human-gated-required", action: "workerFeatureCloseout", source: "agent-worker" },
|