@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10
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 +106 -109
- package/README.md +2 -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 +43 -3
- package/dist/executors/workspace-write-snapshot.js +68 -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/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 +129 -4
- 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 +29 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +17 -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 +238 -51
- 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 +265 -237
- 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
|
}
|
|
@@ -41,6 +41,7 @@ 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");
|
|
@@ -915,6 +925,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
915
925
|
effective = await materializeEffectiveBackendPytestCollection({
|
|
916
926
|
workspaceRoot: input.cwd,
|
|
917
927
|
initial,
|
|
928
|
+
layout: backendLayout,
|
|
918
929
|
});
|
|
919
930
|
}
|
|
920
931
|
else if (initial.status === "REPAIRABLE") {
|
|
@@ -974,6 +985,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
974
985
|
workspaceRoot: input.cwd,
|
|
975
986
|
initial,
|
|
976
987
|
final: finalFacts,
|
|
988
|
+
layout: backendLayout,
|
|
977
989
|
});
|
|
978
990
|
}
|
|
979
991
|
else {
|
|
@@ -1036,6 +1048,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1036
1048
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
1037
1049
|
eligibility,
|
|
1038
1050
|
eligibilityInputHashes,
|
|
1051
|
+
layout: backendLayout,
|
|
1039
1052
|
});
|
|
1040
1053
|
return {
|
|
1041
1054
|
ok: true,
|
|
@@ -1059,7 +1072,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1059
1072
|
else if (pipeline === "markdown-traceability") {
|
|
1060
1073
|
let report;
|
|
1061
1074
|
try {
|
|
1062
|
-
report = await validateBackendMarkdownTraceability(input.cwd);
|
|
1075
|
+
report = await validateBackendMarkdownTraceability(input.cwd, backendLayout);
|
|
1063
1076
|
}
|
|
1064
1077
|
catch (error) {
|
|
1065
1078
|
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 +1157,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1144
1157
|
}
|
|
1145
1158
|
else if (pipeline === "markdown-execute-html") {
|
|
1146
1159
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
1147
|
-
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
1160
|
+
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness, backendLayout);
|
|
1148
1161
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1149
1162
|
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1150
1163
|
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
@@ -1719,6 +1732,33 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1719
1732
|
}
|
|
1720
1733
|
}
|
|
1721
1734
|
async function executeBackendTestPipelineWithWriteGuard(input, meta) {
|
|
1735
|
+
if (meta.spec.backendTestWorkspaceControl === "filesystem-only") {
|
|
1736
|
+
const paths = input.task.writeSet ?? input.task.allowedPaths;
|
|
1737
|
+
let beforeSnapshot;
|
|
1738
|
+
try {
|
|
1739
|
+
beforeSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1740
|
+
}
|
|
1741
|
+
catch (error) {
|
|
1742
|
+
return { ok: false, stdout: "", stderr: `workspace filesystem baseline unavailable: ${error instanceof Error ? error.message : String(error)}`, failureCategory: "write-guard", durationMs: 0 };
|
|
1743
|
+
}
|
|
1744
|
+
const result = await executeBackendTestPipeline(input, meta);
|
|
1745
|
+
try {
|
|
1746
|
+
const afterSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1747
|
+
const guard = validateShellWriteGuard({
|
|
1748
|
+
changedPaths: diffWorkspaceWriteSnapshots(beforeSnapshot, afterSnapshot),
|
|
1749
|
+
writePolicy: input.task.writePolicy,
|
|
1750
|
+
writeSet: input.task.writeSet,
|
|
1751
|
+
allowedPaths: input.task.allowedPaths,
|
|
1752
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
1753
|
+
});
|
|
1754
|
+
if (!guard.ok)
|
|
1755
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${guard.violations.join(", ")}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
catch (error) {
|
|
1759
|
+
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" };
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1722
1762
|
let beforeStatus;
|
|
1723
1763
|
try {
|
|
1724
1764
|
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
|
/**
|
|
@@ -11,10 +11,10 @@ export function createNpmUpdateClient(options) {
|
|
|
11
11
|
const timeoutMs = options.timeoutMs ?? DEFAULT_NPM_QUERY_TIMEOUT_MS;
|
|
12
12
|
const cwd = options.cwd;
|
|
13
13
|
const env = options.env;
|
|
14
|
-
const
|
|
14
|
+
const distTagVersionCheck = async (channel) => {
|
|
15
15
|
let result;
|
|
16
16
|
try {
|
|
17
|
-
result = await runner(npmCommand, ["view", PACKAGE_NAME,
|
|
17
|
+
result = await runner(npmCommand, ["view", PACKAGE_NAME, `dist-tags.${channel}`, "--json"], { timeoutMs, cwd, env });
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
20
|
return { ok: false, reason: "spawn-failure" };
|
|
@@ -27,17 +27,19 @@ export function createNpmUpdateClient(options) {
|
|
|
27
27
|
if (result.exitCode !== 0) {
|
|
28
28
|
return { ok: false, reason: "command-failed" };
|
|
29
29
|
}
|
|
30
|
-
const version =
|
|
30
|
+
const version = parseNpmViewDistTag(result.stdout, channel);
|
|
31
31
|
return version
|
|
32
32
|
? { ok: true, version }
|
|
33
33
|
: { ok: false, reason: "invalid-response" };
|
|
34
34
|
};
|
|
35
|
+
const latestVersionCheck = () => distTagVersionCheck("latest");
|
|
35
36
|
return {
|
|
36
37
|
async latestVersion() {
|
|
37
38
|
const result = await latestVersionCheck();
|
|
38
39
|
return result.ok ? result.version : undefined;
|
|
39
40
|
},
|
|
40
41
|
latestVersionCheck,
|
|
42
|
+
distTagVersionCheck,
|
|
41
43
|
async canAutoInstall() {
|
|
42
44
|
return proveGlobalNpmInstall({
|
|
43
45
|
currentVersion: options.currentVersion,
|
|
@@ -49,10 +51,11 @@ export function createNpmUpdateClient(options) {
|
|
|
49
51
|
timeoutMs,
|
|
50
52
|
});
|
|
51
53
|
},
|
|
52
|
-
async installExact(version) {
|
|
53
|
-
if (!
|
|
54
|
+
async installExact(version, channel = "latest") {
|
|
55
|
+
if (!isAllowedChannelVersion(version, channel)) {
|
|
54
56
|
return { ok: false, reason: "invalid-version" };
|
|
55
|
-
|
|
57
|
+
}
|
|
58
|
+
const result = await runner(npmCommand, buildExactInstallArgs(version, channel), {
|
|
56
59
|
timeoutMs: Math.max(timeoutMs, 120000),
|
|
57
60
|
cwd,
|
|
58
61
|
env,
|
|
@@ -79,6 +82,13 @@ export function createNpmUpdateClient(options) {
|
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
export function parseNpmViewLatest(stdout) {
|
|
85
|
+
return parseNpmViewDistTag(stdout, "latest");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `latest` stays stable-only. `next` is explicitly selected by a human and
|
|
89
|
+
* may legitimately be a SemVer prerelease, but remains an exact version.
|
|
90
|
+
*/
|
|
91
|
+
export function parseNpmViewDistTag(stdout, channel) {
|
|
82
92
|
const trimmed = stdout.trim();
|
|
83
93
|
let value = trimmed;
|
|
84
94
|
try {
|
|
@@ -89,11 +99,12 @@ export function parseNpmViewLatest(stdout) {
|
|
|
89
99
|
}
|
|
90
100
|
if (typeof value !== "string")
|
|
91
101
|
return undefined;
|
|
92
|
-
return
|
|
102
|
+
return isAllowedChannelVersion(value, channel) ? value : undefined;
|
|
93
103
|
}
|
|
94
|
-
export function buildExactInstallArgs(version) {
|
|
95
|
-
if (!
|
|
96
|
-
throw new Error(`invalid
|
|
104
|
+
export function buildExactInstallArgs(version, channel = "latest") {
|
|
105
|
+
if (!isAllowedChannelVersion(version, channel)) {
|
|
106
|
+
throw new Error(`invalid ${channel} version: ${version}`);
|
|
107
|
+
}
|
|
97
108
|
return ["install", "-g", `${PACKAGE_NAME}@${version}`, "--no-fund", "--no-audit"];
|
|
98
109
|
}
|
|
99
110
|
export function verifyNpmListVersion(stdout, expectedVersion) {
|
|
@@ -106,7 +117,13 @@ export function verifyNpmListVersion(stdout, expectedVersion) {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
export function verifyPathVersion(stdout, expectedVersion) {
|
|
109
|
-
|
|
120
|
+
// `loop-agent --version` may append a build provenance marker. The release
|
|
121
|
+
// version itself must still match exactly; accepting no other trailing form
|
|
122
|
+
// keeps PATH verification bound to the requested stable package.
|
|
123
|
+
return new RegExp(`^${escapeRegExp(expectedVersion)}(?: \\(build [^)]+\\))?$`).test(stdout.trim());
|
|
124
|
+
}
|
|
125
|
+
function escapeRegExp(value) {
|
|
126
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
127
|
}
|
|
111
128
|
async function proveGlobalNpmInstall(options) {
|
|
112
129
|
const root = await options.runner(options.npmCommand, ["root", "-g"], {
|
|
@@ -142,6 +159,11 @@ function isStrictRelease(version) {
|
|
|
142
159
|
const parsed = semver.parse(version);
|
|
143
160
|
return Boolean(parsed && parsed.prerelease.length === 0);
|
|
144
161
|
}
|
|
162
|
+
function isAllowedChannelVersion(version, channel) {
|
|
163
|
+
if (channel === "latest")
|
|
164
|
+
return isStrictRelease(version);
|
|
165
|
+
return Boolean(semver.valid(version));
|
|
166
|
+
}
|
|
145
167
|
async function runCommand(command, args, options) {
|
|
146
168
|
return new Promise((resolve) => {
|
|
147
169
|
const child = spawn(command, [...args], {
|
|
@@ -147,6 +147,8 @@ export const backendTestConfigSchema = z
|
|
|
147
147
|
gapDoc: z.string().min(1).optional(),
|
|
148
148
|
/** gap-fill pytest execution scope: only affected items (default) or all eligible. */
|
|
149
149
|
executeScope: z.enum(["affected", "all"]).optional(),
|
|
150
|
+
/** Backend-test-only workspace mutation control; omitted means Git guard. */
|
|
151
|
+
workspaceControl: z.enum(["git", "filesystem-only"]).optional(),
|
|
150
152
|
})
|
|
151
153
|
.strict()
|
|
152
154
|
.optional();
|
|
@@ -237,5 +239,13 @@ export const taskConfigSchema = z.preprocess((raw) => {
|
|
|
237
239
|
if ("executor" in obj || "cursorModel" in obj) {
|
|
238
240
|
throw new Error(CURSOR_TASK_FIELD_REMOVED_ERROR);
|
|
239
241
|
}
|
|
242
|
+
const backendTest = obj.backendTest;
|
|
243
|
+
if (backendTest &&
|
|
244
|
+
typeof backendTest === "object" &&
|
|
245
|
+
!Array.isArray(backendTest) &&
|
|
246
|
+
"workspaceControl" in backendTest &&
|
|
247
|
+
obj.taskKind !== "backend-test") {
|
|
248
|
+
throw new Error("backendTest.workspaceControl is only valid when taskKind is backend-test");
|
|
249
|
+
}
|
|
240
250
|
return raw;
|
|
241
251
|
}, taskConfigObjectSchema);
|
|
@@ -232,8 +232,11 @@ export async function applyTaskContract(input) {
|
|
|
232
232
|
// Re-parse through schema to ensure full TaskConfig shape for hash stability
|
|
233
233
|
const fullConfig = parseTaskConfigForRuntime(mergedConfig);
|
|
234
234
|
const taskConfigContent = serializeTaskConfigForDisk(fullConfig);
|
|
235
|
-
const requirementContent = projectRequirementMarkdown(input.draft);
|
|
235
|
+
const requirementContent = projectRequirementMarkdown(input.draft, input.requirementLedger);
|
|
236
236
|
const constraintsContent = projectConstraintsMarkdown(input.draft);
|
|
237
|
+
const requirementLedgerContent = input.requirementLedger !== undefined
|
|
238
|
+
? `${JSON.stringify(input.requirementLedger, null, 2)}\n`
|
|
239
|
+
: undefined;
|
|
237
240
|
// Validate references manifest hash if present (do not modify)
|
|
238
241
|
const paths = getTaskPaths(input.repoRoot, input.taskId);
|
|
239
242
|
const manifestText = await readFile(getSourceManifestPath(paths.sourceDir), "utf-8").catch(() => null);
|
|
@@ -241,6 +244,7 @@ export async function applyTaskContract(input) {
|
|
|
241
244
|
requirementBytes: requirementContent,
|
|
242
245
|
constraintsBytes: constraintsContent,
|
|
243
246
|
referenceManifestBytes: manifestText,
|
|
247
|
+
requirementLedgerBytes: requirementLedgerContent,
|
|
244
248
|
});
|
|
245
249
|
const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
|
|
246
250
|
const canonicalHash = computeCanonicalHash({
|
|
@@ -277,6 +281,9 @@ export async function applyTaskContract(input) {
|
|
|
277
281
|
constraintsContent,
|
|
278
282
|
taskConfigContent,
|
|
279
283
|
contractRef: nextRef,
|
|
284
|
+
...(requirementLedgerContent !== undefined
|
|
285
|
+
? { requirementLedgerContent }
|
|
286
|
+
: {}),
|
|
280
287
|
},
|
|
281
288
|
});
|
|
282
289
|
await appendRequestLedgerEntry({
|
|
@@ -312,6 +319,9 @@ export async function applyTaskContract(input) {
|
|
|
312
319
|
changedFiles: [
|
|
313
320
|
"source/需求.md",
|
|
314
321
|
"source/执行约束.md",
|
|
322
|
+
...(requirementLedgerContent !== undefined
|
|
323
|
+
? ["source/requirement-ledger.json"]
|
|
324
|
+
: []),
|
|
315
325
|
"task.json",
|
|
316
326
|
"contract.json",
|
|
317
327
|
],
|
|
@@ -29,3 +29,5 @@ export const TRANSACTIONS_DIRNAME = TRANSACTIONS_DIR_NAME;
|
|
|
29
29
|
export const REQUEST_LEDGER_BASENAME = REQUEST_LEDGER_FILE_NAME;
|
|
30
30
|
export const REQUIREMENT_FILE_NAME = "需求.md";
|
|
31
31
|
export const CONSTRAINTS_FILE_NAME = "执行约束.md";
|
|
32
|
+
/** Source fidelity ledger (20260824-frontend-source-fidelity-ledger-v1). */
|
|
33
|
+
export const REQUIREMENT_LEDGER_FILE_NAME = "requirement-ledger.json";
|
|
@@ -35,6 +35,9 @@ export function computeSourceHashes(input) {
|
|
|
35
35
|
if (input.referenceManifestBytes) {
|
|
36
36
|
sourceHashes.referenceManifestSha256 = sha256Utf8(input.referenceManifestBytes);
|
|
37
37
|
}
|
|
38
|
+
if (input.requirementLedgerBytes) {
|
|
39
|
+
sourceHashes.requirementLedgerSha256 = sha256Utf8(input.requirementLedgerBytes);
|
|
40
|
+
}
|
|
38
41
|
return sourceHashes;
|
|
39
42
|
}
|
|
40
43
|
export function refMatchesObserved(ref, observedCanonicalHash) {
|