@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +114 -109
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +69 -7
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +107 -20
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/governance/checks.js +1 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +133 -7
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +312 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +583 -172
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +29 -9
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +174 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +533 -79
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +6 -4
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +186 -8
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +168 -16
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +28 -17
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +239 -52
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +62 -12
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +267 -238
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -485,6 +485,9 @@ function extractSdkUsageSample(event) {
|
|
|
485
485
|
const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
|
|
486
486
|
return responseKey ? { responseKey, tokens } : { tokens };
|
|
487
487
|
}
|
|
488
|
+
function maxSdkContextTokens(samples) {
|
|
489
|
+
return samples.reduce((maximum, sample) => Math.max(maximum, sample.tokens), 0);
|
|
490
|
+
}
|
|
488
491
|
function aggregateSdkTokenUsage(samples) {
|
|
489
492
|
const identified = new Map();
|
|
490
493
|
let anonymousMaximum = 0;
|
|
@@ -537,6 +540,9 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
537
540
|
const stdoutPreview = new BoundedTextPreview("stdout");
|
|
538
541
|
const stdoutCollector = createPiJsonlStreamCollector();
|
|
539
542
|
const usageSamples = [];
|
|
543
|
+
let observedTurns = 0;
|
|
544
|
+
let contextBudgetExhausted = false;
|
|
545
|
+
let contextBudgetDetail = "";
|
|
540
546
|
// Writer-thinking-exhausted diagnosis evidence (AC-002). Collected in the
|
|
541
547
|
// subscribe callback regardless of shouldPersistSessionEvent filtering, so a
|
|
542
548
|
// length-stopped thinking-only attempt with no persisted write tool calls is
|
|
@@ -625,6 +631,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
625
631
|
});
|
|
626
632
|
let resolveStall;
|
|
627
633
|
let resolveSettlement;
|
|
634
|
+
let resolveContextBudget;
|
|
628
635
|
let settled = false;
|
|
629
636
|
let abortConfirmed = true;
|
|
630
637
|
const persistRuntimeNote = (event) => {
|
|
@@ -655,6 +662,30 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
655
662
|
clearTimeout(stallHandle);
|
|
656
663
|
stallHandle = setTimeout(() => resolveStall?.("stall"), stallTimeoutMs);
|
|
657
664
|
};
|
|
665
|
+
const checkContextBudget = () => {
|
|
666
|
+
if (contextBudgetExhausted || !options.contextBudget)
|
|
667
|
+
return;
|
|
668
|
+
// Context protection follows the largest provider-reported response
|
|
669
|
+
// snapshot, not cumulative billing usage across turns. Summing repeated
|
|
670
|
+
// input contexts would abort healthy multi-turn sessions far below the
|
|
671
|
+
// model's actual context window.
|
|
672
|
+
const observedTokens = maxSdkContextTokens(usageSamples);
|
|
673
|
+
const turnExceeded = options.contextBudget.maxTurns !== undefined &&
|
|
674
|
+
observedTurns >= options.contextBudget.maxTurns;
|
|
675
|
+
const tokenExceeded = options.contextBudget.maxTokens !== undefined &&
|
|
676
|
+
observedTokens >= options.contextBudget.maxTokens;
|
|
677
|
+
if (!turnExceeded && !tokenExceeded)
|
|
678
|
+
return;
|
|
679
|
+
contextBudgetExhausted = true;
|
|
680
|
+
contextBudgetDetail = [
|
|
681
|
+
"context-budget-exhausted:",
|
|
682
|
+
`observedTurns=${observedTurns}`,
|
|
683
|
+
`observedTokens=${observedTokens}`,
|
|
684
|
+
`maxTurns=${options.contextBudget.maxTurns ?? "unbounded"}`,
|
|
685
|
+
`maxTokens=${options.contextBudget.maxTokens ?? "unbounded"}`,
|
|
686
|
+
].join(" ");
|
|
687
|
+
resolveContextBudget?.("context-budget");
|
|
688
|
+
};
|
|
658
689
|
unsubscribe = session.subscribe((event) => {
|
|
659
690
|
if (event.type === "agent_settled" && !settled) {
|
|
660
691
|
settled = true;
|
|
@@ -678,6 +709,9 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
678
709
|
const usageSample = extractSdkUsageSample(event);
|
|
679
710
|
if (usageSample)
|
|
680
711
|
usageSamples.push(usageSample);
|
|
712
|
+
if (event.type === "turn_end")
|
|
713
|
+
observedTurns += 1;
|
|
714
|
+
checkContextBudget();
|
|
681
715
|
// Capture writer-thinking-exhausted evidence BEFORE the
|
|
682
716
|
// shouldPersistSessionEvent filter: thinking_delta is intentionally
|
|
683
717
|
// excluded from persisted JSONL, but must still count toward the
|
|
@@ -700,6 +734,11 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
700
734
|
const runTurn = async (userMessage) => {
|
|
701
735
|
const settlementPromise = resetSettlement();
|
|
702
736
|
const stallPromise = resetStallPromise();
|
|
737
|
+
const contextBudgetPromise = options.contextBudget
|
|
738
|
+
? new Promise((resolve) => {
|
|
739
|
+
resolveContextBudget = resolve;
|
|
740
|
+
})
|
|
741
|
+
: null;
|
|
703
742
|
const promptPromise = session.prompt(userMessage);
|
|
704
743
|
// A settlement event is permitted to win the race while prompt() remains
|
|
705
744
|
// pending. Observe a later rejection so it never becomes unhandled.
|
|
@@ -710,6 +749,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
710
749
|
settlementPromise,
|
|
711
750
|
...(timeoutPromise ? [timeoutPromise] : []),
|
|
712
751
|
...(stallPromise ? [stallPromise] : []),
|
|
752
|
+
...(contextBudgetPromise ? [contextBudgetPromise] : []),
|
|
713
753
|
]);
|
|
714
754
|
};
|
|
715
755
|
const disposeSession = async () => {
|
|
@@ -737,7 +777,12 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
737
777
|
: options.userMessage;
|
|
738
778
|
let continuesSent = 0;
|
|
739
779
|
let raced = await runTurn(promptMessage);
|
|
740
|
-
if (raced === "
|
|
780
|
+
if (raced === "context-budget") {
|
|
781
|
+
appendStderr(contextBudgetDetail);
|
|
782
|
+
abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
|
|
783
|
+
await disposeSession();
|
|
784
|
+
}
|
|
785
|
+
else if (raced === "stall") {
|
|
741
786
|
abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
|
|
742
787
|
const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
|
|
743
788
|
const canContinue = abortConfirmed &&
|
|
@@ -154,6 +154,28 @@ function assertMkdirRelative(relative, ctx) {
|
|
|
154
154
|
throw new PiWriterPathDeniedError("forbidden", relative || ".");
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
export async function assertReadablePath(targetPath, ctx) {
|
|
158
|
+
const raw = String(targetPath ?? "").trim();
|
|
159
|
+
if (!raw)
|
|
160
|
+
throw new PiWriterPathDeniedError("empty-path", targetPath ?? "");
|
|
161
|
+
const repoRoot = path.resolve(ctx.repoRoot);
|
|
162
|
+
const absoluteTarget = path.isAbsolute(raw)
|
|
163
|
+
? path.resolve(raw)
|
|
164
|
+
: path.resolve(repoRoot, raw);
|
|
165
|
+
if (!isPathInside(repoRoot, absoluteTarget)) {
|
|
166
|
+
throw new PiWriterPathDeniedError("outside-repo", raw);
|
|
167
|
+
}
|
|
168
|
+
const effective = await resolveEffectiveMutationTarget(repoRoot, absoluteTarget);
|
|
169
|
+
const relative = repoRelativePath(repoRoot, absoluteTarget, raw, false);
|
|
170
|
+
const effectiveRelative = repoRelativePath(effective.realRoot, effective.absoluteTarget, raw, false);
|
|
171
|
+
const readSet = ctx.readSet ?? [];
|
|
172
|
+
for (const candidate of [relative, effectiveRelative]) {
|
|
173
|
+
if (readSet.length === 0 || !matchesAnyPattern(candidate, readSet)) {
|
|
174
|
+
throw new PiWriterPathDeniedError("read-outside-read-set", candidate, "path is outside the node readSet");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return relative;
|
|
178
|
+
}
|
|
157
179
|
/**
|
|
158
180
|
* Assert a mutation target is writable under the DAG writer policy.
|
|
159
181
|
* Returns the normalized lexical repo-relative path on success.
|
|
@@ -201,11 +223,30 @@ export async function assertMkdirPath(dirPath, ctx) {
|
|
|
201
223
|
assertMkdirRelative(effectiveRelative, ctx);
|
|
202
224
|
return lexicalRelative;
|
|
203
225
|
}
|
|
226
|
+
export function createPiReaderFsOperations(ctx) {
|
|
227
|
+
return {
|
|
228
|
+
readFile: async (absolutePath) => {
|
|
229
|
+
await assertReadablePath(absolutePath, ctx);
|
|
230
|
+
return fsReadFile(absolutePath);
|
|
231
|
+
},
|
|
232
|
+
access: async (absolutePath) => {
|
|
233
|
+
await assertReadablePath(absolutePath, ctx);
|
|
234
|
+
await fsAccess(absolutePath);
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
204
238
|
/** Build FS operations that enforce writer policy before mutations. */
|
|
205
239
|
export function createPiWriterFsOperations(ctx) {
|
|
240
|
+
const strictReadSet = (ctx.readSet?.length ?? 0) > 0;
|
|
206
241
|
return {
|
|
207
|
-
readFile: (absolutePath) =>
|
|
242
|
+
readFile: async (absolutePath) => {
|
|
243
|
+
if (strictReadSet)
|
|
244
|
+
await assertReadablePath(absolutePath, ctx);
|
|
245
|
+
return fsReadFile(absolutePath);
|
|
246
|
+
},
|
|
208
247
|
access: async (absolutePath) => {
|
|
248
|
+
if (strictReadSet)
|
|
249
|
+
await assertReadablePath(absolutePath, ctx);
|
|
209
250
|
await fsAccess(absolutePath);
|
|
210
251
|
},
|
|
211
252
|
writeFile: async (absolutePath, content) => {
|
|
@@ -218,6 +259,20 @@ export function createPiWriterFsOperations(ctx) {
|
|
|
218
259
|
},
|
|
219
260
|
};
|
|
220
261
|
}
|
|
262
|
+
export async function createPiReaderCustomTools(ctx) {
|
|
263
|
+
const sdk = (await import("@earendil-works/pi-coding-agent"));
|
|
264
|
+
const createReadToolDefinition = sdk.createReadToolDefinition;
|
|
265
|
+
if (typeof createReadToolDefinition !== "function") {
|
|
266
|
+
throw new Error("pi read tool policy unavailable: createReadToolDefinition missing from SDK");
|
|
267
|
+
}
|
|
268
|
+
const read = createReadToolDefinition(path.resolve(ctx.repoRoot), {
|
|
269
|
+
operations: createPiReaderFsOperations(ctx),
|
|
270
|
+
});
|
|
271
|
+
if (read?.name !== "read") {
|
|
272
|
+
throw new Error("pi read tool policy unavailable: SDK tool must be named read");
|
|
273
|
+
}
|
|
274
|
+
return [read];
|
|
275
|
+
}
|
|
221
276
|
/**
|
|
222
277
|
* Create same-name custom edit/write tool definitions that override built-ins
|
|
223
278
|
* via Pi SDK customTools registration. Fails closed when SDK factories are missing.
|
|
@@ -262,5 +317,6 @@ export function buildPiWriterToolPolicyContext(input) {
|
|
|
262
317
|
writeSet: input.writeSet ?? [],
|
|
263
318
|
forbiddenPaths: input.forbiddenPaths ?? [],
|
|
264
319
|
writePolicy: input.writePolicy ?? "exclusive",
|
|
320
|
+
readSet: input.readSet ?? [],
|
|
265
321
|
};
|
|
266
322
|
}
|
|
@@ -32,15 +32,16 @@ import { materializeBackendTestExecutionContract } from "../workflows/dag/backen
|
|
|
32
32
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
33
33
|
import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
|
|
34
34
|
import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
35
|
-
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
35
|
+
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, classifyBackendTestCorrespondenceRepair, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
36
36
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
37
37
|
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
38
38
|
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
39
|
-
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
39
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, summarizeBackendTestExclusions, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
40
40
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
41
41
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
42
42
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
43
43
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
44
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
44
45
|
import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
|
|
45
46
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
46
47
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
@@ -539,13 +540,22 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
539
540
|
const started = Date.now();
|
|
540
541
|
// Plan A: prefer the frozen layout carried by the compiled spec; fall back
|
|
541
542
|
// to the historical default for hand-written legacy DAGs.
|
|
542
|
-
const
|
|
543
|
+
const baseBackendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
543
544
|
? {
|
|
544
545
|
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
545
546
|
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
546
547
|
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
547
548
|
}
|
|
548
549
|
: undefined);
|
|
550
|
+
const markdownPlanArtifact = path.join(meta.runDir, "generate-backend-md-plan-pi", "plan.md");
|
|
551
|
+
const backendLayout = existsSync(markdownPlanArtifact)
|
|
552
|
+
? {
|
|
553
|
+
...baseBackendLayout,
|
|
554
|
+
readmePath: path
|
|
555
|
+
.relative(input.cwd, markdownPlanArtifact)
|
|
556
|
+
.replaceAll(path.sep, "/"),
|
|
557
|
+
}
|
|
558
|
+
: baseBackendLayout;
|
|
549
559
|
try {
|
|
550
560
|
if (!pipeline)
|
|
551
561
|
throw new Error("missing backend-test pipeline id");
|
|
@@ -860,26 +870,49 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
860
870
|
const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
|
|
861
871
|
await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
862
872
|
outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
|
|
863
|
-
if (correspondence.facts.status === "FAIL"
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
.
|
|
867
|
-
.
|
|
868
|
-
|
|
869
|
-
facts.status = "REPAIRABLE";
|
|
870
|
-
facts.repairEligible = true;
|
|
871
|
-
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
|
|
873
|
+
if (correspondence.facts.status === "FAIL") {
|
|
874
|
+
const repair = classifyBackendTestCorrespondenceRepair(correspondence.facts, backendLayout);
|
|
875
|
+
if (repair.markdownBlockingEntries.length > 0) {
|
|
876
|
+
facts.status = "BLOCKED";
|
|
877
|
+
facts.repairEligible = false;
|
|
878
|
+
facts.repairPaths = [];
|
|
872
879
|
facts.findings.push({
|
|
873
|
-
kind: "markdown-
|
|
880
|
+
kind: "markdown-correspondence-contract",
|
|
874
881
|
classification: "test-asset-defect",
|
|
875
|
-
repairability: "
|
|
876
|
-
detail:
|
|
877
|
-
.
|
|
878
|
-
.flatMap((entry) => entry.findings)
|
|
882
|
+
repairability: "blocked",
|
|
883
|
+
detail: repair.markdownBlockingEntries
|
|
884
|
+
.map((entry) => `${entry.caseId ?? "unknown Case"}: ${entry.reasonCodes.join(", ")}`)
|
|
879
885
|
.join("; ")
|
|
880
|
-
.slice(0, 12_000)
|
|
886
|
+
.slice(0, 12_000),
|
|
881
887
|
});
|
|
882
888
|
}
|
|
889
|
+
else if (repair.pytestRepairPaths.length > 0 &&
|
|
890
|
+
(facts.status === "PASS" || facts.status === "REPAIRABLE")) {
|
|
891
|
+
facts.status = "REPAIRABLE";
|
|
892
|
+
facts.repairEligible = true;
|
|
893
|
+
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...repair.pytestRepairPaths])).sort();
|
|
894
|
+
const repairEntries = correspondence.facts.entries.filter((entry) => repair.pytestRepairEntries.some((candidate) => candidate.caseId === entry.caseId));
|
|
895
|
+
for (const entry of repairEntries.slice(0, 40)) {
|
|
896
|
+
const classified = repair.pytestRepairEntries.find((candidate) => candidate.caseId === entry.caseId);
|
|
897
|
+
facts.findings.push({
|
|
898
|
+
kind: "markdown-pytest-correspondence",
|
|
899
|
+
classification: "test-asset-defect",
|
|
900
|
+
repairability: "repairable",
|
|
901
|
+
detail: entry.findings.join("; ").slice(0, 2_000) || "Markdown-to-pytest correspondence is incomplete",
|
|
902
|
+
...(entry.caseId ? { caseId: entry.caseId } : {}),
|
|
903
|
+
reasonCodes: classified?.reasonCodes ?? [],
|
|
904
|
+
repairPaths: classified?.paths ?? [],
|
|
905
|
+
payloadExpectedPaths: Array.from(new Set([
|
|
906
|
+
...entry.payloadAssessment.requiredPaths,
|
|
907
|
+
...entry.payloadAssessment.allowedPaths,
|
|
908
|
+
])).sort(),
|
|
909
|
+
payloadObservedPaths: entry.payloadAssessment.observedPaths,
|
|
910
|
+
payloadMissingPaths: entry.payloadAssessment.missingPaths,
|
|
911
|
+
payloadUnexpectedPaths: entry.payloadAssessment.unexpectedPaths,
|
|
912
|
+
payloadEnumMismatches: entry.payloadAssessment.enumMismatches,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
883
916
|
}
|
|
884
917
|
}
|
|
885
918
|
catch (error) {
|
|
@@ -902,6 +935,19 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
902
935
|
collectedItemCount: facts.collectedItemCount,
|
|
903
936
|
missingMappedScripts: facts.missingMappedScripts,
|
|
904
937
|
repairPaths: facts.repairPaths,
|
|
938
|
+
repairFindings: facts.findings.map((finding) => ({
|
|
939
|
+
kind: finding.kind,
|
|
940
|
+
repairability: finding.repairability,
|
|
941
|
+
detail: finding.detail,
|
|
942
|
+
...(finding.caseId ? { caseId: finding.caseId } : {}),
|
|
943
|
+
...(finding.reasonCodes ? { reasonCodes: finding.reasonCodes } : {}),
|
|
944
|
+
...(finding.repairPaths ? { repairPaths: finding.repairPaths } : {}),
|
|
945
|
+
...(finding.payloadExpectedPaths ? { payloadExpectedPaths: finding.payloadExpectedPaths } : {}),
|
|
946
|
+
...(finding.payloadObservedPaths ? { payloadObservedPaths: finding.payloadObservedPaths } : {}),
|
|
947
|
+
...(finding.payloadMissingPaths ? { payloadMissingPaths: finding.payloadMissingPaths } : {}),
|
|
948
|
+
...(finding.payloadUnexpectedPaths ? { payloadUnexpectedPaths: finding.payloadUnexpectedPaths } : {}),
|
|
949
|
+
...(finding.payloadEnumMismatches ? { payloadEnumMismatches: finding.payloadEnumMismatches } : {}),
|
|
950
|
+
})),
|
|
905
951
|
factsPath: "contracts/backend-test-pytest-collection-initial.json",
|
|
906
952
|
}),
|
|
907
953
|
stderr: "",
|
|
@@ -915,6 +961,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
915
961
|
effective = await materializeEffectiveBackendPytestCollection({
|
|
916
962
|
workspaceRoot: input.cwd,
|
|
917
963
|
initial,
|
|
964
|
+
layout: backendLayout,
|
|
918
965
|
});
|
|
919
966
|
}
|
|
920
967
|
else if (initial.status === "REPAIRABLE") {
|
|
@@ -974,6 +1021,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
974
1021
|
workspaceRoot: input.cwd,
|
|
975
1022
|
initial,
|
|
976
1023
|
final: finalFacts,
|
|
1024
|
+
layout: backendLayout,
|
|
977
1025
|
});
|
|
978
1026
|
}
|
|
979
1027
|
else {
|
|
@@ -999,6 +1047,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
999
1047
|
})
|
|
1000
1048
|
: { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
|
|
1001
1049
|
const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
|
|
1050
|
+
const exclusionSummary = summarizeBackendTestExclusions(eligibility.excludedItems);
|
|
1002
1051
|
const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
|
|
1003
1052
|
await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
|
|
1004
1053
|
await writeFile(eligibilityFactsPath, `${JSON.stringify({
|
|
@@ -1009,6 +1058,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1009
1058
|
excludedItemCount: eligibility.excludedItems.length,
|
|
1010
1059
|
eligibleItemIds: eligibility.eligibleItemIds,
|
|
1011
1060
|
excludedItems: eligibility.excludedItems,
|
|
1061
|
+
exclusionSummary,
|
|
1062
|
+
pytestSpawned: false,
|
|
1063
|
+
businessTestBodyExecutedCount: 0,
|
|
1012
1064
|
inputHashes: eligibilityInputHashes,
|
|
1013
1065
|
}, null, 2)}\n`, "utf8");
|
|
1014
1066
|
const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
|
|
@@ -1021,6 +1073,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1021
1073
|
`- Collected items: ${effective.collectedItemIds.length}`,
|
|
1022
1074
|
`- Eligible items: ${eligibility.eligibleItemIds.length}`,
|
|
1023
1075
|
`- Excluded items: ${eligibility.excludedItems.length}`,
|
|
1076
|
+
`- Excluded Cases: ${exclusionSummary.excludedCaseCount}`,
|
|
1077
|
+
`- Exclusion categories: ${Object.entries(exclusionSummary.excludedByReason).map(([category, count]) => `${category}=${count}`).join("; ") || "none"}`,
|
|
1078
|
+
"- pytest spawned: false",
|
|
1079
|
+
"- Business test bodies executed: 0",
|
|
1024
1080
|
"",
|
|
1025
1081
|
"## Excluded Items",
|
|
1026
1082
|
"",
|
|
@@ -1036,6 +1092,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1036
1092
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
1037
1093
|
eligibility,
|
|
1038
1094
|
eligibilityInputHashes,
|
|
1095
|
+
layout: backendLayout,
|
|
1039
1096
|
});
|
|
1040
1097
|
return {
|
|
1041
1098
|
ok: true,
|
|
@@ -1048,6 +1105,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1048
1105
|
executionReadinessStatus: readiness.status,
|
|
1049
1106
|
eligibleItemCount: readiness.eligibleItemIds.length,
|
|
1050
1107
|
excludedItemCount: readiness.excludedItems.length,
|
|
1108
|
+
exclusionSummary: readiness.exclusionSummary,
|
|
1109
|
+
pytestSpawned: readiness.pytestSpawned,
|
|
1110
|
+
businessTestBodyExecutedCount: readiness.businessTestBodyExecutedCount,
|
|
1051
1111
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
1052
1112
|
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
1053
1113
|
reportPath: artifacts.reportPath,
|
|
@@ -1059,7 +1119,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1059
1119
|
else if (pipeline === "markdown-traceability") {
|
|
1060
1120
|
let report;
|
|
1061
1121
|
try {
|
|
1062
|
-
report = await validateBackendMarkdownTraceability(input.cwd);
|
|
1122
|
+
report = await validateBackendMarkdownTraceability(input.cwd, backendLayout);
|
|
1063
1123
|
}
|
|
1064
1124
|
catch (error) {
|
|
1065
1125
|
report = `# Backend Test Traceability\n\n## Status\n\nFAIL\n\n## Findings\n\n- Traceability validation could not complete: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
@@ -1144,7 +1204,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1144
1204
|
}
|
|
1145
1205
|
else if (pipeline === "markdown-execute-html") {
|
|
1146
1206
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
1147
|
-
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
1207
|
+
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness, backendLayout);
|
|
1148
1208
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1149
1209
|
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1150
1210
|
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
@@ -1719,6 +1779,33 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1719
1779
|
}
|
|
1720
1780
|
}
|
|
1721
1781
|
async function executeBackendTestPipelineWithWriteGuard(input, meta) {
|
|
1782
|
+
if (meta.spec.backendTestWorkspaceControl === "filesystem-only") {
|
|
1783
|
+
const paths = input.task.writeSet ?? input.task.allowedPaths;
|
|
1784
|
+
let beforeSnapshot;
|
|
1785
|
+
try {
|
|
1786
|
+
beforeSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1787
|
+
}
|
|
1788
|
+
catch (error) {
|
|
1789
|
+
return { ok: false, stdout: "", stderr: `workspace filesystem baseline unavailable: ${error instanceof Error ? error.message : String(error)}`, failureCategory: "write-guard", durationMs: 0 };
|
|
1790
|
+
}
|
|
1791
|
+
const result = await executeBackendTestPipeline(input, meta);
|
|
1792
|
+
try {
|
|
1793
|
+
const afterSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1794
|
+
const guard = validateShellWriteGuard({
|
|
1795
|
+
changedPaths: diffWorkspaceWriteSnapshots(beforeSnapshot, afterSnapshot),
|
|
1796
|
+
writePolicy: input.task.writePolicy,
|
|
1797
|
+
writeSet: input.task.writeSet,
|
|
1798
|
+
allowedPaths: input.task.allowedPaths,
|
|
1799
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
1800
|
+
});
|
|
1801
|
+
if (!guard.ok)
|
|
1802
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${guard.violations.join(", ")}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1803
|
+
return result;
|
|
1804
|
+
}
|
|
1805
|
+
catch (error) {
|
|
1806
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1722
1809
|
let beforeStatus;
|
|
1723
1810
|
try {
|
|
1724
1811
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
5
|
+
const normalize = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
6
|
+
function rootBeforeWildcard(pattern) {
|
|
7
|
+
const value = normalize(pattern);
|
|
8
|
+
const wildcard = value.search(/[?*]/);
|
|
9
|
+
if (wildcard < 0)
|
|
10
|
+
return path.posix.dirname(value);
|
|
11
|
+
const prefix = value.slice(0, wildcard);
|
|
12
|
+
return prefix.slice(0, prefix.lastIndexOf("/")) || ".";
|
|
13
|
+
}
|
|
14
|
+
async function walk(rootCwd, relative, files) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(path.resolve(rootCwd, relative), { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if ([".git", ".harness", "node_modules"].includes(entry.name))
|
|
24
|
+
continue;
|
|
25
|
+
const child = normalize(path.posix.join(relative, entry.name));
|
|
26
|
+
if (entry.isDirectory())
|
|
27
|
+
await walk(rootCwd, child, files);
|
|
28
|
+
else if (entry.isFile())
|
|
29
|
+
files.add(child);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function captureWorkspaceWriteSnapshot(input) {
|
|
33
|
+
const candidates = new Set();
|
|
34
|
+
for (const pattern of input.paths) {
|
|
35
|
+
await walk(input.rootCwd, rootBeforeWildcard(pattern), candidates);
|
|
36
|
+
if (!/[?*]/.test(pattern)) {
|
|
37
|
+
try {
|
|
38
|
+
if ((await lstat(path.resolve(input.rootCwd, pattern))).isFile())
|
|
39
|
+
candidates.add(normalize(pattern));
|
|
40
|
+
}
|
|
41
|
+
catch { /* absent target */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const files = {};
|
|
45
|
+
let totalBytes = 0;
|
|
46
|
+
for (const relative of [...candidates].filter((file) => input.paths.some((pattern) => pathMatchesPattern(file, pattern))).sort()) {
|
|
47
|
+
if (Object.keys(files).length >= 20_000)
|
|
48
|
+
throw new Error("workspace snapshot file limit exceeded");
|
|
49
|
+
const absolute = path.resolve(input.rootCwd, relative);
|
|
50
|
+
const stat = await lstat(absolute);
|
|
51
|
+
if (!stat.isFile())
|
|
52
|
+
throw new Error(`workspace snapshot encountered non-regular file: ${relative}`);
|
|
53
|
+
totalBytes += stat.size;
|
|
54
|
+
if (totalBytes > 64 * 1024 * 1024)
|
|
55
|
+
throw new Error("workspace snapshot byte limit exceeded");
|
|
56
|
+
files[relative] = { size: stat.size, sha256: createHash("sha256").update(await readFile(absolute)).digest("hex") };
|
|
57
|
+
}
|
|
58
|
+
const body = JSON.stringify(files);
|
|
59
|
+
return { schemaVersion: 1, controlMode: "filesystem-only", files, sha256: createHash("sha256").update(body).digest("hex") };
|
|
60
|
+
}
|
|
61
|
+
export function diffWorkspaceWriteSnapshots(before, after) {
|
|
62
|
+
const paths = new Set([...Object.keys(before.files), ...Object.keys(after.files)]);
|
|
63
|
+
return [...paths].sort().filter((file) => {
|
|
64
|
+
const left = before.files[file];
|
|
65
|
+
const right = after.files[file];
|
|
66
|
+
return !left || !right || left.size !== right.size || left.sha256 !== right.sha256;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -2,6 +2,29 @@ import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/pr
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertHarnessWriteAllowed, } from "./completed-facts-guard.js";
|
|
4
4
|
let atomicWriteSequence = 0;
|
|
5
|
+
function isTransientJsonReadError(error) {
|
|
6
|
+
if (error instanceof SyntaxError)
|
|
7
|
+
return true;
|
|
8
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
9
|
+
return false;
|
|
10
|
+
const code = String(error.code ?? "");
|
|
11
|
+
return code === "EBUSY" || code === "EPERM" || code === "EACCES";
|
|
12
|
+
}
|
|
13
|
+
export async function readJsonWithTransientRetry(targetPath, options = {}) {
|
|
14
|
+
const attempts = Math.max(1, Math.floor(options.attempts ?? 6));
|
|
15
|
+
const delayMs = Math.max(0, Math.floor(options.delayMs ?? 20));
|
|
16
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await readFile(targetPath, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (attempt === attempts || !isTransientJsonReadError(error))
|
|
22
|
+
throw error;
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error("unreachable transient JSON read state");
|
|
27
|
+
}
|
|
5
28
|
async function writeAtomic(targetPath, content, options) {
|
|
6
29
|
assertHarnessWriteAllowed(targetPath, options);
|
|
7
30
|
const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal switch for the experimental "rerun failed node with an edited
|
|
3
|
-
* prompt" capability.
|
|
4
|
-
*
|
|
5
|
-
* feature is internal.
|
|
3
|
+
* prompt" capability. The feature is enabled by default; the environment
|
|
4
|
+
* variable remains as an emergency kill switch while the feature is internal.
|
|
6
5
|
*/
|
|
7
6
|
export const DAG_RERUN_PROMPT_OVERRIDE_ENV = "HARNESS_DAG_RERUN_PROMPT_OVERRIDE";
|
|
8
7
|
/** Upper bound for an override input body (characters). */
|
|
9
8
|
export const MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS = 65_536;
|
|
10
9
|
export function isDagRerunPromptOverrideEnabled(env = process.env) {
|
|
11
|
-
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV];
|
|
12
|
-
return value
|
|
10
|
+
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV]?.trim().toLowerCase();
|
|
11
|
+
return value !== "0" && value !== "false" && value !== "off";
|
|
13
12
|
}
|
|
14
13
|
export const DAG_RERUN_PROMPT_ADDENDUM_TAG = "operator_addendum";
|
|
15
14
|
/**
|