@tea-agent/loop-agent 0.38.2 → 0.38.3
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 +18 -0
- package/README.md +2 -0
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +58 -1
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +56 -2
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
- package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-Bj2M8iO5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
- package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DTZOKgAn.js} +142 -63
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-DdnapKIH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-BVjAT9b8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/observe/node-input.js +72 -3
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/views/dag-inspector.js +51 -0
- package/dist/worker/observe/static/views/session-timeline.js +18 -5
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +56 -4
- package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/failure-category.js +4 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +140 -39
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +79 -101
- package/dist/workflows/dag/node-execution.js +119 -8
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/rerun-plan.js +22 -3
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +23 -0
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/package.json +8 -4
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
|
@@ -21,7 +21,6 @@ import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-em
|
|
|
21
21
|
import { discoverProjectGovernancePresence } from "./project-governance-context.js";
|
|
22
22
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
23
23
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
24
|
-
import { REQUIREMENT_FACT_ROLES } from "../../task/source-prepare/artifact-meta.js";
|
|
25
24
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
26
25
|
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
27
26
|
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
@@ -1502,27 +1501,11 @@ function buildSourceContextBlock(sources) {
|
|
|
1502
1501
|
.relative(path.join(sources.taskDir, "source"), reference.path)
|
|
1503
1502
|
.replaceAll(path.sep, "/");
|
|
1504
1503
|
const referenceRef = toDagSourcePath(sources, reference.path);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const referenceExcerpt = isFactRole
|
|
1511
|
-
? {
|
|
1512
|
-
text: reference.markdown.trim(),
|
|
1513
|
-
truncated: false,
|
|
1514
|
-
originalChars: reference.markdown.trim().length,
|
|
1515
|
-
maxChars: Number.POSITIVE_INFINITY,
|
|
1516
|
-
}
|
|
1517
|
-
: excerptMarkdown(reference.markdown, {
|
|
1518
|
-
sourceRef: referenceRef,
|
|
1519
|
-
});
|
|
1520
|
-
boundReadPaths.push(`- reference ${relativePath}${isFactRole ? " (full source)" : ""}: ${referenceRef}`);
|
|
1521
|
-
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, ...(isFactRole
|
|
1522
|
-
? [
|
|
1523
|
-
`Full source injected (role: ${reference.role}) — complete and authoritative; no excerpt truncation applied.`,
|
|
1524
|
-
]
|
|
1525
|
-
: []), referenceExcerpt.text);
|
|
1504
|
+
const referenceExcerpt = excerptMarkdown(reference.markdown, {
|
|
1505
|
+
sourceRef: referenceRef,
|
|
1506
|
+
});
|
|
1507
|
+
boundReadPaths.push(`- reference ${relativePath}: ${referenceRef}`);
|
|
1508
|
+
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, referenceExcerpt.text);
|
|
1526
1509
|
}
|
|
1527
1510
|
parts.push("## Bound source read paths", ...boundReadPaths, "Use these repository-readable paths for any Pi read-tool calls. Bound files under `.harness/tasks/<taskId>/source/**` are read-only inputs: reading them is allowed even though writing `.harness/**` is forbidden.", "Never resolve task-relative citations such as `source/需求.md` or `source/references/*` against the repository root, invent `source/<taskId>/...`, search for substitutes, or fall back to `docs/**` when a bound read fails.", "## Task config summary", `- taskId: ${sources.taskConfig.taskId}`, `- flow: ${sources.taskConfig.flow}`, `- complexity: ${sources.taskConfig.complexity}`, `- contextProfile: ${sources.taskConfig.contextProfile}`, `- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none — review before execute)"}`, `- forbiddenPaths: ${sources.taskConfig.forbiddenPaths.join(", ") || "(none)"}`, '- Pi DAG nodes are read-only unless toolProfile="write" is explicitly selected for a bounded writer node.', "- Agent DAG read-only nodes must not write root artifacts/**; root artifacts/ is not a per-node scratchpad.", `- Derived execution contract and immutable references live under the Bound source read paths above (not as repo-root \`source/...\`).`);
|
|
1528
1511
|
if (sources.taskConfig.hardConstraints.length > 0) {
|
|
@@ -1560,49 +1543,10 @@ async function loadMaterializedSourceReferences(sourceDir) {
|
|
|
1560
1543
|
}
|
|
1561
1544
|
await collect(referenceDir);
|
|
1562
1545
|
referencePaths.sort((left, right) => left.localeCompare(right));
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
.replaceAll(path.sep, "/");
|
|
1568
|
-
const role = roleByPath.get(relative);
|
|
1569
|
-
return {
|
|
1570
|
-
path: filePath,
|
|
1571
|
-
markdown: await readFile(filePath, "utf-8"),
|
|
1572
|
-
...(role !== undefined ? { role } : {}),
|
|
1573
|
-
};
|
|
1574
|
-
}));
|
|
1575
|
-
}
|
|
1576
|
-
/**
|
|
1577
|
-
* Reads `source-manifest.json` into a materializedPath → role map so the DAG
|
|
1578
|
-
* generator can tell fact-source references (requirement/acceptance) apart
|
|
1579
|
-
* from archival ones (analysis/clarification/design). A missing or malformed
|
|
1580
|
-
* manifest yields an empty map; such references keep the bounded-excerpt
|
|
1581
|
-
* treatment instead of being injected in full.
|
|
1582
|
-
*/
|
|
1583
|
-
async function loadReferenceRoles(sourceDir) {
|
|
1584
|
-
const roleByPath = new Map();
|
|
1585
|
-
const manifestPath = path.join(sourceDir, "source-manifest.json");
|
|
1586
|
-
let raw;
|
|
1587
|
-
try {
|
|
1588
|
-
raw = await readFile(manifestPath, "utf-8");
|
|
1589
|
-
}
|
|
1590
|
-
catch {
|
|
1591
|
-
return roleByPath;
|
|
1592
|
-
}
|
|
1593
|
-
try {
|
|
1594
|
-
const manifest = JSON.parse(raw);
|
|
1595
|
-
for (const document of manifest.documents ?? []) {
|
|
1596
|
-
if (typeof document.materializedPath === "string" &&
|
|
1597
|
-
typeof document.role === "string") {
|
|
1598
|
-
roleByPath.set(document.materializedPath.replaceAll(path.sep, "/"), document.role);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
catch {
|
|
1603
|
-
// A malformed manifest must never break reference injection.
|
|
1604
|
-
}
|
|
1605
|
-
return roleByPath;
|
|
1546
|
+
return Promise.all(referencePaths.map(async (filePath) => ({
|
|
1547
|
+
path: filePath,
|
|
1548
|
+
markdown: await readFile(filePath, "utf-8"),
|
|
1549
|
+
})));
|
|
1606
1550
|
}
|
|
1607
1551
|
export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
1608
1552
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
@@ -2433,7 +2377,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2433
2377
|
"## Runtime contract skeleton (deterministic and protected)",
|
|
2434
2378
|
JSON.stringify(frontendContractSkeleton),
|
|
2435
2379
|
"",
|
|
2436
|
-
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then
|
|
2380
|
+
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then writes a hash-bound canonical JSON artifact; downstream review and prewrite consume that artifact path, not planner stdout.",
|
|
2437
2381
|
"",
|
|
2438
2382
|
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
2439
2383
|
"- schemaId",
|
|
@@ -2550,11 +2494,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2550
2494
|
`Non-static targets (type unit/component/integration/mock) MUST set file to a concrete code file inside the implementation writeSet (task allowedPaths); the prewrite gate rejects any non-static target whose file falls outside the writeSet.`,
|
|
2551
2495
|
`Command-level checks that run project-wide (all tests, typecheck, build, governance) MUST use type "static" and must NOT be bound as non-static targets with file=package.json/tsconfig.json/vite.config.ts/scripts/*. Static targets are exempt from the writeSet containment check.`,
|
|
2552
2496
|
].join("\n");
|
|
2553
|
-
const mandatorySourceReadInstruction = [
|
|
2554
|
-
"## Mandatory full source read before contracting",
|
|
2555
|
-
"Before producing this contract/plan, use the Pi read tool to read the FULL bound source files (需求.md, 执行约束.md, and every `references/*` Bound readPath listed above) — the inline copies above may be truncated excerpts, and requirement/acceptance references are authoritative only in their full form.",
|
|
2556
|
-
"Do not drop scope fields, acceptance criteria, non-goals, UI states, or column/field definitions that exist in the full sources but are absent from the inline excerpts; if a field appears in the full source, it belongs in the contract.",
|
|
2557
|
-
].join("\n");
|
|
2558
2497
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2559
2498
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2560
2499
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2751,7 +2690,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2751
2690
|
"Read task source and produce a concise frontend implementation contract.",
|
|
2752
2691
|
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
|
|
2753
2692
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2754
|
-
mandatorySourceReadInstruction,
|
|
2755
2693
|
sourceContext,
|
|
2756
2694
|
].join("\n\n"),
|
|
2757
2695
|
},
|
|
@@ -2792,7 +2730,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2792
2730
|
allowedPaths: readOnlyPaths,
|
|
2793
2731
|
forbiddenPaths,
|
|
2794
2732
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2795
|
-
outputContract: "JSON-only patch output: one-line lead-in, then exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. The runtime applies the patch, validates it, and
|
|
2733
|
+
outputContract: "JSON-only patch output: one-line lead-in, then exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. The runtime applies the patch, validates it, and writes a hash-bound canonical JSON artifact for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.",
|
|
2796
2734
|
subtask_prompt: [
|
|
2797
2735
|
"Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Return JSON-only output containing only an editable RFC 7386 plan patch. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2798
2736
|
"The patch fields become the complete implementation plan after deterministic merge. Do not produce a separate plan document, prose mirror, or full contract.",
|
|
@@ -2806,7 +2744,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2806
2744
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2807
2745
|
verificationTargetFileInstruction,
|
|
2808
2746
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2809
|
-
mandatorySourceReadInstruction,
|
|
2810
2747
|
fixedVerificationContext,
|
|
2811
2748
|
sourceContext,
|
|
2812
2749
|
mockContextBlock,
|
|
@@ -2828,7 +2765,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2828
2765
|
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
2829
2766
|
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
|
|
2830
2767
|
subtask_prompt: [
|
|
2831
|
-
"Audit the frontend plan before implementation. frontend-plan-pi is emitted
|
|
2768
|
+
"Audit the frontend plan before implementation. frontend-plan-pi is emitted as a hash-bound canonical JSON artifact after the runtime applied and validated the planner's editable patch against its protected skeleton; read that artifact with the read tool and do not infer the contract from stdout. There is no separate plan prose.",
|
|
2832
2769
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2833
2770
|
"Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
2834
2771
|
"Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
@@ -2857,15 +2794,15 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2857
2794
|
allowedPaths: readOnlyPaths,
|
|
2858
2795
|
forbiddenPaths,
|
|
2859
2796
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2860
|
-
outputContract: "When the initial design review requests revision, return a one-line lead-in followed by exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only;
|
|
2797
|
+
outputContract: "When the initial design review requests revision, return a one-line lead-in followed by exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only; this node compiles the patch onto the original canonical artifact and writes a hash-bound revised contract. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
|
|
2861
2798
|
subtask_prompt: [
|
|
2862
2799
|
"Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
|
|
2863
2800
|
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
|
|
2864
|
-
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. Only include fields you change; omit unchanged fields (
|
|
2801
|
+
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. Only include fields you change; omit unchanged fields (this node applies the patch on the original canonical artifact). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
|
|
2865
2802
|
requirementCoverageInstruction,
|
|
2866
2803
|
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2867
2804
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2868
|
-
"Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta — this
|
|
2805
|
+
"Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta — this node compiles it onto the original canonical artifact; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block. Do not include secrets or unsafe paths.",
|
|
2869
2806
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
2870
2807
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2871
2808
|
verificationTargetFileInstruction,
|
|
@@ -3799,11 +3736,31 @@ const BACKEND_TEST_DEFAULTS = {
|
|
|
3799
3736
|
export function applyBackendTestLayoutToText(text, layout) {
|
|
3800
3737
|
if (layout.isDefault)
|
|
3801
3738
|
return text;
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3739
|
+
const replacements = [
|
|
3740
|
+
["testcase/test_", `${layout.scriptDir}/test_`],
|
|
3741
|
+
["testcase/md/", `${layout.markdownDir}/`],
|
|
3742
|
+
["testcase/**", `${layout.testRoot}/**`],
|
|
3743
|
+
["testcase/", `${layout.testRoot}/`],
|
|
3744
|
+
];
|
|
3745
|
+
replacements.sort((a, b) => b[0].length - a[0].length);
|
|
3746
|
+
let output = "";
|
|
3747
|
+
const resolvedPrefix = `${layout.testRoot}/`;
|
|
3748
|
+
for (let i = 0; i < text.length;) {
|
|
3749
|
+
if (text.startsWith(resolvedPrefix, i)) {
|
|
3750
|
+
output += resolvedPrefix;
|
|
3751
|
+
i += resolvedPrefix.length;
|
|
3752
|
+
continue;
|
|
3753
|
+
}
|
|
3754
|
+
const hit = replacements.find(([token]) => text.startsWith(token, i));
|
|
3755
|
+
if (hit) {
|
|
3756
|
+
output += hit[1];
|
|
3757
|
+
i += hit[0].length;
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
output += text[i];
|
|
3761
|
+
i += 1;
|
|
3762
|
+
}
|
|
3763
|
+
return output;
|
|
3807
3764
|
}
|
|
3808
3765
|
/**
|
|
3809
3766
|
* Builds the `node -e` command for the backend-test module manifest shell.
|
|
@@ -4369,7 +4326,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4369
4326
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
4370
4327
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
4371
4328
|
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
4372
|
-
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4329
|
+
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4373
4330
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
4374
4331
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4375
4332
|
intake.boundedSourceContext,
|
|
@@ -4479,7 +4436,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4479
4436
|
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, then perform an exact-set check: each Case's `### 测试点` set must equal (not merely contain) the union of those three binding lists; delete stale/legacy aliases and ensure every binding-list Test Point is present, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
4480
4437
|
"This is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; every label must occupy its own machine-readable list line, and a Case must never concatenate target/setup operations or multiple `Payload Contract` tokens onto one line, and explanatory prose/details must not repeat any `Payload Contract:` token; never infer missing keys or enum values. A target GET/DELETE operation with no request body must remain `Payload Contract: none` even when its setup journey performs POST/PUT with a DTO; setup payloads never redefine the target Case payload contract. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.",
|
|
4481
4438
|
"For affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints. Do not invent a concrete identifier type when the source only requires presence; for a missing-resource 404 path with unspecified identifier syntax/type, synchronize the Case to a create-delete-derived valid identifier journey rather than an arbitrary UUID/text placeholder.",
|
|
4482
|
-
"Scenario Partitions synchronization: when README declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Directly add missing slot rows/Cases
|
|
4439
|
+
"Scenario Partitions synchronization: when README declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Directly add missing slot rows/Cases. You may delete an illegal Partition row that has no source-backed finite domain, together with its derived `TP-SP-*` slots/Cases. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
4483
4440
|
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in README or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
|
|
4484
4441
|
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
4485
4442
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
@@ -4958,15 +4915,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4958
4915
|
writeSet: ragWriteSet,
|
|
4959
4916
|
allowedPaths: [...ragWriteSet],
|
|
4960
4917
|
forbiddenPaths: forbidden,
|
|
4961
|
-
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage.",
|
|
4962
|
-
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package.",
|
|
4918
|
+
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.",
|
|
4919
|
+
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
|
|
4963
4920
|
shell: {
|
|
4964
|
-
commands: [
|
|
4965
|
-
|
|
4966
|
-
"node -e",
|
|
4967
|
-
JSON.stringify("const fs=require('fs'),path=require('path');const dest='testcase/frontend/rag/standard-scenarios.v1.json';const candidates=[path.join('docs','templates','frontend-test-standard-scenarios.v1.json')];let src=null;for(const c of candidates){if(fs.existsSync(c)){src=c;break;}}fs.mkdirSync(path.dirname(dest),{recursive:true});if(src){fs.copyFileSync(src,dest);process.stdout.write(JSON.stringify({status:'copied',from:src,to:dest}));}else{const minimal={schemaVersion:1,id:'frontend-test-standard-scenarios-v1',scenarios:[{id:'STD-FE-SMOKE-ENTRY',title:'入口可打开',category:'smoke',priority:'must',testPoints:['open'],minCases:1}]};fs.writeFileSync(dest,JSON.stringify(minimal,null,2)+'\n');process.stdout.write(JSON.stringify({status:'fallback',to:dest}));}"),
|
|
4968
|
-
].join(" "),
|
|
4969
|
-
],
|
|
4921
|
+
commands: [],
|
|
4922
|
+
frontendTestStandardScenarios: {},
|
|
4970
4923
|
cwd: ".",
|
|
4971
4924
|
timeoutMs: 60_000,
|
|
4972
4925
|
},
|
|
@@ -5002,15 +4955,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5002
4955
|
writeSet: ragWriteSet,
|
|
5003
4956
|
allowedPaths: [...ragWriteSet],
|
|
5004
4957
|
forbiddenPaths: forbidden,
|
|
5005
|
-
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (
|
|
5006
|
-
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/
|
|
4958
|
+
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
|
|
4959
|
+
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
|
|
5007
4960
|
shell: {
|
|
5008
|
-
commands: [
|
|
5009
|
-
|
|
5010
|
-
"node -e",
|
|
5011
|
-
JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl from context.md');baseUrl=baseUrl.replace(/[)\\}\\],."'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
5012
|
-
].join(" "),
|
|
5013
|
-
],
|
|
4961
|
+
commands: [],
|
|
4962
|
+
frontendTestEnvironmentProbe: {},
|
|
5014
4963
|
cwd: ".",
|
|
5015
4964
|
timeoutMs: 60000,
|
|
5016
4965
|
},
|
|
@@ -6478,6 +6427,13 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
|
6478
6427
|
spec = await buildSupervisedHybridDag(standard, sources);
|
|
6479
6428
|
}
|
|
6480
6429
|
applyProjectGovernanceReview(spec, template, sources);
|
|
6430
|
+
// Backend implementation templates only (D1 outer gate): assign the two
|
|
6431
|
+
// Pi extension buckets after all template-specific nodes exist.
|
|
6432
|
+
if (template === "standard-dag" ||
|
|
6433
|
+
template === "review-gated-dag" ||
|
|
6434
|
+
template === "supervised-implementation") {
|
|
6435
|
+
applyBackendPiExtensionBuckets(spec);
|
|
6436
|
+
}
|
|
6481
6437
|
// New generate path always emits DagSpec v4 + bindings.
|
|
6482
6438
|
spec.version = 4;
|
|
6483
6439
|
if (!spec.runtimeContract) {
|
|
@@ -6545,6 +6501,28 @@ function cloneTask(task, patch = {}) {
|
|
|
6545
6501
|
* Dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
|
|
6546
6502
|
* never overwrites an explicit retryPolicy a task already declares.
|
|
6547
6503
|
*/
|
|
6504
|
+
/**
|
|
6505
|
+
* Backend-implementation Pi extension buckets (plan 2026-08-21 D3):
|
|
6506
|
+
* - read side: every non-writer, non-closeout Pi node gets navigation
|
|
6507
|
+
* extensions (pi-codegraph + pi-lens);
|
|
6508
|
+
* - write side: bounded writers get only pi-codegraph (pi-lens registers
|
|
6509
|
+
* ast_grep_replace, which can bypass the writeSet — not loading it is
|
|
6510
|
+
* simpler and safer than filtering after load);
|
|
6511
|
+
* - everything else (closeout, shell, static, other templates) stays closed.
|
|
6512
|
+
* Idempotent: never overwrites an explicit piExtensions a task declares.
|
|
6513
|
+
*/
|
|
6514
|
+
function applyBackendPiExtensionBuckets(spec) {
|
|
6515
|
+
for (const task of spec.tasks) {
|
|
6516
|
+
if (task.piExtensions !== undefined)
|
|
6517
|
+
continue;
|
|
6518
|
+
if (task.executor !== "pi")
|
|
6519
|
+
continue;
|
|
6520
|
+
if (task.id === "closeout-pi")
|
|
6521
|
+
continue;
|
|
6522
|
+
task.piExtensions =
|
|
6523
|
+
task.toolProfile === "write" ? ["pi-codegraph"] : ["pi-codegraph", "pi-lens"];
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6548
6526
|
function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
6549
6527
|
for (const task of spec.tasks) {
|
|
6550
6528
|
if (task.retryPolicy !== undefined)
|
|
@@ -15,6 +15,8 @@ import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from
|
|
|
15
15
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
16
16
|
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
17
17
|
import "./contract-validator-registrations.js";
|
|
18
|
+
import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
|
|
19
|
+
import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
|
|
18
20
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
19
21
|
import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
|
|
20
22
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
@@ -135,6 +137,11 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
|
135
137
|
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
|
|
136
138
|
"The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
|
|
137
139
|
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
140
|
+
"The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
|
|
141
|
+
"```openspec-citations",
|
|
142
|
+
'{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
|
|
143
|
+
'{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
|
|
144
|
+
"```",
|
|
138
145
|
];
|
|
139
146
|
}
|
|
140
147
|
if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
|
|
@@ -142,6 +149,11 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
|
142
149
|
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
|
|
143
150
|
"The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
|
|
144
151
|
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
152
|
+
"The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
|
|
153
|
+
"```openspec-citations",
|
|
154
|
+
'{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
|
|
155
|
+
'{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
|
|
156
|
+
"```",
|
|
145
157
|
];
|
|
146
158
|
}
|
|
147
159
|
return [
|
|
@@ -731,10 +743,17 @@ export async function executeDagNode(input) {
|
|
|
731
743
|
let terminalResult;
|
|
732
744
|
let previousFailureCategory;
|
|
733
745
|
let previousProtocolReason;
|
|
746
|
+
let pendingStructuredArtifact;
|
|
747
|
+
let lastRepairAllowlist = [];
|
|
734
748
|
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
735
749
|
const attemptStartedAt = new Date().toISOString();
|
|
736
750
|
node.currentAttempt = attemptNumber;
|
|
737
751
|
node.livenessStatus = "active";
|
|
752
|
+
// Capture the session-events.jsonl length BEFORE the executor appends this
|
|
753
|
+
// attempt's events, so the post-attempt repair tool audit can be scoped to
|
|
754
|
+
// exactly this attempt's segment (B1: earlier attempts legitimately use
|
|
755
|
+
// ls/find and must not be attributed to the repair attempt).
|
|
756
|
+
const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
|
|
738
757
|
let result;
|
|
739
758
|
try {
|
|
740
759
|
validateRepairArtifactGateBeforeShell({
|
|
@@ -757,12 +776,53 @@ export async function executeDagNode(input) {
|
|
|
757
776
|
(acc[i.path] ??= []).push(i.detail);
|
|
758
777
|
return acc;
|
|
759
778
|
}, {});
|
|
779
|
+
let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics);
|
|
780
|
+
if (attemptNumber > 1 &&
|
|
781
|
+
isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
|
|
782
|
+
isStructuredRepairableFailureCategory(previousFailureCategory) &&
|
|
783
|
+
(await hasNonEmptyStructuredCandidate({
|
|
784
|
+
runDir,
|
|
785
|
+
nodeId,
|
|
786
|
+
beforeAttempt: attemptNumber,
|
|
787
|
+
}))) {
|
|
788
|
+
const originalCanonical = task.structuredContractOutput?.schemaId ===
|
|
789
|
+
"frontend-implementation-contract-revision-patch-v1"
|
|
790
|
+
? (await readFrontendCanonicalCandidate(runDir, "frontend-plan-pi"))
|
|
791
|
+
: undefined;
|
|
792
|
+
const frozen = await freezeStructuredOutputRepairContext({
|
|
793
|
+
runDir,
|
|
794
|
+
runId: state.runId,
|
|
795
|
+
taskId: spec.sourceBinding?.taskId ?? task.id,
|
|
796
|
+
nodeId,
|
|
797
|
+
nodeAttempt: attemptNumber,
|
|
798
|
+
targetSchemaId: task.structuredContractOutput.schemaId,
|
|
799
|
+
skeleton: task.structuredContractOutput?.skeleton,
|
|
800
|
+
originalCanonical,
|
|
801
|
+
latestStopReason: previousFailureCategory === "structured-output-truncated"
|
|
802
|
+
? "length"
|
|
803
|
+
: previousFailureCategory,
|
|
804
|
+
});
|
|
805
|
+
lastRepairAllowlist = allowedRepairReadPaths({
|
|
806
|
+
runDir,
|
|
807
|
+
context: frozen.context,
|
|
808
|
+
contextPath: frozen.relativePath,
|
|
809
|
+
});
|
|
810
|
+
attemptPrompt = await buildStructuredOutputRepairPrompt({
|
|
811
|
+
runDir,
|
|
812
|
+
context: frozen.context,
|
|
813
|
+
contextPath: frozen.relativePath,
|
|
814
|
+
contextSha256: frozen.sha256,
|
|
815
|
+
maxAttempts,
|
|
816
|
+
schemaGuidance: frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
817
|
+
previousReason: previousProtocolReason,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
760
820
|
result = await executeNode({
|
|
761
821
|
task,
|
|
762
822
|
cwd,
|
|
763
823
|
model,
|
|
764
824
|
...(thinking ? { thinking } : {}),
|
|
765
|
-
prompt:
|
|
825
|
+
prompt: attemptPrompt,
|
|
766
826
|
attempt: attemptNumber,
|
|
767
827
|
reportActivity,
|
|
768
828
|
timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
|
|
@@ -778,6 +838,35 @@ export async function executeDagNode(input) {
|
|
|
778
838
|
durationMs: 0,
|
|
779
839
|
};
|
|
780
840
|
}
|
|
841
|
+
if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
|
|
842
|
+
const rawText = canonicalNodeOutput(result);
|
|
843
|
+
if (rawText.trim().length > 0) {
|
|
844
|
+
await persistStructuredAttemptRaw({
|
|
845
|
+
runDir,
|
|
846
|
+
nodeId,
|
|
847
|
+
attempt: attemptNumber,
|
|
848
|
+
text: rawText,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (attemptNumber > 1 && lastRepairAllowlist.length > 0) {
|
|
852
|
+
const audit = await auditRepairAttemptToolUse({
|
|
853
|
+
runDir,
|
|
854
|
+
nodeId,
|
|
855
|
+
allowedPaths: lastRepairAllowlist,
|
|
856
|
+
fromByteOffset: attemptSessionEventsOffset,
|
|
857
|
+
});
|
|
858
|
+
if (!audit.ok) {
|
|
859
|
+
result = {
|
|
860
|
+
...result,
|
|
861
|
+
ok: false,
|
|
862
|
+
failureCategory: GOVERNANCE_BLOCKED_CATEGORY,
|
|
863
|
+
stderr: [result.stderr, audit.reason]
|
|
864
|
+
.filter(Boolean)
|
|
865
|
+
.join("\n"),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
781
870
|
// R0: executor ok=true still fails closed when outputProtocol is violated.
|
|
782
871
|
// Valid semantic results (e.g. VERDICT: request-revision) pass validation.
|
|
783
872
|
if (result.ok && task.outputProtocol) {
|
|
@@ -814,16 +903,17 @@ export async function executeDagNode(input) {
|
|
|
814
903
|
structuredContractOutput: task.structuredContractOutput,
|
|
815
904
|
});
|
|
816
905
|
if (!contractCheck.ok) {
|
|
817
|
-
// A provider stopReason=length means the response was cut before the
|
|
818
|
-
// JSON contract could complete; separate it from an ordinary bad
|
|
819
|
-
// contract so the retry switches to a JSON-only output strategy.
|
|
820
906
|
const attemptStopReason = result.stopReason;
|
|
907
|
+
const failureCategory = contractCheck.classification === "governance-blocked"
|
|
908
|
+
? GOVERNANCE_BLOCKED_CATEGORY
|
|
909
|
+
: attemptStopReason === "length" ||
|
|
910
|
+
contractCheck.classification === "truncated"
|
|
911
|
+
? "structured-output-truncated"
|
|
912
|
+
: "invalid-output";
|
|
821
913
|
result = {
|
|
822
914
|
...result,
|
|
823
915
|
ok: false,
|
|
824
|
-
failureCategory
|
|
825
|
-
? "structured-output-truncated"
|
|
826
|
-
: "invalid-output",
|
|
916
|
+
failureCategory,
|
|
827
917
|
stderr: [result.stderr, contractCheck.reason]
|
|
828
918
|
.filter(Boolean)
|
|
829
919
|
.join("\n"),
|
|
@@ -838,6 +928,9 @@ export async function executeDagNode(input) {
|
|
|
838
928
|
stdout: contractCheck.normalizedText,
|
|
839
929
|
};
|
|
840
930
|
}
|
|
931
|
+
if (contractCheck.artifact) {
|
|
932
|
+
pendingStructuredArtifact = contractCheck.artifact;
|
|
933
|
+
}
|
|
841
934
|
previousProtocolReason = undefined;
|
|
842
935
|
}
|
|
843
936
|
}
|
|
@@ -908,13 +1001,26 @@ export async function executeDagNode(input) {
|
|
|
908
1001
|
break;
|
|
909
1002
|
const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
|
|
910
1003
|
const retryable = retryPolicy !== undefined &&
|
|
1004
|
+
result.failureCategory !== GOVERNANCE_BLOCKED_CATEGORY &&
|
|
911
1005
|
(isRetryablePiFailureCategory(result.failureCategory, {
|
|
912
1006
|
retryCategories: retryPolicy.retryCategories,
|
|
913
1007
|
}) ||
|
|
914
1008
|
(result.failureCategory === "protocol-invalid" &&
|
|
915
1009
|
task.outputProtocol?.retryOnInvalid === true));
|
|
916
|
-
if (!canRetry || !retryable)
|
|
1010
|
+
if (!canRetry || !retryable) {
|
|
1011
|
+
if (!canRetry &&
|
|
1012
|
+
isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
|
|
1013
|
+
isStructuredRepairableFailureCategory(result.failureCategory)) {
|
|
1014
|
+
result = {
|
|
1015
|
+
...result,
|
|
1016
|
+
failureCategory: STRUCTURED_REPAIR_EXHAUSTED_CATEGORY,
|
|
1017
|
+
};
|
|
1018
|
+
terminalResult = result;
|
|
1019
|
+
previousFailureCategory = result.failureCategory;
|
|
1020
|
+
node.failureCategory = result.failureCategory;
|
|
1021
|
+
}
|
|
917
1022
|
break;
|
|
1023
|
+
}
|
|
918
1024
|
const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
|
|
919
1025
|
if (delayMs > 0) {
|
|
920
1026
|
const backoffStartedAt = new Date().toISOString();
|
|
@@ -1063,6 +1169,11 @@ export async function executeDagNode(input) {
|
|
|
1063
1169
|
node.structuredArtifactSchemaId = "backend-test-result-v1";
|
|
1064
1170
|
}
|
|
1065
1171
|
}
|
|
1172
|
+
else if (pendingStructuredArtifact) {
|
|
1173
|
+
node.structuredArtifactPath = pendingStructuredArtifact.path;
|
|
1174
|
+
node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
|
|
1175
|
+
node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
|
|
1176
|
+
}
|
|
1066
1177
|
}
|
|
1067
1178
|
const finishedAt = new Date().toISOString();
|
|
1068
1179
|
node.finishedAt = finishedAt;
|
|
@@ -116,7 +116,21 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
|
|
|
116
116
|
].join("\n"));
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
|
-
if (!record || record.status !== "FINISHED"
|
|
119
|
+
if (!record || record.status !== "FINISHED")
|
|
120
|
+
continue;
|
|
121
|
+
if (record.structuredArtifactPath &&
|
|
122
|
+
record.structuredArtifactSchemaId ===
|
|
123
|
+
"frontend-implementation-contract-v1") {
|
|
124
|
+
sections.push([
|
|
125
|
+
`## Upstream output: ${depId}`,
|
|
126
|
+
"Validated structured artifact (read-only runner evidence — use read tool to fetch; do not edit). Do not infer the contract from stdout.",
|
|
127
|
+
`- path: ${record.structuredArtifactPath}`,
|
|
128
|
+
`- schema: ${record.structuredArtifactSchemaId}`,
|
|
129
|
+
`- sha256: ${record.structuredArtifactSha256 ?? "unknown"}`,
|
|
130
|
+
].join("\n"));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!upstreamText)
|
|
120
134
|
continue;
|
|
121
135
|
const artifactKind = stdout ? "stdout" : "assistant";
|
|
122
136
|
const artifactPath = stdout
|