@tea-agent/loop-agent 0.12.0 → 0.13.0-beta.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 +155 -153
- package/CHANGELOG.md +338 -265
- package/README.md +345 -298
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/generate-task-dag.js +28 -28
- package/dist/application/evaluation/candidate-hash.js +75 -0
- package/dist/application/evaluation/candidate.js +52 -0
- package/dist/application/evaluation/replay.js +289 -0
- package/dist/application/evaluation/types.js +130 -0
- package/dist/cli/command-definitions.js +27 -7
- package/dist/cli/program.js +8 -4
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/eval.js +235 -0
- package/dist/commands/init.js +544 -506
- package/dist/commands/knowledge.js +129 -31
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/pi-sdk-executor.js +38 -24
- package/dist/executors/shell-executor.js +34 -2
- package/dist/executors/shell-presets.js +20 -0
- package/dist/executors/shell-verification.js +7 -0
- package/dist/governance/manifest-types.js +4 -0
- package/dist/infrastructure/evaluation/candidate-store.js +435 -0
- package/dist/infrastructure/evaluation/store.js +40 -0
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/config-types.js +28 -1
- package/dist/task/runtime.js +27 -27
- package/dist/worker/cli.js +96 -1
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/read-model.js +246 -41
- package/dist/worker/observe/routes.js +173 -15
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +46 -27
- package/dist/worker/observe/static/app.js +150 -150
- package/dist/worker/observe/static/constants.js +148 -148
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-helpers.js +172 -172
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dag-model.js +72 -72
- package/dist/worker/observe/static/dom.js +61 -61
- package/dist/worker/observe/static/format-pool.js +67 -67
- package/dist/worker/observe/static/format.js +292 -292
- package/dist/worker/observe/static/index.html +308 -308
- package/dist/worker/observe/static/kpi.js +94 -94
- package/dist/worker/observe/static/relations.js +133 -128
- package/dist/worker/observe/static/router.js +93 -85
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/shell-chrome.js +68 -68
- package/dist/worker/observe/static/state.js +253 -253
- package/dist/worker/observe/static/styles.css +1902 -1890
- package/dist/worker/observe/static/views/batch.js +227 -226
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/dag-inspector.js +607 -477
- package/dist/worker/observe/static/views/dag.js +362 -362
- package/dist/worker/observe/static/views/dashboard.js +445 -442
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -453
- package/dist/worker/observe/static/views/pool.js +350 -347
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/session-timeline.js +205 -205
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/task.js +314 -260
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +2404 -360
- package/dist/workflows/dag/node-execution.js +9 -0
- package/dist/workflows/dag/prompt.js +9 -0
- package/dist/workflows/dag/report.js +35 -1
- package/dist/workflows/dag/runner.js +28 -2
- package/dist/workflows/dag/task-demand-routing.js +383 -0
- package/dist/workflows/dag/types.js +51 -13
- package/dist/workflows/dag/upstream-artifacts.js +1 -0
- package/dist/workflows/dag/validate.js +59 -1
- package/docs/README.md +106 -104
- package/docs/agent-dag-recovery-playbook.md +195 -184
- package/docs/agent-dag-runner.md +67 -67
- package/docs/architecture/README.md +26 -26
- package/docs/architecture/dag-execution.md +140 -140
- package/docs/architecture/evolution.md +54 -53
- package/docs/architecture/facts-and-state.md +71 -58
- package/docs/architecture/runtime-boundaries.md +191 -191
- package/docs/architecture/system-overview.md +93 -93
- package/docs/architecture/worker-and-feature.md +85 -81
- package/docs/cursor-prompt-sidecar.md +36 -36
- package/docs/decisions/README.md +18 -15
- package/docs/design/README.md +167 -77
- package/docs/development-principles.md +73 -73
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +15 -9
- package/docs/exec-plans/completed/README.md +85 -73
- package/docs/feature-workflow.md +389 -261
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +289 -280
- package/docs/loop-agent-harness.md +142 -130
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +64 -54
- package/docs/reports/README.md +117 -94
- package/docs/skills/README.md +7 -7
- package/docs/skills/vetted-skill-registry.md +29 -27
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +190 -190
- package/docs/templates/agent-dag.final-verification.json +185 -185
- package/docs/templates/agent-dag.schema.json +411 -383
- package/docs/templates/agent-dag.supervised-implementation.json +501 -501
- package/docs/templates/backend-test-analysis.schema.json +44 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +202 -139
- package/docs/templates/backend-test-dag.json +311 -276
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -125
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -81
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -33
- package/docs/templates/frontend-task-constraints.md +35 -25
- package/docs/templates/frontend-task-requirement.md +70 -61
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
- package/docs/templates/frontend-test-dag.json +23 -0
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
- package/docs/templates/harness.schema.json +221 -221
- package/docs/templates/hybrid-dag.json +188 -188
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +178 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/templates/product-line/AGENTS.md +8 -8
- package/docs/templates/product-line/README.md +9 -9
- package/docs/templates/product-line/acceptance.yaml +14 -14
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/task-graph.yaml +15 -15
- package/docs/templates/product-line/task.yaml +64 -64
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/docs/verification-matrix.md +70 -66
- package/examples/decision-gate-agent-dag.json +177 -177
- package/examples/example-dag.json +46 -46
- package/examples/hybrid-loop-agent-dag.json +189 -189
- package/harness.json +66 -66
- package/package.json +88 -46
- package/scripts/check-product-line-docs.sh +29 -29
- package/scripts/check-task-pool-root.sh +32 -32
- package/scripts/kb-bootstrap-init-skeleton.sh +240 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +164 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +39 -37
- package/skills/agent-worker/references/agent-worker-operator.md +60 -43
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -0
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
- package/skills/analyze-product-dependencies/references/example.md +76 -0
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
- package/skills/analyze-product-requirements/SKILL.md +90 -0
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
- package/skills/analyze-product-requirements/references/example.md +86 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/frontend-design-review/SKILL.md +66 -59
- package/skills/frontend-design-review/references/review-checklist.md +58 -37
- package/skills/frontend-implementation/SKILL.md +47 -51
- package/skills/frontend-implementation/references/code-standards.md +32 -34
- package/skills/frontend-implementation/references/design-spec.md +46 -46
- package/skills/frontend-implementation/references/node-contracts.md +76 -32
- package/skills/frontend-review/SKILL.md +59 -53
- package/skills/frontend-review/references/review-findings.md +47 -42
- package/skills/frontend-verification/SKILL.md +53 -40
- package/skills/frontend-verification/references/verification-checklist.md +68 -56
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/grill-with-docs/SKILL.md +88 -88
- package/skills/grill-with-docs/adr-format.md +47 -47
- package/skills/grill-with-docs/context-format.md +60 -60
- package/skills/init-capability-evolution/SKILL.md +70 -70
- package/skills/loop-agent/SKILL.md +151 -151
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +505 -452
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +263 -263
- package/skills/loop-agent/references/hybrid-dag.md +238 -233
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -89
- package/skills/loop-agent/references/verification-and-failure-handling.md +139 -139
- package/skills/playwright-cli/SKILL.md +420 -0
- package/skills/playwright-cli/references/element-attributes.md +23 -0
- package/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/skills/playwright-cli/references/request-mocking.md +87 -0
- package/skills/playwright-cli/references/running-code.md +241 -0
- package/skills/playwright-cli/references/session-management.md +225 -0
- package/skills/playwright-cli/references/storage-state.md +275 -0
- package/skills/playwright-cli/references/test-generation.md +433 -0
- package/skills/playwright-cli/references/tracing.md +139 -0
- package/skills/playwright-cli/references/video-recording.md +143 -0
- package/skills/playwright-cli-case-generator/SKILL.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { getRunsJsonlPath,
|
|
4
|
+
import { getRunsJsonlPath, getStatesRoot, } from "./run-store.js";
|
|
5
|
+
import { TASK_POOL_STATE_ERROR_CODES, } from "./types.js";
|
|
5
6
|
export const taskPoolRunFactSchema = z.object({
|
|
6
|
-
schemaVersion: z.literal(1),
|
|
7
|
-
|
|
7
|
+
schemaVersion: z.literal(1),
|
|
8
|
+
batchRunId: z.string().min(1),
|
|
9
|
+
workerRunId: z.string().min(1),
|
|
10
|
+
taskId: z.string().min(1),
|
|
11
|
+
featureId: z.string().min(1),
|
|
12
|
+
status: z.enum(["succeeded", "failed", "run-error"]),
|
|
13
|
+
recordedAt: z.string().datetime(),
|
|
8
14
|
}).passthrough();
|
|
9
15
|
export const taskPoolStateFactSchema = z.object({
|
|
10
|
-
taskId: z.string().min(1),
|
|
16
|
+
taskId: z.string().min(1),
|
|
17
|
+
status: z.enum([
|
|
18
|
+
"Draft",
|
|
19
|
+
"Ready",
|
|
20
|
+
"Queued",
|
|
21
|
+
"Running",
|
|
22
|
+
"AgentCompleted",
|
|
23
|
+
"VerificationRunning",
|
|
24
|
+
"HumanReview",
|
|
25
|
+
"Done",
|
|
26
|
+
"Failed",
|
|
27
|
+
"Blocked",
|
|
28
|
+
"Abandoned",
|
|
29
|
+
]),
|
|
30
|
+
updatedAt: z.string().datetime(),
|
|
31
|
+
schemaVersion: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
32
|
+
featureId: z.string().min(1).optional(),
|
|
11
33
|
}).passthrough();
|
|
12
34
|
export async function readValidTaskPoolRuns(repoRoot) {
|
|
13
35
|
const records = [];
|
|
14
36
|
const warnings = [];
|
|
15
37
|
try {
|
|
16
38
|
const raw = await readFile(getRunsJsonlPath(repoRoot), "utf-8");
|
|
17
|
-
for (const [index, line] of raw.split(/\r?\n/).filter(Boolean).entries())
|
|
39
|
+
for (const [index, line] of raw.split(/\r?\n/).filter(Boolean).entries()) {
|
|
18
40
|
try {
|
|
19
41
|
const parsed = taskPoolRunFactSchema.safeParse(JSON.parse(line));
|
|
20
42
|
if (parsed.success)
|
|
@@ -25,6 +47,7 @@ export async function readValidTaskPoolRuns(repoRoot) {
|
|
|
25
47
|
catch {
|
|
26
48
|
warnings.push(`Task Pool runs.jsonl line ${index + 1} is corrupt`);
|
|
27
49
|
}
|
|
50
|
+
}
|
|
28
51
|
}
|
|
29
52
|
catch (error) {
|
|
30
53
|
if (!isNotFound(error))
|
|
@@ -32,23 +55,80 @@ export async function readValidTaskPoolRuns(repoRoot) {
|
|
|
32
55
|
}
|
|
33
56
|
return { records, warnings };
|
|
34
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Valid state reader for morning-report and similar consumers.
|
|
60
|
+
* - v2 feature-scoped states: included when path/content identity match.
|
|
61
|
+
* - bare taskId collisions among v2: warning, do not overwrite records.
|
|
62
|
+
* - legacy flat states: warning with migration-required code; never treated as Ready facts.
|
|
63
|
+
*/
|
|
35
64
|
export async function readValidTaskPoolStates(repoRoot) {
|
|
36
65
|
const records = {};
|
|
37
66
|
const warnings = [];
|
|
38
|
-
const stateDir =
|
|
67
|
+
const stateDir = getStatesRoot(repoRoot);
|
|
39
68
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
const entries = await readdir(stateDir, { withFileTypes: true });
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
72
|
+
const entryPath = path.join(stateDir, entry.name);
|
|
42
73
|
try {
|
|
43
|
-
const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(
|
|
44
|
-
if (parsed.success
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(entryPath, "utf-8")));
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
warnings.push(`Task Pool state is semantically invalid: ${entry.name}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const expectedTaskId = entry.name.slice(0, -5);
|
|
80
|
+
if (parsed.data.taskId !== expectedTaskId) {
|
|
81
|
+
warnings.push(`Task Pool state is semantically invalid: ${entry.name}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
warnings.push(`${TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED}: legacy state ${entry.name}`);
|
|
48
85
|
}
|
|
49
86
|
catch {
|
|
50
|
-
warnings.push(`Task Pool state is corrupt: ${entry}`);
|
|
87
|
+
warnings.push(`Task Pool state is corrupt: ${entry.name}`);
|
|
51
88
|
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.isDirectory())
|
|
92
|
+
continue;
|
|
93
|
+
const featureId = entry.name;
|
|
94
|
+
const featureDir = path.join(stateDir, featureId);
|
|
95
|
+
let featureEntries;
|
|
96
|
+
try {
|
|
97
|
+
featureEntries = await readdir(featureDir);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
warnings.push(`Task Pool feature state directory is unreadable: ${featureId}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const fileName of featureEntries) {
|
|
104
|
+
if (!fileName.endsWith(".json"))
|
|
105
|
+
continue;
|
|
106
|
+
const relative = `${featureId}/${fileName}`;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(featureDir, fileName), "utf-8")));
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
warnings.push(`Task Pool state is semantically invalid: ${relative}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const expectedTaskId = fileName.slice(0, -5);
|
|
114
|
+
const data = parsed.data;
|
|
115
|
+
if (data.taskId !== expectedTaskId ||
|
|
116
|
+
data.featureId !== featureId ||
|
|
117
|
+
data.schemaVersion !== 2) {
|
|
118
|
+
warnings.push(`${TASK_POOL_STATE_ERROR_CODES.PATH_MISMATCH}: ${relative}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (records[data.taskId]) {
|
|
122
|
+
warnings.push(`${TASK_POOL_STATE_ERROR_CODES.IDENTITY_AMBIGUOUS}: multiple features share taskId ${data.taskId}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
records[data.taskId] = data;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
warnings.push(`Task Pool state is corrupt: ${relative}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
52
132
|
}
|
|
53
133
|
catch (error) {
|
|
54
134
|
if (!isNotFound(error))
|
|
@@ -56,4 +136,9 @@ export async function readValidTaskPoolStates(repoRoot) {
|
|
|
56
136
|
}
|
|
57
137
|
return { records, warnings };
|
|
58
138
|
}
|
|
59
|
-
function isNotFound(error) {
|
|
139
|
+
function isNotFound(error) {
|
|
140
|
+
return Boolean(error &&
|
|
141
|
+
typeof error === "object" &&
|
|
142
|
+
"code" in error &&
|
|
143
|
+
error.code === "ENOENT");
|
|
144
|
+
}
|
|
@@ -23,7 +23,16 @@ export async function renderMorningReport(options) {
|
|
|
23
23
|
...(features.length === 0 ? ["- Status: no Feature Packet discovered", "- Next Action: add or locate a Feature Packet", "- Why: no shared Feature read model is available", "- Evidence: none"] : features.flatMap((feature) => {
|
|
24
24
|
const why = feature.blockingItems[0]?.message ?? `${feature.summary.tasksSucceeded}/${feature.summary.tasksTotal} tasks; ${feature.summary.requiredAcCovered}/${feature.summary.requiredAcTotal} required AC`;
|
|
25
25
|
const evidence = [feature.evidence.delivery, feature.evidence.closeout, feature.evidence.morningReport, feature.evidence.observeSnapshot, ...feature.blockingItems.flatMap((item) => item.evidence)].find(Boolean) ?? "none";
|
|
26
|
-
|
|
26
|
+
const selected = feature.planning?.selected[0];
|
|
27
|
+
const deferred = feature.planning?.deferred ?? [];
|
|
28
|
+
const blocked = feature.planning?.blocked ?? [];
|
|
29
|
+
const deferredText = deferred.length > 0
|
|
30
|
+
? deferred.map((item) => `${item.taskId} (${item.reasonCode})`).join(", ")
|
|
31
|
+
: "none";
|
|
32
|
+
const blockedText = blocked.length > 0
|
|
33
|
+
? blocked.map((item) => `${item.taskId} (${item.reasonCode}${item.blockedBy.length ? `; blockedBy ${item.blockedBy.join(",")}` : ""})`).join(", ")
|
|
34
|
+
: "none";
|
|
35
|
+
return [`### ${feature.featureId}`, "", `- Status: ${feature.status}`, `- Next Task: ${selected ? `${selected.taskId} (${selected.priority})` : "none"}`, `- Deferred: ${deferredText}`, `- Blocked: ${blockedText}`, `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`, `- Why: ${why}`, `- Evidence: ${evidence}`, ""];
|
|
27
36
|
})),
|
|
28
37
|
"## Summary",
|
|
29
38
|
"",
|
|
@@ -40,7 +49,8 @@ export async function renderMorningReport(options) {
|
|
|
40
49
|
];
|
|
41
50
|
for (const run of runs) {
|
|
42
51
|
const followUp = await followUpSummary(run, options.repoRoot);
|
|
43
|
-
|
|
52
|
+
const identity = `${run.featureId}/${run.taskId}`;
|
|
53
|
+
lines.push(`| ${identity} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${followUp ?? run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
|
|
44
54
|
}
|
|
45
55
|
if (followUps > 0) {
|
|
46
56
|
lines.push("", "## Human Actions", "");
|
|
@@ -4,10 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
6
6
|
import { deriveFailureRoute, deriveFailureRouteFromError, } from "../pool/failure-routing.js";
|
|
7
|
-
import { findRunByWorkerRunId, getTaskPoolRoot,
|
|
7
|
+
import { findRunByWorkerRunId, getTaskPoolRoot, readFeatureTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
|
|
8
8
|
import { runTaskSpec, } from "../run-task/run-task.js";
|
|
9
9
|
import { formatDuration, noopProgressReporter, } from "../progress-reporter.js";
|
|
10
|
-
import {
|
|
10
|
+
import { planReadyTasks } from "../task-graph/ready-planner.js";
|
|
11
11
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
12
12
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
13
13
|
import { preflightTargetRepo } from "../preflight.js";
|
|
@@ -34,15 +34,26 @@ export async function runReadyTasks(options) {
|
|
|
34
34
|
const startedAt = now.toISOString();
|
|
35
35
|
const batchRunId = options.batchRunId ?? buildBatchRunId(now);
|
|
36
36
|
const graph = await loadTaskGraph(options.featureDir);
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
37
|
+
const featureId = graph.feature_id;
|
|
38
|
+
const states = await readFeatureTaskPoolStates(options.repoRoot, featureId);
|
|
39
|
+
const taskSpecs = await loadFeatureTaskSpecs(options.featureDir, graph);
|
|
40
|
+
const plan = planReadyTasks({
|
|
41
|
+
featureId,
|
|
42
|
+
graph,
|
|
43
|
+
taskSpecs,
|
|
44
|
+
states,
|
|
45
|
+
selectionLimit: options.limit ?? graph.nodes.length,
|
|
46
|
+
});
|
|
47
|
+
const limitedTaskIds = plan.selected.map((candidate) => candidate.taskId);
|
|
40
48
|
const tasks = [];
|
|
41
49
|
const runner = options.runTask ?? runTaskSpec;
|
|
42
50
|
const progress = options.progress ?? noopProgressReporter;
|
|
43
51
|
const total = limitedTaskIds.length;
|
|
44
52
|
let index = 0;
|
|
45
53
|
const batchStartedAt = Date.now();
|
|
54
|
+
const readyPlanPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "ready-plan.json");
|
|
55
|
+
await mkdir(path.dirname(readyPlanPath), { recursive: true });
|
|
56
|
+
await writeFile(readyPlanPath, `${JSON.stringify(plan, null, 2)}\n`, "utf-8");
|
|
46
57
|
emit(progress, {
|
|
47
58
|
type: "readyQueue.computed",
|
|
48
59
|
source: "worker",
|
|
@@ -61,7 +72,10 @@ export async function runReadyTasks(options) {
|
|
|
61
72
|
index += 1;
|
|
62
73
|
const node = graph.nodes.find((candidate) => candidate.id === taskId);
|
|
63
74
|
const taskSpecPath = path.join(options.featureDir, "tasks", node?.task ?? `${taskId}.yaml`);
|
|
64
|
-
const taskSpec = await loadTaskSpec(taskSpecPath);
|
|
75
|
+
const taskSpec = taskSpecs.get(taskId) ?? await loadTaskSpec(taskSpecPath);
|
|
76
|
+
if (taskSpec.feature_id !== featureId) {
|
|
77
|
+
throw new Error(`task ${taskId} feature_id ${taskSpec.feature_id} does not match graph feature_id ${featureId}`);
|
|
78
|
+
}
|
|
65
79
|
const retryOfWorkerRunId = states[taskId]?.retryOfWorkerRunId;
|
|
66
80
|
let workerRunId = retryOfWorkerRunId
|
|
67
81
|
? buildRetryWorkerRunId(taskId, retryOfWorkerRunId, now)
|
|
@@ -69,14 +83,14 @@ export async function runReadyTasks(options) {
|
|
|
69
83
|
buildStableWorkerRunId(taskId, taskSpec, now, controllerIdentity);
|
|
70
84
|
if (workerRunId) {
|
|
71
85
|
let existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
|
|
72
|
-
if (existing && !
|
|
86
|
+
if (existing && !runCanBeReused(existing, featureId, taskId, controllerIdentity)) {
|
|
73
87
|
workerRunId = buildControllerScopedWorkerRunId(workerRunId, controllerIdentity);
|
|
74
88
|
existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
|
|
75
|
-
if (existing && !
|
|
76
|
-
throw new Error(`workerRunId collision has incompatible controller identity: ${workerRunId}`);
|
|
89
|
+
if (existing && !runCanBeReused(existing, featureId, taskId, controllerIdentity)) {
|
|
90
|
+
throw new Error(`workerRunId collision has incompatible feature/controller identity: ${workerRunId}`);
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
|
-
if (existing) {
|
|
93
|
+
if (existing && runCanBeReused(existing, featureId, taskId, controllerIdentity)) {
|
|
80
94
|
progress.task(`task ${index}/${total} ${taskId}: reuse existing run ${workerRunId}`);
|
|
81
95
|
emit(progress, {
|
|
82
96
|
type: "task.reused",
|
|
@@ -112,6 +126,8 @@ export async function runReadyTasks(options) {
|
|
|
112
126
|
let result;
|
|
113
127
|
try {
|
|
114
128
|
await writeTaskPoolState(options.repoRoot, {
|
|
129
|
+
schemaVersion: 2,
|
|
130
|
+
featureId,
|
|
115
131
|
taskId,
|
|
116
132
|
status: "Running",
|
|
117
133
|
updatedAt: new Date().toISOString(),
|
|
@@ -277,6 +293,12 @@ export async function runReadyTasks(options) {
|
|
|
277
293
|
summary,
|
|
278
294
|
tasks,
|
|
279
295
|
batchRunPath,
|
|
296
|
+
readyPlanPath,
|
|
297
|
+
selectedTaskIds: limitedTaskIds,
|
|
298
|
+
deferredTaskIds: plan.deferred.map((candidate) => candidate.taskId),
|
|
299
|
+
orderingPolicy: plan.orderingPolicy,
|
|
300
|
+
selectionLimit: plan.selectionLimit,
|
|
301
|
+
effectiveConcurrency: plan.effectiveConcurrency,
|
|
280
302
|
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
281
303
|
};
|
|
282
304
|
await mkdir(path.dirname(batchRunPath), { recursive: true });
|
|
@@ -345,7 +367,9 @@ function buildControllerScopedWorkerRunId(baseWorkerRunId, controllerIdentity) {
|
|
|
345
367
|
.slice(0, 10);
|
|
346
368
|
return `${baseWorkerRunId}-controller-${hash}`;
|
|
347
369
|
}
|
|
348
|
-
function
|
|
370
|
+
function runCanBeReused(existing, featureId, taskId, controllerIdentity) {
|
|
371
|
+
if (existing.featureId !== featureId || existing.taskId !== taskId)
|
|
372
|
+
return false;
|
|
349
373
|
if (!existing.controllerIdentity && !controllerIdentity)
|
|
350
374
|
return true;
|
|
351
375
|
return controllerIdentitiesMatch(existing.controllerIdentity, controllerIdentity);
|
|
@@ -370,22 +394,13 @@ async function loadTaskGraph(featureDir) {
|
|
|
370
394
|
async function loadTaskSpec(taskSpecPath) {
|
|
371
395
|
return taskSpecSchema.parse(YAML.parse(await readFile(taskSpecPath, "utf-8")));
|
|
372
396
|
}
|
|
373
|
-
function
|
|
374
|
-
const
|
|
375
|
-
for (const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (state.status === "Running" || state.status === "Queued")
|
|
379
|
-
graphState[taskId] = "running";
|
|
380
|
-
if (state.status === "Failed")
|
|
381
|
-
graphState[taskId] = "failed";
|
|
382
|
-
if (state.status === "Blocked")
|
|
383
|
-
graphState[taskId] = "blocked";
|
|
397
|
+
async function loadFeatureTaskSpecs(featureDir, graph) {
|
|
398
|
+
const specs = new Map();
|
|
399
|
+
for (const node of graph.nodes) {
|
|
400
|
+
const taskSpec = await loadTaskSpec(path.join(featureDir, "tasks", node.task ?? `${node.id}.yaml`));
|
|
401
|
+
specs.set(node.id, taskSpec);
|
|
384
402
|
}
|
|
385
|
-
return
|
|
386
|
-
}
|
|
387
|
-
function isPoolReady(state) {
|
|
388
|
-
return !state || state.status === "Ready";
|
|
403
|
+
return specs;
|
|
389
404
|
}
|
|
390
405
|
function summarize(tasks) {
|
|
391
406
|
return {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const OWN_STATUS_REASON = {
|
|
2
|
+
Draft: "task-draft",
|
|
3
|
+
Queued: "task-queued",
|
|
4
|
+
Running: "task-running",
|
|
5
|
+
AgentCompleted: "task-agent-completed",
|
|
6
|
+
VerificationRunning: "task-verification-running",
|
|
7
|
+
HumanReview: "task-human-review",
|
|
8
|
+
Done: "task-done",
|
|
9
|
+
Failed: "task-failed",
|
|
10
|
+
Blocked: "task-blocked",
|
|
11
|
+
Abandoned: "task-abandoned",
|
|
12
|
+
};
|
|
13
|
+
/** Contract severity: failed > blocked > not-completed > missing-state. */
|
|
14
|
+
function dependencyReason(unmet) {
|
|
15
|
+
if (unmet.some((item) => item.state === "Failed"))
|
|
16
|
+
return "dependency-failed";
|
|
17
|
+
if (unmet.some((item) => item.state === "Blocked"))
|
|
18
|
+
return "dependency-blocked";
|
|
19
|
+
if (unmet.some((item) => item.state !== undefined))
|
|
20
|
+
return "dependency-not-completed";
|
|
21
|
+
return "dependency-missing-state";
|
|
22
|
+
}
|
|
23
|
+
function ownReason(status) {
|
|
24
|
+
if (status === "Ready")
|
|
25
|
+
return undefined;
|
|
26
|
+
return OWN_STATUS_REASON[status] ?? "task-state-invalid";
|
|
27
|
+
}
|
|
28
|
+
export function planReadyTasks(input) {
|
|
29
|
+
if (!Number.isInteger(input.selectionLimit) || input.selectionLimit < 1) {
|
|
30
|
+
throw new Error("selectionLimit must be a positive integer");
|
|
31
|
+
}
|
|
32
|
+
const eligible = [];
|
|
33
|
+
const blocked = [];
|
|
34
|
+
for (const [graphIndex, node] of input.graph.nodes.entries()) {
|
|
35
|
+
const spec = input.taskSpecs.get(node.id);
|
|
36
|
+
if (!spec) {
|
|
37
|
+
blocked.push({
|
|
38
|
+
featureId: input.featureId,
|
|
39
|
+
taskId: node.id,
|
|
40
|
+
reasonCode: "task-spec-missing",
|
|
41
|
+
reason: "TaskSpec is missing",
|
|
42
|
+
blockedBy: [],
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (spec.id !== node.id || spec.type !== node.type || !same(spec.depends_on, node.depends_on)) {
|
|
47
|
+
blocked.push({
|
|
48
|
+
featureId: input.featureId,
|
|
49
|
+
taskId: node.id,
|
|
50
|
+
priority: spec.priority,
|
|
51
|
+
reasonCode: "task-spec-mismatch",
|
|
52
|
+
reason: "TaskSpec does not match task graph",
|
|
53
|
+
blockedBy: [],
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const state = input.states[node.id];
|
|
58
|
+
if (state) {
|
|
59
|
+
const own = ownReason(state.status);
|
|
60
|
+
if (own) {
|
|
61
|
+
blocked.push({
|
|
62
|
+
featureId: input.featureId,
|
|
63
|
+
taskId: node.id,
|
|
64
|
+
priority: spec.priority,
|
|
65
|
+
reasonCode: own,
|
|
66
|
+
reason: own === "task-state-invalid"
|
|
67
|
+
? `task status is invalid: ${String(state.status)}`
|
|
68
|
+
: `task status is ${state.status}`,
|
|
69
|
+
blockedBy: [],
|
|
70
|
+
currentStatus: state.status,
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const deps = node.depends_on.map((id) => ({ id, state: input.states[id]?.status }));
|
|
76
|
+
const unmet = deps.filter(({ state: depState }) => depState !== "Done");
|
|
77
|
+
if (unmet.length) {
|
|
78
|
+
blocked.push({
|
|
79
|
+
featureId: input.featureId,
|
|
80
|
+
taskId: node.id,
|
|
81
|
+
priority: spec.priority,
|
|
82
|
+
reasonCode: dependencyReason(unmet),
|
|
83
|
+
reason: "dependencies are not completed",
|
|
84
|
+
blockedBy: unmet.map((item) => item.id),
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
eligible.push({
|
|
89
|
+
featureId: input.featureId,
|
|
90
|
+
taskId: node.id,
|
|
91
|
+
title: spec.title,
|
|
92
|
+
type: spec.type,
|
|
93
|
+
priority: spec.priority,
|
|
94
|
+
riskLevel: spec.risk_level,
|
|
95
|
+
graphIndex,
|
|
96
|
+
reason: "dependencies-satisfied",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
eligible.sort((a, b) => priority(a.priority) - priority(b.priority) ||
|
|
100
|
+
a.graphIndex - b.graphIndex ||
|
|
101
|
+
ascii(a.taskId, b.taskId));
|
|
102
|
+
const selected = eligible.slice(0, input.selectionLimit);
|
|
103
|
+
const deferred = eligible.slice(input.selectionLimit).map((candidate) => ({
|
|
104
|
+
...candidate,
|
|
105
|
+
reasonCode: "selection-limit",
|
|
106
|
+
selectedAhead: selected.map((item) => item.taskId),
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
featureId: input.featureId,
|
|
111
|
+
executionMode: "serial",
|
|
112
|
+
effectiveConcurrency: 1,
|
|
113
|
+
selectionLimit: input.selectionLimit,
|
|
114
|
+
orderingPolicy: ["priority:P0>P1>P2>P3", "graph-order", "task-id"],
|
|
115
|
+
eligible,
|
|
116
|
+
blocked,
|
|
117
|
+
selected,
|
|
118
|
+
deferred,
|
|
119
|
+
summary: {
|
|
120
|
+
total: input.graph.nodes.length,
|
|
121
|
+
eligible: eligible.length,
|
|
122
|
+
blocked: blocked.length,
|
|
123
|
+
selected: selected.length,
|
|
124
|
+
deferred: deferred.length,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function priority(value) {
|
|
129
|
+
return { P0: 0, P1: 1, P2: 2, P3: 3 }[value];
|
|
130
|
+
}
|
|
131
|
+
function ascii(a, b) {
|
|
132
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
function same(a, b) {
|
|
135
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
136
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
|
+
export const BACKEND_TEST_ANALYSIS_SCHEMA_ID = "backend-test-analysis-v1";
|
|
7
|
+
const sourceRefSchema = z.string().min(1);
|
|
8
|
+
const fieldSchema = z.object({
|
|
9
|
+
name: z.string().min(1),
|
|
10
|
+
type: z.string().min(1).optional(),
|
|
11
|
+
required: z.boolean().optional(),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
}).strict();
|
|
14
|
+
const errorCaseSchema = z.object({
|
|
15
|
+
status: z.number().int().min(400).max(599).optional(),
|
|
16
|
+
code: z.string().min(1).optional(),
|
|
17
|
+
messageField: z.string().min(1).optional(),
|
|
18
|
+
description: z.string().min(1),
|
|
19
|
+
}).strict();
|
|
20
|
+
export const backendTestAnalysisContractSchema = z.object({
|
|
21
|
+
schemaVersion: z.literal(1),
|
|
22
|
+
sourceBinding: z.object({
|
|
23
|
+
taskId: z.string().min(1),
|
|
24
|
+
requirementPath: z.string().min(1),
|
|
25
|
+
requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
26
|
+
referencePaths: z.array(z.string().min(1)),
|
|
27
|
+
requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
|
|
28
|
+
}).strict(),
|
|
29
|
+
acceptanceCriteria: z.array(z.object({
|
|
30
|
+
id: z.string().regex(/^AC-[A-Z0-9]+(?:-[A-Z0-9]+)*$/),
|
|
31
|
+
text: z.string().min(1),
|
|
32
|
+
sourceRef: sourceRefSchema,
|
|
33
|
+
}).strict()),
|
|
34
|
+
endpoints: z.array(z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
|
|
37
|
+
path: z.string().startsWith("/"),
|
|
38
|
+
requestFields: z.array(fieldSchema),
|
|
39
|
+
responseFields: z.array(fieldSchema),
|
|
40
|
+
successStatuses: z.array(z.number().int().min(100).max(399)),
|
|
41
|
+
errorCases: z.array(errorCaseSchema),
|
|
42
|
+
}).strict()),
|
|
43
|
+
dataModels: z.array(z.object({ id: z.string().min(1), description: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
|
|
44
|
+
businessRules: z.array(z.object({ id: z.string().min(1), text: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
|
|
45
|
+
stateTransitions: z.array(z.object({ from: z.string().min(1), to: z.string().min(1), trigger: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
|
|
46
|
+
boundaryConstraints: z.array(z.object({ field: z.string().min(1), constraint: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
|
|
47
|
+
externalDependencies: z.array(z.object({ name: z.string().min(1), description: z.string().min(1), sourceRef: sourceRefSchema.optional() }).strict()),
|
|
48
|
+
risks: z.array(z.object({ description: z.string().min(1), sourceRef: sourceRefSchema.optional() }).strict()),
|
|
49
|
+
evidenceGaps: z.array(z.object({ description: z.string().min(1), requirementId: z.string().optional(), sourceRef: sourceRefSchema.optional() }).strict()),
|
|
50
|
+
}).strict();
|
|
51
|
+
const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
|
|
52
|
+
const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
|
|
53
|
+
function secretIssues(value, at = "$", issues = []) {
|
|
54
|
+
if (typeof value === "string" && SECRET_VALUE.test(value))
|
|
55
|
+
issues.push(`${at}: secret-like value is forbidden`);
|
|
56
|
+
if (Array.isArray(value))
|
|
57
|
+
value.forEach((item, index) => secretIssues(item, `${at}[${index}]`, issues));
|
|
58
|
+
else if (value && typeof value === "object") {
|
|
59
|
+
for (const [key, child] of Object.entries(value)) {
|
|
60
|
+
if (SECRET_KEY.test(key))
|
|
61
|
+
issues.push(`${at}.${key}: secret-shaped field name is forbidden`);
|
|
62
|
+
secretIssues(child, `${at}.${key}`, issues);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
export function extractStrictJsonObject(text) {
|
|
68
|
+
const trimmed = text.trim();
|
|
69
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
70
|
+
return JSON.parse(trimmed);
|
|
71
|
+
const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
|
|
72
|
+
if (blocks.length !== 1 || trimmed.replace(blocks[0][0], "").trim()) {
|
|
73
|
+
throw new Error("analysis output must be one pure JSON object or one fenced json block with no trailing text");
|
|
74
|
+
}
|
|
75
|
+
return JSON.parse(blocks[0][1]);
|
|
76
|
+
}
|
|
77
|
+
function assertSourceBinding(contract, binding) {
|
|
78
|
+
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
79
|
+
const references = binding.sources.filter((source) => source.kind === "reference").map((source) => source.path).sort();
|
|
80
|
+
const actualReferences = [...contract.sourceBinding.referencePaths].sort();
|
|
81
|
+
if (!requirement || contract.sourceBinding.taskId !== binding.taskId || contract.sourceBinding.requirementPath !== requirement.path || contract.sourceBinding.requirementSha256 !== requirement.sha256) {
|
|
82
|
+
throw new Error("analysis source binding does not match DAG requirement source");
|
|
83
|
+
}
|
|
84
|
+
if (JSON.stringify(actualReferences) !== JSON.stringify(references))
|
|
85
|
+
throw new Error("analysis referencePaths do not match DAG source binding");
|
|
86
|
+
if (JSON.stringify(contract.sourceBinding.requirementIds) !== JSON.stringify(binding.requirementIds))
|
|
87
|
+
throw new Error("analysis requirementIds do not match DAG source binding");
|
|
88
|
+
const explicitAc = binding.requirementIds.filter((id) => id.startsWith("AC-"));
|
|
89
|
+
const contractAc = contract.acceptanceCriteria.map((item) => item.id);
|
|
90
|
+
for (const id of explicitAc)
|
|
91
|
+
if (!contractAc.includes(id) && !contract.evidenceGaps.some((gap) => gap.requirementId === id))
|
|
92
|
+
throw new Error(`analysis contract does not cover explicit acceptance criterion ${id}`);
|
|
93
|
+
}
|
|
94
|
+
export async function materializeBackendTestAnalysisContract(input) {
|
|
95
|
+
if (!input.sourceBinding)
|
|
96
|
+
throw new Error("backend-test analysis gate requires DAG sourceBinding");
|
|
97
|
+
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) || !/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir))
|
|
98
|
+
throw new Error("unsafe structured artifact path");
|
|
99
|
+
const nodePath = path.join(input.runDir, `${input.fromNodeId}.json`);
|
|
100
|
+
const record = JSON.parse(await readFile(nodePath, "utf8"));
|
|
101
|
+
const raw = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = extractStrictJsonObject(raw);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
const secrets = secretIssues(parsed);
|
|
110
|
+
if (secrets.length)
|
|
111
|
+
throw new Error(`invalid-output: ${secrets.join("; ")}`);
|
|
112
|
+
const result = backendTestAnalysisContractSchema.safeParse(parsed);
|
|
113
|
+
if (!result.success)
|
|
114
|
+
throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
115
|
+
assertSourceBinding(result.data, input.sourceBinding);
|
|
116
|
+
const relativePath = path.posix.join(input.outputDir, input.artifactName);
|
|
117
|
+
const artifactPath = await writeDagRunJsonArtifact(input.runDir, relativePath, result.data);
|
|
118
|
+
const normalized = `${JSON.stringify(result.data, null, 2)}\n`;
|
|
119
|
+
return { path: artifactPath, sha256: createHash("sha256").update(normalized).digest("hex"), schemaId: BACKEND_TEST_ANALYSIS_SCHEMA_ID };
|
|
120
|
+
}
|