@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -92
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +10 -0
- package/dist/task/source-prepare/fragment-inventory.js +1 -14
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- 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/resource-preferences-store.js +1 -1
- 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 +94 -64
- 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-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -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 +2 -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 +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/index.html +1 -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/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 +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- package/dist/worker/observe/static/views/dag.js +2 -1
- 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/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
- package/dist/workflows/dag/init-hybrid.js +178 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.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
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -4,16 +4,18 @@ import { readFile } from "node:fs/promises";
|
|
|
4
4
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
5
5
|
import { executePiStep, resolvePiBackend, } from "./pi-executor.js";
|
|
6
6
|
import { resolveDagPiExtensions, } from "./pi-extension-resolver.js";
|
|
7
|
-
import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
|
|
7
|
+
import { buildPiWriterToolPolicyContext, createPiReaderCustomTools, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
|
|
8
8
|
import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
|
|
9
9
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
10
10
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
11
11
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
12
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
12
13
|
import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
13
14
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
14
15
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
15
|
-
import { frontendTestLayoutFromSpec
|
|
16
|
+
import { frontendTestLayoutFromSpec } from "../workflows/dag/frontend-test-layout.js";
|
|
16
17
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
18
|
+
import { writeEffectiveContextReceipt } from "../workflows/dag/context-receipt.js";
|
|
17
19
|
/**
|
|
18
20
|
* Writer classification for a length-stopped thinking-only attempt: the model
|
|
19
21
|
* exhausted its output budget thinking but never issued a write/edit tool call
|
|
@@ -181,6 +183,9 @@ export function resolveDagPiStepName(task) {
|
|
|
181
183
|
return resolvePiExecutorStep(resolveDagPiPersona(task));
|
|
182
184
|
}
|
|
183
185
|
export function resolveDagPiToolNames(task) {
|
|
186
|
+
if ((task.readSet?.length ?? 0) > 0) {
|
|
187
|
+
return isDagPiWriteTask(task) ? ["read", "edit", "write"] : ["read"];
|
|
188
|
+
}
|
|
184
189
|
if (!isDagPiWriteTask(task))
|
|
185
190
|
return [...DAG_PI_READONLY_TOOLS];
|
|
186
191
|
const tools = [...DAG_PI_WRITE_TOOLS];
|
|
@@ -237,6 +242,9 @@ export function buildDagPiUserMessage(task, persona, step) {
|
|
|
237
242
|
`You are executing hybrid DAG node "${task.id}" (role=${role}, piPersona=${persona}, piStep=${step}, writePolicy=${writePolicy}).`,
|
|
238
243
|
"The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
|
|
239
244
|
"Use read-only tools only. Do not edit, write, or commit repository files.",
|
|
245
|
+
(task.readSet?.length ?? 0) > 0
|
|
246
|
+
? `Strict read set: ${task.readSet.join(", ")}. The read tool rejects every other repository path; do not search for substitutes.`
|
|
247
|
+
: undefined,
|
|
240
248
|
"Return your conclusion as plain Markdown text suitable for downstream DAG nodes.",
|
|
241
249
|
"Do not wrap the output in code fences and do not add conversational preamble.",
|
|
242
250
|
].join(" ");
|
|
@@ -446,9 +454,28 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
446
454
|
};
|
|
447
455
|
}
|
|
448
456
|
let beforeStatus;
|
|
457
|
+
let beforeWorkspaceSnapshot;
|
|
449
458
|
let beforePathFingerprints;
|
|
450
|
-
/** tools-only
|
|
451
|
-
const
|
|
459
|
+
/** tools-only and filesystem-only keep the tool sandbox but skip Git baseline. */
|
|
460
|
+
const filesystemOnly = isWriteTask && meta.spec.backendTestWorkspaceControl === "filesystem-only";
|
|
461
|
+
const skipGitWriteGuard = isWriteTask && (input.task.writeGuardPolicy === "tools-only" || filesystemOnly);
|
|
462
|
+
if (isWriteTask && filesystemOnly) {
|
|
463
|
+
try {
|
|
464
|
+
beforeWorkspaceSnapshot = await captureWorkspaceWriteSnapshot({
|
|
465
|
+
rootCwd: input.cwd,
|
|
466
|
+
paths: input.task.writeSet ?? input.task.allowedPaths,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
stdout: "",
|
|
473
|
+
stderr: `workspace filesystem baseline unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
474
|
+
failureCategory: "write-guard",
|
|
475
|
+
durationMs: Date.now() - started,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
452
479
|
if (isWriteTask && !skipGitWriteGuard) {
|
|
453
480
|
try {
|
|
454
481
|
beforeStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, { phase: "pi-writer-before" });
|
|
@@ -490,12 +517,24 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
490
517
|
let playwrightToolContext;
|
|
491
518
|
if (allowsPlaywrightCli && !isWriteTask) {
|
|
492
519
|
return {
|
|
493
|
-
ok: false,
|
|
520
|
+
ok: false,
|
|
521
|
+
stdout: "",
|
|
522
|
+
stderr: "pi command capability requires an explicit bounded write tool profile",
|
|
523
|
+
failureCategory: "tool-policy",
|
|
524
|
+
durationMs: Date.now() - started,
|
|
494
525
|
};
|
|
495
526
|
}
|
|
496
|
-
|
|
527
|
+
const strictReadSet = (input.task.readSet?.length ?? 0) > 0;
|
|
528
|
+
if (isWriteTask || allowsPlaywrightCli || strictReadSet) {
|
|
497
529
|
try {
|
|
498
530
|
const customTools = [];
|
|
531
|
+
if (strictReadSet) {
|
|
532
|
+
const readContext = buildPiWriterToolPolicyContext({
|
|
533
|
+
repoRoot: input.cwd,
|
|
534
|
+
readSet: input.task.readSet,
|
|
535
|
+
});
|
|
536
|
+
customTools.push(...(await createPiReaderCustomTools(readContext)));
|
|
537
|
+
}
|
|
499
538
|
if (isWriteTask) {
|
|
500
539
|
const policyContext = buildPiWriterToolPolicyContext({
|
|
501
540
|
repoRoot: input.cwd,
|
|
@@ -503,6 +542,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
503
542
|
writeSet: input.task.writeSet,
|
|
504
543
|
forbiddenPaths: input.task.forbiddenPaths,
|
|
505
544
|
writePolicy: input.task.writePolicy,
|
|
545
|
+
readSet: input.task.readSet,
|
|
506
546
|
});
|
|
507
547
|
customTools.push(...(await createPiWriterCustomTools(policyContext)));
|
|
508
548
|
}
|
|
@@ -607,19 +647,42 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
607
647
|
const piExtensionPaths = piExtensionsResolution && resolvePiBackend() !== "cli-only"
|
|
608
648
|
? piExtensionsResolution.resolved.flatMap((entry) => entry.entryPaths)
|
|
609
649
|
: undefined;
|
|
650
|
+
const modelConfig = resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined);
|
|
651
|
+
const toolNames = resolveDagPiToolNames(input.task);
|
|
652
|
+
const userMessage = buildDagPiUserMessage(input.task, persona, step);
|
|
653
|
+
await writeEffectiveContextReceipt({
|
|
654
|
+
runDir: meta.runDir,
|
|
655
|
+
runId: meta.runId,
|
|
656
|
+
nodeId: input.task.id,
|
|
657
|
+
attempt,
|
|
658
|
+
task: input.task,
|
|
659
|
+
spec: meta.spec,
|
|
660
|
+
repoRoot: input.cwd,
|
|
661
|
+
systemPrompt: input.prompt,
|
|
662
|
+
userMessage,
|
|
663
|
+
resolvedModel: `${modelConfig.provider}/${modelConfig.model}`,
|
|
664
|
+
resolvedSkills: input.resolvedSkills ?? [],
|
|
665
|
+
resolvedExtensions: piExtensionsResolution && resolvePiBackend() !== "cli-only"
|
|
666
|
+
? piExtensionsResolution.resolved.map((entry) => entry.id)
|
|
667
|
+
: [],
|
|
668
|
+
activeTools: toolNames,
|
|
669
|
+
});
|
|
610
670
|
result = await piStepFn({
|
|
611
671
|
attachedFiles: [],
|
|
612
|
-
modelConfig
|
|
672
|
+
modelConfig,
|
|
613
673
|
prompt: input.prompt,
|
|
614
674
|
repoRoot: input.cwd,
|
|
615
675
|
step,
|
|
616
|
-
toolNames
|
|
617
|
-
userMessage
|
|
676
|
+
toolNames,
|
|
677
|
+
userMessage,
|
|
618
678
|
sessionEventsPath: path.join(meta.runDir, input.task.id, "session-events.jsonl"),
|
|
619
679
|
// Absolute max wall clock from adaptive liveness policy (defaults to 4h).
|
|
620
680
|
timeoutMs: input.timeoutMs,
|
|
621
681
|
stallTimeoutMs: input.stallTimeoutMs,
|
|
622
682
|
abortGraceMs: input.abortGraceMs,
|
|
683
|
+
...(input.task.contextBudget
|
|
684
|
+
? { contextBudget: input.task.contextBudget }
|
|
685
|
+
: {}),
|
|
623
686
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
624
687
|
...(piExtensionPaths && piExtensionPaths.length > 0
|
|
625
688
|
? { piExtensionPaths }
|
|
@@ -668,6 +731,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
668
731
|
persona,
|
|
669
732
|
step,
|
|
670
733
|
});
|
|
734
|
+
if (input.task.id === "generate-backend-md-plan-pi" && result.assistantText?.trim()) {
|
|
735
|
+
await writeTextArtifactFile(path.join(meta.runDir, input.task.id, "plan.md"), result.assistantText.trim() + "\n");
|
|
736
|
+
}
|
|
671
737
|
const mapped = mapPiResultToDagNodeResult(result, input.task.writerOutcomePolicy
|
|
672
738
|
? WRITER_OUTCOME_PROTOCOL_LINE
|
|
673
739
|
: input.task.firstProtocolLine);
|
|
@@ -678,6 +744,27 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
678
744
|
let writeGuardViolations = [];
|
|
679
745
|
let changeManifestAfterStatus;
|
|
680
746
|
let changeManifestChangedFiles;
|
|
747
|
+
let afterWorkspaceSnapshot;
|
|
748
|
+
if (filesystemOnly && beforeWorkspaceSnapshot) {
|
|
749
|
+
try {
|
|
750
|
+
afterWorkspaceSnapshot = await captureWorkspaceWriteSnapshot({
|
|
751
|
+
rootCwd: input.cwd,
|
|
752
|
+
paths: input.task.writeSet ?? input.task.allowedPaths,
|
|
753
|
+
});
|
|
754
|
+
changeManifestChangedFiles = diffWorkspaceWriteSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot);
|
|
755
|
+
const guard = validateShellWriteGuardFromDiff({
|
|
756
|
+
changedFiles: changeManifestChangedFiles,
|
|
757
|
+
task: input.task,
|
|
758
|
+
concurrentSiblingWriteSets: meta.concurrentSiblingWriteSets,
|
|
759
|
+
});
|
|
760
|
+
writeGuardOk = guard.ok;
|
|
761
|
+
writeGuardViolations = guard.violations;
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
writeGuardOk = false;
|
|
765
|
+
writeGuardViolations = [`workspace snapshot unavailable: ${error instanceof Error ? error.message : String(error)}`];
|
|
766
|
+
}
|
|
767
|
+
}
|
|
681
768
|
if (beforeStatus !== undefined) {
|
|
682
769
|
let recoveryEvidence;
|
|
683
770
|
let removalPendingRecheck;
|
|
@@ -850,7 +937,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
850
937
|
}
|
|
851
938
|
}
|
|
852
939
|
if (writeGuardOk &&
|
|
853
|
-
beforeStatus !== undefined &&
|
|
940
|
+
(beforeStatus !== undefined || beforeWorkspaceSnapshot !== undefined) &&
|
|
854
941
|
changeManifestChangedFiles !== undefined) {
|
|
855
942
|
await persistWriterChangeManifest({
|
|
856
943
|
runDir: meta.runDir,
|
|
@@ -860,13 +947,12 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
860
947
|
approvalSourceNodeId: input.task.resolvedFinalWriteSetApproval?.approvalSourceNodeId,
|
|
861
948
|
approvalDigest: input.task.resolvedFinalWriteSetApproval?.approvalDigest,
|
|
862
949
|
changedFiles: changeManifestChangedFiles,
|
|
863
|
-
beforeStatus,
|
|
864
|
-
afterStatus: changeManifestAfterStatus ??
|
|
950
|
+
beforeStatus: beforeStatus ?? JSON.stringify(beforeWorkspaceSnapshot),
|
|
951
|
+
afterStatus: changeManifestAfterStatus ?? JSON.stringify(afterWorkspaceSnapshot),
|
|
865
952
|
});
|
|
866
953
|
}
|
|
867
954
|
let completenessFailure;
|
|
868
|
-
if (writeGuardOk &&
|
|
869
|
-
isBackendTestCompletenessRetryCandidate(input.task)) {
|
|
955
|
+
if (writeGuardOk && isBackendTestCompletenessRetryCandidate(input.task)) {
|
|
870
956
|
try {
|
|
871
957
|
const writerRole = backendTestWriterProgressRoleForTask(input.task.id);
|
|
872
958
|
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
@@ -879,7 +965,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
879
965
|
const progress = isBackendTestShardChildTask(input.task)
|
|
880
966
|
? await assessBackendTestShardChildCompleteness({
|
|
881
967
|
workspaceRoot: input.cwd,
|
|
882
|
-
role: writerRole === "pytest-generate"
|
|
968
|
+
role: writerRole === "pytest-generate"
|
|
969
|
+
? "pytest-generate"
|
|
970
|
+
: "md-generate",
|
|
883
971
|
writeSet: input.task.writeSet ?? [],
|
|
884
972
|
})
|
|
885
973
|
: isBackendTestMdPlanTask(input.task)
|
|
@@ -925,7 +1013,10 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
925
1013
|
};
|
|
926
1014
|
}
|
|
927
1015
|
}
|
|
928
|
-
if (mapped.ok &&
|
|
1016
|
+
if (mapped.ok &&
|
|
1017
|
+
writeGuardOk &&
|
|
1018
|
+
!writerOutcomeViolation &&
|
|
1019
|
+
!completenessFailure) {
|
|
929
1020
|
return mapped;
|
|
930
1021
|
}
|
|
931
1022
|
const writerThinkingExhausted = isWriterThinkingExhausted(result, mapped, changeManifestChangedFiles);
|
|
@@ -1331,9 +1422,7 @@ async function persistWriterChangeManifest(input) {
|
|
|
1331
1422
|
...(input.approvalSourceNodeId
|
|
1332
1423
|
? { approvalSourceNodeId: input.approvalSourceNodeId }
|
|
1333
1424
|
: {}),
|
|
1334
|
-
...(input.approvalDigest
|
|
1335
|
-
? { approvalDigest: input.approvalDigest }
|
|
1336
|
-
: {}),
|
|
1425
|
+
...(input.approvalDigest ? { approvalDigest: input.approvalDigest } : {}),
|
|
1337
1426
|
changedFiles: input.changedFiles,
|
|
1338
1427
|
beforeStatusSha256: createHash("sha256")
|
|
1339
1428
|
.update(input.beforeStatus)
|
|
@@ -1005,12 +1005,14 @@ export function shouldRetryWithFallback(stderr, stdout) {
|
|
|
1005
1005
|
return ["quota", "rate-limit", "unavailable", "network"].includes(category);
|
|
1006
1006
|
}
|
|
1007
1007
|
export function classifyPiFailure(input) {
|
|
1008
|
+
if (input.exitCode === 0 && input.assistantText.trim())
|
|
1009
|
+
return "success";
|
|
1010
|
+
if (input.stderr.toLowerCase().includes("context-budget-exhausted:"))
|
|
1011
|
+
return "context-budget-exhausted";
|
|
1008
1012
|
if (input.timedOut)
|
|
1009
1013
|
return "timeout";
|
|
1010
1014
|
if (input.outputTooLarge)
|
|
1011
1015
|
return "output-too-large";
|
|
1012
|
-
if (input.exitCode === 0 && input.assistantText.trim())
|
|
1013
|
-
return "success";
|
|
1014
1016
|
const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
|
|
1015
1017
|
// Prefer explicit rate-limit / HTTP 429 before generic "limit" quota phrases.
|
|
1016
1018
|
// Chinese gateways often say "请求频率已达到限制" without English "rate limit".
|
|
@@ -212,6 +212,9 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
212
212
|
const model = input.provider && input.model
|
|
213
213
|
? await resolveModel(modelRuntime, input.provider, input.model)
|
|
214
214
|
: undefined;
|
|
215
|
+
if (input.provider && input.model && !model) {
|
|
216
|
+
throw new Error(`pi SDK model unavailable: ${input.provider}/${input.model}; refusing to fall back to the session default`);
|
|
217
|
+
}
|
|
215
218
|
// Inject rate-limit-tolerant auto-retry settings for the session lifecycle.
|
|
216
219
|
// Prefer SettingsManager.inMemory so harness runs do not depend on ~/.pi or
|
|
217
220
|
// project .pi/settings.json being present/trusted.
|
|
@@ -485,6 +488,9 @@ function extractSdkUsageSample(event) {
|
|
|
485
488
|
const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
|
|
486
489
|
return responseKey ? { responseKey, tokens } : { tokens };
|
|
487
490
|
}
|
|
491
|
+
function maxSdkContextTokens(samples) {
|
|
492
|
+
return samples.reduce((maximum, sample) => Math.max(maximum, sample.tokens), 0);
|
|
493
|
+
}
|
|
488
494
|
function aggregateSdkTokenUsage(samples) {
|
|
489
495
|
const identified = new Map();
|
|
490
496
|
let anonymousMaximum = 0;
|
|
@@ -537,6 +543,9 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
537
543
|
const stdoutPreview = new BoundedTextPreview("stdout");
|
|
538
544
|
const stdoutCollector = createPiJsonlStreamCollector();
|
|
539
545
|
const usageSamples = [];
|
|
546
|
+
let observedTurns = 0;
|
|
547
|
+
let contextBudgetExhausted = false;
|
|
548
|
+
let contextBudgetDetail = "";
|
|
540
549
|
// Writer-thinking-exhausted diagnosis evidence (AC-002). Collected in the
|
|
541
550
|
// subscribe callback regardless of shouldPersistSessionEvent filtering, so a
|
|
542
551
|
// length-stopped thinking-only attempt with no persisted write tool calls is
|
|
@@ -625,6 +634,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
625
634
|
});
|
|
626
635
|
let resolveStall;
|
|
627
636
|
let resolveSettlement;
|
|
637
|
+
let resolveContextBudget;
|
|
628
638
|
let settled = false;
|
|
629
639
|
let abortConfirmed = true;
|
|
630
640
|
const persistRuntimeNote = (event) => {
|
|
@@ -655,6 +665,30 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
655
665
|
clearTimeout(stallHandle);
|
|
656
666
|
stallHandle = setTimeout(() => resolveStall?.("stall"), stallTimeoutMs);
|
|
657
667
|
};
|
|
668
|
+
const checkContextBudget = () => {
|
|
669
|
+
if (contextBudgetExhausted || !options.contextBudget)
|
|
670
|
+
return;
|
|
671
|
+
// Context protection follows the largest provider-reported response
|
|
672
|
+
// snapshot, not cumulative billing usage across turns. Summing repeated
|
|
673
|
+
// input contexts would abort healthy multi-turn sessions far below the
|
|
674
|
+
// model's actual context window.
|
|
675
|
+
const observedTokens = maxSdkContextTokens(usageSamples);
|
|
676
|
+
const turnExceeded = options.contextBudget.maxTurns !== undefined &&
|
|
677
|
+
observedTurns >= options.contextBudget.maxTurns;
|
|
678
|
+
const tokenExceeded = options.contextBudget.maxTokens !== undefined &&
|
|
679
|
+
observedTokens >= options.contextBudget.maxTokens;
|
|
680
|
+
if (!turnExceeded && !tokenExceeded)
|
|
681
|
+
return;
|
|
682
|
+
contextBudgetExhausted = true;
|
|
683
|
+
contextBudgetDetail = [
|
|
684
|
+
"context-budget-exhausted:",
|
|
685
|
+
`observedTurns=${observedTurns}`,
|
|
686
|
+
`observedTokens=${observedTokens}`,
|
|
687
|
+
`maxTurns=${options.contextBudget.maxTurns ?? "unbounded"}`,
|
|
688
|
+
`maxTokens=${options.contextBudget.maxTokens ?? "unbounded"}`,
|
|
689
|
+
].join(" ");
|
|
690
|
+
resolveContextBudget?.("context-budget");
|
|
691
|
+
};
|
|
658
692
|
unsubscribe = session.subscribe((event) => {
|
|
659
693
|
if (event.type === "agent_settled" && !settled) {
|
|
660
694
|
settled = true;
|
|
@@ -678,6 +712,9 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
678
712
|
const usageSample = extractSdkUsageSample(event);
|
|
679
713
|
if (usageSample)
|
|
680
714
|
usageSamples.push(usageSample);
|
|
715
|
+
if (event.type === "turn_end")
|
|
716
|
+
observedTurns += 1;
|
|
717
|
+
checkContextBudget();
|
|
681
718
|
// Capture writer-thinking-exhausted evidence BEFORE the
|
|
682
719
|
// shouldPersistSessionEvent filter: thinking_delta is intentionally
|
|
683
720
|
// excluded from persisted JSONL, but must still count toward the
|
|
@@ -700,6 +737,11 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
700
737
|
const runTurn = async (userMessage) => {
|
|
701
738
|
const settlementPromise = resetSettlement();
|
|
702
739
|
const stallPromise = resetStallPromise();
|
|
740
|
+
const contextBudgetPromise = options.contextBudget
|
|
741
|
+
? new Promise((resolve) => {
|
|
742
|
+
resolveContextBudget = resolve;
|
|
743
|
+
})
|
|
744
|
+
: null;
|
|
703
745
|
const promptPromise = session.prompt(userMessage);
|
|
704
746
|
// A settlement event is permitted to win the race while prompt() remains
|
|
705
747
|
// pending. Observe a later rejection so it never becomes unhandled.
|
|
@@ -710,6 +752,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
710
752
|
settlementPromise,
|
|
711
753
|
...(timeoutPromise ? [timeoutPromise] : []),
|
|
712
754
|
...(stallPromise ? [stallPromise] : []),
|
|
755
|
+
...(contextBudgetPromise ? [contextBudgetPromise] : []),
|
|
713
756
|
]);
|
|
714
757
|
};
|
|
715
758
|
const disposeSession = async () => {
|
|
@@ -737,7 +780,12 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
737
780
|
: options.userMessage;
|
|
738
781
|
let continuesSent = 0;
|
|
739
782
|
let raced = await runTurn(promptMessage);
|
|
740
|
-
if (raced === "
|
|
783
|
+
if (raced === "context-budget") {
|
|
784
|
+
appendStderr(contextBudgetDetail);
|
|
785
|
+
abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
|
|
786
|
+
await disposeSession();
|
|
787
|
+
}
|
|
788
|
+
else if (raced === "stall") {
|
|
741
789
|
abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
|
|
742
790
|
const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
|
|
743
791
|
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,
|
|
@@ -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
|
+
}
|
|
@@ -38,6 +38,8 @@ export function openConsoleAppData(options) {
|
|
|
38
38
|
confirmations: path.join(repoNamespace, "confirmations"),
|
|
39
39
|
dagReceipts: path.join(repoNamespace, "dag-receipts"),
|
|
40
40
|
staged: path.join(repoNamespace, "staged"),
|
|
41
|
+
contextExports: path.join(repoNamespace, "context-exports"),
|
|
42
|
+
artifactRevisions: path.join(repoNamespace, "artifact-revisions"),
|
|
41
43
|
assessments: path.join(repoNamespace, "assessments"),
|
|
42
44
|
interviews: path.join(repoNamespace, "interviews"),
|
|
43
45
|
chats: path.join(repoNamespace, "chats"),
|
|
@@ -51,6 +53,8 @@ export function openConsoleAppData(options) {
|
|
|
51
53
|
paths.confirmations,
|
|
52
54
|
paths.dagReceipts,
|
|
53
55
|
paths.staged,
|
|
56
|
+
paths.contextExports,
|
|
57
|
+
paths.artifactRevisions,
|
|
54
58
|
paths.assessments,
|
|
55
59
|
paths.interviews,
|
|
56
60
|
paths.chats,
|