@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -4
- package/README.md +4 -4
- package/dist/application/dag/generate-task-dag.js +25 -8
- package/dist/commands/dag-init-hybrid.js +3 -2
- package/dist/commands/delegate.js +11 -11
- package/dist/commands/init.js +29 -14
- package/dist/commands/instructions.js +1 -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 +362 -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/task/read-model.js +3 -7
- package/dist/task/runtime.js +12 -3
- 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/run-task/run-task.js +18 -2
- package/dist/worker/runner/run-ready.js +41 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -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 +224 -39
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/reconcile-run.js +24 -0
- package/dist/workflows/dag/types.js +57 -0
- package/dist/workflows/dag/validate.js +19 -2
- package/dist/workflows/loop/actions/dag-action.js +2 -1
- 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/init-surface.manifest.json +3 -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 +14 -16
- package/skills/loop-agent/references/command-reference.md +33 -12
- package/skills/loop-agent/references/harness-policy.md +28 -6
- package/skills/loop-agent/references/hybrid-dag.md +8 -7
- package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
- package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
- package/skills/loop-agent/references/task-workflow.md +13 -5
|
@@ -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, collectJacocoCoverage, 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"',
|
|
493
|
+
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
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"',
|
|
390
513
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
391
|
-
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" >&2; exit 127; fi',
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// pytest-html data-jsonblob island, so a JUnit XML report is no
|
|
396
|
-
// longer generated.
|
|
397
|
-
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
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,82 @@ 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
|
+
// Optional JaCoCo coverage collection (Java services). When jacocoCoverage
|
|
633
|
+
// is configured on the shell node, dump exec data over TCP from the
|
|
634
|
+
// JaCoCo tcpserver agent, convert via jacococli.jar, and parse into a
|
|
635
|
+
// code-coverage-v1 contract. Failure-safe: any error → coverage stays
|
|
636
|
+
// null and the L-5 dashboard degrades line/branch to unavailable.
|
|
637
|
+
let coverageContract = null;
|
|
638
|
+
const jacocoCfg = input.task.shell?.jacocoCoverage;
|
|
639
|
+
if (jacocoCfg) {
|
|
640
|
+
const sourceBinding = meta.spec.sourceBinding;
|
|
641
|
+
const requirementIds = sourceBinding?.requirementIds ?? [];
|
|
642
|
+
try {
|
|
643
|
+
coverageContract = await collectJacocoCoverage({
|
|
644
|
+
endpoint: jacocoCfg.endpoint,
|
|
645
|
+
cliJarPath: jacocoCfg.cliJarPath,
|
|
646
|
+
includes: jacocoCfg.includes ?? "*",
|
|
647
|
+
connectTimeoutMs: jacocoCfg.connectTimeoutMs ?? 5000,
|
|
648
|
+
workDir: reportsDir,
|
|
649
|
+
sourceScope: { requirementIds, paths: [] },
|
|
650
|
+
commitSha: null,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// coverage unavailable: JaCoCo endpoint unreachable or parse failed.
|
|
655
|
+
}
|
|
656
|
+
if (coverageContract) {
|
|
657
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
658
|
+
await mkdir(contractsDir, { recursive: true });
|
|
659
|
+
await writeFile(path.join(contractsDir, "code-coverage-v1.json"), JSON.stringify(coverageContract, null, 2), "utf8");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const l5Metrics = computeL5ReportMetrics({
|
|
663
|
+
result: { passed: parsed.passed, failed: parsed.failed, error: parsed.errors, skipped: parsed.skipped },
|
|
664
|
+
manifest: manifestForL5,
|
|
665
|
+
coverage: coverageContract,
|
|
666
|
+
criticalRiskCount: parsed.failed + parsed.errors > 0 ? 1 : 0,
|
|
667
|
+
});
|
|
668
|
+
const failureSummaries = parsed.cases
|
|
669
|
+
.filter((c) => c.status !== "passed")
|
|
670
|
+
.map((c) => {
|
|
671
|
+
const id = c.name.match(/BE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}/)?.[0] ?? c.name;
|
|
672
|
+
return { caseId: id, title: c.name, message: c.message ?? c.status, script: `${c.classname.split("::")[0] ?? c.classname}` };
|
|
673
|
+
});
|
|
674
|
+
const l5Html = renderBackendTestL5Dashboard({
|
|
675
|
+
title: meta.spec.title,
|
|
676
|
+
objective: meta.spec.objective,
|
|
677
|
+
parsed,
|
|
678
|
+
metrics: l5Metrics,
|
|
679
|
+
caseValidationSummary,
|
|
680
|
+
traceabilitySummary,
|
|
681
|
+
failures: failureSummaries.length > 0 ? failureSummaries : undefined,
|
|
682
|
+
});
|
|
683
|
+
const l5Path = await writeRunReport(meta.runDir, "backend-test-l5-dashboard.html", l5Html);
|
|
432
684
|
const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
|
|
433
|
-
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, facts);
|
|
685
|
+
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `l5-dashboard=${l5Path}`, `result=${resultArtifact.path}`, facts);
|
|
434
686
|
}
|
|
435
687
|
else if (pipeline === "contracts") {
|
|
436
688
|
const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
|
|
@@ -889,6 +1141,38 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
889
1141
|
};
|
|
890
1142
|
}
|
|
891
1143
|
}
|
|
1144
|
+
async function executeFrontendTestEvidenceValidation(input) {
|
|
1145
|
+
const started = Date.now();
|
|
1146
|
+
try {
|
|
1147
|
+
const result = await validateFrontendCaseEvidence({ workspaceRoot: input.cwd });
|
|
1148
|
+
const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
|
|
1149
|
+
if (result.hardFail) {
|
|
1150
|
+
return {
|
|
1151
|
+
ok: false,
|
|
1152
|
+
stdout: "",
|
|
1153
|
+
stderr: `frontend-test evidence hard-fail: ${JSON.stringify(result.issues)}`,
|
|
1154
|
+
failureCategory: "nonzero-exit",
|
|
1155
|
+
durationMs: Date.now() - started,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
ok: true,
|
|
1160
|
+
stdout: output,
|
|
1161
|
+
stderr: "",
|
|
1162
|
+
failureCategory: "success",
|
|
1163
|
+
durationMs: Date.now() - started,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
return {
|
|
1168
|
+
ok: false,
|
|
1169
|
+
stdout: "",
|
|
1170
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
1171
|
+
failureCategory: "invalid-output",
|
|
1172
|
+
durationMs: Date.now() - started,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
892
1176
|
async function executeFrontendLintBaseline(input, meta) {
|
|
893
1177
|
const started = Date.now();
|
|
894
1178
|
const shell = input.task.shell;
|
|
@@ -989,6 +1273,9 @@ export async function executeDagShellNode(input, meta) {
|
|
|
989
1273
|
return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
|
|
990
1274
|
}
|
|
991
1275
|
}
|
|
1276
|
+
if (shell?.frontendTestEvidenceValidation) {
|
|
1277
|
+
return executeFrontendTestEvidenceValidation(input);
|
|
1278
|
+
}
|
|
992
1279
|
if (shell?.backendTestPipeline) {
|
|
993
1280
|
return executeBackendTestPipelineWithWriteGuard(input, meta);
|
|
994
1281
|
}
|
|
@@ -1260,10 +1547,61 @@ export async function executeDagShellNode(input, meta) {
|
|
|
1260
1547
|
if (!shell || commands.length === 0) {
|
|
1261
1548
|
throw new Error(`shell task ${input.task.id} requires shell.preset, shell.verdictGate, and/or non-empty shell.commands`);
|
|
1262
1549
|
}
|
|
1550
|
+
const started = Date.now();
|
|
1551
|
+
if (shell.mavenVerificationPlan) {
|
|
1552
|
+
try {
|
|
1553
|
+
assertMavenPlanFresh(input.cwd, shell.mavenVerificationPlan, { shellCommands: commands });
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
const message = error instanceof MavenPlanStaleError
|
|
1557
|
+
? error.message
|
|
1558
|
+
: error instanceof Error
|
|
1559
|
+
? error.message
|
|
1560
|
+
: String(error);
|
|
1561
|
+
const staleMessage = message.includes("verification-plan-stale")
|
|
1562
|
+
? message
|
|
1563
|
+
: `verification-plan-stale: ${message}`;
|
|
1564
|
+
const staleResults = commands.map((command) => ({
|
|
1565
|
+
command,
|
|
1566
|
+
cwd: input.cwd,
|
|
1567
|
+
durationMs: 0,
|
|
1568
|
+
exitCode: null,
|
|
1569
|
+
failureCategory: "verification-plan-stale",
|
|
1570
|
+
ok: false,
|
|
1571
|
+
stderr: staleMessage,
|
|
1572
|
+
stderrBytes: Buffer.byteLength(staleMessage),
|
|
1573
|
+
stderrTruncated: false,
|
|
1574
|
+
stdout: "",
|
|
1575
|
+
stdoutBytes: 0,
|
|
1576
|
+
stdoutTruncated: false,
|
|
1577
|
+
timedOut: false,
|
|
1578
|
+
}));
|
|
1579
|
+
await writeDagNodeTextArtifact(meta.runDir, input.task.id, "result.summary.md", buildShellResultSummaryMarkdown({
|
|
1580
|
+
nodeId: input.task.id,
|
|
1581
|
+
runId: meta.runId,
|
|
1582
|
+
rootCwd: input.cwd,
|
|
1583
|
+
results: staleResults,
|
|
1584
|
+
}));
|
|
1585
|
+
return {
|
|
1586
|
+
ok: false,
|
|
1587
|
+
stdout: "",
|
|
1588
|
+
stderr: `${staleMessage}\nRegenerate/validate the DAG before running Maven verification.`,
|
|
1589
|
+
failureCategory: "verification-plan-stale",
|
|
1590
|
+
durationMs: Date.now() - started,
|
|
1591
|
+
...{
|
|
1592
|
+
commandResults: staleResults.map((result) => ({
|
|
1593
|
+
ok: result.ok,
|
|
1594
|
+
exitCode: result.exitCode,
|
|
1595
|
+
failureCategory: result.failureCategory,
|
|
1596
|
+
command: result.command,
|
|
1597
|
+
})),
|
|
1598
|
+
},
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1263
1602
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
1264
1603
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
1265
1604
|
const results = [];
|
|
1266
|
-
const started = Date.now();
|
|
1267
1605
|
let beforeStatus;
|
|
1268
1606
|
try {
|
|
1269
1607
|
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" },
|
package/dist/task/read-model.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { queryCursorRuns } from "../records/one-shot-runs.js";
|
|
4
|
-
import {
|
|
5
|
-
import { listAllDagRunEntries, readDagRunSpec } from "../workflows/dag/lifecycle.js";
|
|
4
|
+
import { listAllDagRunEntries, readDagRunSpec, } from "../workflows/dag/lifecycle.js";
|
|
6
5
|
import { getTaskDir, getTaskPaths, getTaskStatus } from "./runtime.js";
|
|
7
6
|
async function exists(filePath) {
|
|
8
7
|
try {
|
|
@@ -28,9 +27,6 @@ async function listRelatedDagRuns(repoRoot, taskId) {
|
|
|
28
27
|
}
|
|
29
28
|
return related;
|
|
30
29
|
}
|
|
31
|
-
function defaultDagDraftPath(taskId) {
|
|
32
|
-
return defaultHybridDagOutputPath(taskId);
|
|
33
|
-
}
|
|
34
30
|
function closeoutPath(repoRoot, taskId) {
|
|
35
31
|
return path.join(repoRoot, "docs", "progress", `${taskId}.md`);
|
|
36
32
|
}
|
|
@@ -43,7 +39,7 @@ function deriveNextAction(input) {
|
|
|
43
39
|
}
|
|
44
40
|
if (!input.dagDraftExists && !input.latestDagRun) {
|
|
45
41
|
return {
|
|
46
|
-
command: `npm run dev -- dag run-task ${input.taskId} --profile auto --strict-models
|
|
42
|
+
command: `npm run dev -- dag run-task ${input.taskId} --profile auto --strict-models`,
|
|
47
43
|
reason: "source ready; DAG draft/run evidence not found",
|
|
48
44
|
};
|
|
49
45
|
}
|
|
@@ -86,7 +82,7 @@ export async function buildTaskReadModel(repoRoot, taskId) {
|
|
|
86
82
|
if (!(await exists(requirementPath)))
|
|
87
83
|
missing.push(requirementPath);
|
|
88
84
|
const sourceReady = missing.length === 0;
|
|
89
|
-
const dagDraftPath =
|
|
85
|
+
const dagDraftPath = paths.dagDraftPath;
|
|
90
86
|
const dagDraftExists = await exists(dagDraftPath);
|
|
91
87
|
const dagRuns = await listRelatedDagRuns(repoRoot, taskId);
|
|
92
88
|
const latestDagRun = dagRuns.at(-1) ?? null;
|