@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30
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 +4 -1
- package/CHANGELOG.md +267 -65
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +118 -14
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +8 -1
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +533 -29
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +22 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +478 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +244 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +12 -5
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +83 -10
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
- package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +805 -113
- package/dist/workflows/dag/node-execution.js +194 -6
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +113 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +157 -1
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -1
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +9 -6
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { collectNextInfo, formatNextInfo, } from "../../scripts/next-info.mjs";
|
|
2
|
+
export async function runInspectNext(repoRoot, args = []) {
|
|
3
|
+
const json = args.includes("--json");
|
|
4
|
+
const result = collectNextInfo({ cwd: repoRoot });
|
|
5
|
+
process.stdout.write(formatNextInfo(result, { json }));
|
|
6
|
+
if (!result.ok)
|
|
7
|
+
process.exitCode = result.exitCode || 1;
|
|
8
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
|
|
2
|
+
import { backendTestConfigSchema, } from "../task/config-types.js";
|
|
2
3
|
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
3
4
|
import { createRunDagProgressObserver, validateRunDagProgressIntervalMs, } from "./run-dag-progress.js";
|
|
4
5
|
const COMMAND = "task advance";
|
|
@@ -14,6 +15,7 @@ const USAGE = `usage:
|
|
|
14
15
|
[--forbidden-path <glob>]...
|
|
15
16
|
[--verify <label:command>]...
|
|
16
17
|
[--verify-timeout <label:timeoutMs>]...
|
|
18
|
+
[--backend-test <json>]
|
|
17
19
|
[--accept-recommendations <digest>]
|
|
18
20
|
[--approve-gate <gate-id:digest>]
|
|
19
21
|
[--reject-gate <gate-id:digest> --reason <text>]
|
|
@@ -52,6 +54,30 @@ function parseVerifyTimeout(flag) {
|
|
|
52
54
|
}
|
|
53
55
|
return { label: flag.slice(0, idx), timeoutMs };
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse `--backend-test <json>` into a validated managed backendTest config.
|
|
59
|
+
* `{}` is legal (explicit clear back to the default layout).
|
|
60
|
+
*/
|
|
61
|
+
function parseBackendTestFlag(raw) {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`invalid --backend-test ${raw}; expected an inline JSON object`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
70
|
+
throw new Error(`invalid --backend-test ${raw}; expected an inline JSON object`);
|
|
71
|
+
}
|
|
72
|
+
const result = backendTestConfigSchema.safeParse(parsed);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
const summary = result.error.issues
|
|
75
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
76
|
+
.join("; ");
|
|
77
|
+
throw new Error(`invalid --backend-test: ${summary}`);
|
|
78
|
+
}
|
|
79
|
+
return result.data ?? {};
|
|
80
|
+
}
|
|
55
81
|
export function parseTaskAdvanceArgs(args) {
|
|
56
82
|
const options = {};
|
|
57
83
|
const positional = [];
|
|
@@ -113,6 +139,10 @@ export function parseTaskAdvanceArgs(args) {
|
|
|
113
139
|
options.verifyTimeout = pushList(options.verifyTimeout, next());
|
|
114
140
|
continue;
|
|
115
141
|
}
|
|
142
|
+
if (token === "--backend-test") {
|
|
143
|
+
options.backendTest = parseBackendTestFlag(next());
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
116
146
|
if (token === "--accept-recommendations") {
|
|
117
147
|
options.acceptRecommendations = next();
|
|
118
148
|
continue;
|
|
@@ -273,6 +303,7 @@ export function toUseCaseInput(repoRoot, taskId, options, observer) {
|
|
|
273
303
|
allowedPaths: options.allowedPaths,
|
|
274
304
|
forbiddenPaths: options.forbiddenPaths,
|
|
275
305
|
verifyCommands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
306
|
+
backendTest: options.backendTest,
|
|
276
307
|
approveGateToken: options.approveGate,
|
|
277
308
|
rejectGateToken: options.rejectGate,
|
|
278
309
|
rejectReason: options.reason,
|
|
@@ -2,14 +2,16 @@ import path from "node:path";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
5
|
-
import { executePiStep, } from "./pi-executor.js";
|
|
5
|
+
import { executePiStep, resolvePiBackend, } from "./pi-executor.js";
|
|
6
|
+
import { resolveDagPiExtensions, } from "./pi-extension-resolver.js";
|
|
6
7
|
import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
|
|
7
8
|
import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
|
|
8
9
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
9
10
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
10
11
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
11
|
-
import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
12
|
+
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";
|
|
12
13
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
14
|
+
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
13
15
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
14
16
|
/**
|
|
15
17
|
* Writer classification for a length-stopped thinking-only attempt: the model
|
|
@@ -326,6 +328,36 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
|
|
|
326
328
|
await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
|
|
327
329
|
return { promptPath, summaryPath };
|
|
328
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Run-owned per-node Pi extension facts (plan 2026-08-21): requested ids,
|
|
333
|
+
* resolved packages (with absolute entry paths), and degraded/missing ids.
|
|
334
|
+
* Written only for nodes that declare `piExtensions`; never into the repo-level
|
|
335
|
+
* DAG template. Best-effort: artifact failures never fail the node.
|
|
336
|
+
*/
|
|
337
|
+
async function writePiExtensionsArtifact(nodeArtifactsDir, requested, resolved, missing) {
|
|
338
|
+
try {
|
|
339
|
+
const runDir = path.dirname(nodeArtifactsDir);
|
|
340
|
+
const nodeId = path.basename(nodeArtifactsDir);
|
|
341
|
+
await writeDagNodeJsonArtifact(runDir, nodeId, "pi-extensions.json", {
|
|
342
|
+
schemaVersion: 1,
|
|
343
|
+
requested: [...requested],
|
|
344
|
+
resolved: resolved.map((entry) => ({
|
|
345
|
+
id: entry.id,
|
|
346
|
+
packageName: entry.packageName,
|
|
347
|
+
version: entry.version,
|
|
348
|
+
path: entry.entryPaths.join(", "),
|
|
349
|
+
})),
|
|
350
|
+
missing: missing.map((entry) => ({
|
|
351
|
+
id: entry.id,
|
|
352
|
+
reason: entry.reason,
|
|
353
|
+
...(entry.detail ? { detail: entry.detail } : {}),
|
|
354
|
+
})),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// best-effort run fact; node execution continues
|
|
359
|
+
}
|
|
360
|
+
}
|
|
329
361
|
async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
|
|
330
362
|
try {
|
|
331
363
|
const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
|
|
@@ -549,6 +581,29 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
549
581
|
};
|
|
550
582
|
}
|
|
551
583
|
}
|
|
584
|
+
// Plan 2026-08-21 D4/D6: resolve the node's explicit Pi extension
|
|
585
|
+
// allowlist against the settings inventory. Degrade on missing packages —
|
|
586
|
+
// never fail the node — and persist requested/resolved/missing facts.
|
|
587
|
+
const piExtensionsResolution = input.task.piExtensions
|
|
588
|
+
? resolveDagPiExtensions({
|
|
589
|
+
repoRoot: input.cwd,
|
|
590
|
+
ids: input.task.piExtensions,
|
|
591
|
+
})
|
|
592
|
+
: undefined;
|
|
593
|
+
if (piExtensionsResolution) {
|
|
594
|
+
const piBackend = resolvePiBackend();
|
|
595
|
+
const cliDegraded = piBackend === "cli-only";
|
|
596
|
+
await writePiExtensionsArtifact(path.join(meta.runDir, input.task.id), input.task.piExtensions, cliDegraded ? [] : piExtensionsResolution.resolved, cliDegraded
|
|
597
|
+
? input.task.piExtensions.map((id) => ({
|
|
598
|
+
id,
|
|
599
|
+
reason: "not-installed",
|
|
600
|
+
detail: "cli backend does not load pi extensions (sdk-only v1)",
|
|
601
|
+
}))
|
|
602
|
+
: piExtensionsResolution.missing);
|
|
603
|
+
}
|
|
604
|
+
const piExtensionPaths = piExtensionsResolution && resolvePiBackend() !== "cli-only"
|
|
605
|
+
? piExtensionsResolution.resolved.flatMap((entry) => entry.entryPaths)
|
|
606
|
+
: undefined;
|
|
552
607
|
result = await piStepFn({
|
|
553
608
|
attachedFiles: [],
|
|
554
609
|
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
@@ -563,6 +618,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
563
618
|
stallTimeoutMs: input.stallTimeoutMs,
|
|
564
619
|
abortGraceMs: input.abortGraceMs,
|
|
565
620
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
621
|
+
...(piExtensionPaths && piExtensionPaths.length > 0
|
|
622
|
+
? { piExtensionPaths }
|
|
623
|
+
: {}),
|
|
566
624
|
onActivity: bridgeActivity
|
|
567
625
|
? (activity) => {
|
|
568
626
|
if (activity.kind === "lease" ||
|
|
@@ -808,6 +866,13 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
808
866
|
isBackendTestCompletenessRetryCandidate(input.task)) {
|
|
809
867
|
try {
|
|
810
868
|
const writerRole = backendTestWriterProgressRoleForTask(input.task.id);
|
|
869
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
870
|
+
? {
|
|
871
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
872
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
873
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
874
|
+
}
|
|
875
|
+
: undefined);
|
|
811
876
|
const progress = isBackendTestShardChildTask(input.task)
|
|
812
877
|
? await assessBackendTestShardChildCompleteness({
|
|
813
878
|
workspaceRoot: input.cwd,
|
|
@@ -815,12 +880,12 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
815
880
|
writeSet: input.task.writeSet ?? [],
|
|
816
881
|
})
|
|
817
882
|
: isBackendTestMdPlanTask(input.task)
|
|
818
|
-
? await assessBackendTestMdPlanCompleteness(input.cwd)
|
|
883
|
+
? await assessBackendTestMdPlanCompleteness(input.cwd, backendLayout)
|
|
819
884
|
: isBackendTestPytestPlanTask(input.task)
|
|
820
|
-
? await assessBackendTestPytestPlanCompleteness(input.cwd)
|
|
885
|
+
? await assessBackendTestPytestPlanCompleteness(input.cwd, backendLayout)
|
|
821
886
|
: writerRole === "pytest-generate"
|
|
822
|
-
? await assessBackendTestPytestWriterCompleteness(input.cwd)
|
|
823
|
-
: await assessBackendTestMdWriterCompleteness(input.cwd);
|
|
887
|
+
? await assessBackendTestPytestWriterCompleteness(input.cwd, backendLayout)
|
|
888
|
+
: await assessBackendTestMdWriterCompleteness(input.cwd, backendLayout);
|
|
824
889
|
await writeBackendTestWriterProgressArtifacts({
|
|
825
890
|
runDir: meta.runDir,
|
|
826
891
|
progress,
|
|
@@ -877,22 +942,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
877
942
|
if (meta.writeGuardAttribution === "best-effort") {
|
|
878
943
|
stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
|
|
879
944
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const writerCleanTimeout = !mapped.ok &&
|
|
884
|
-
mapped.failureCategory === "timeout" &&
|
|
885
|
-
writeGuardOk &&
|
|
945
|
+
const rawFailureCategory = mapped.failureCategory;
|
|
946
|
+
const writeToolCallCount = readWriterThinkingExhaustionEvidence(result).writeToolCallCount;
|
|
947
|
+
const zeroSideEffects = writeGuardOk &&
|
|
886
948
|
!completenessFailure &&
|
|
887
949
|
!writerThinkingExhausted &&
|
|
888
|
-
|
|
950
|
+
!writerOutcomeViolation &&
|
|
889
951
|
changeManifestChangedFiles !== undefined &&
|
|
890
952
|
changeManifestChangedFiles.length === 0 &&
|
|
891
|
-
|
|
953
|
+
writeToolCallCount === 0;
|
|
954
|
+
const targetCleanTransport = !mapped.ok &&
|
|
955
|
+
isTargetTemplateTransientRetryNode(input.task) &&
|
|
956
|
+
rawFailureCategory !== undefined &&
|
|
957
|
+
["timeout", "network", "rate-limit", "unavailable"].includes(rawFailureCategory) &&
|
|
958
|
+
zeroSideEffects;
|
|
959
|
+
const frontendCleanTimeout = !mapped.ok &&
|
|
960
|
+
rawFailureCategory === "timeout" &&
|
|
961
|
+
isWriterTransportRetryCandidate(input.task) &&
|
|
962
|
+
!isTargetTemplateTransientRetryNode(input.task) &&
|
|
963
|
+
zeroSideEffects;
|
|
964
|
+
const writerCleanTimeout = targetCleanTransport || frontendCleanTimeout;
|
|
892
965
|
return {
|
|
893
966
|
...mapped,
|
|
894
967
|
ok: false,
|
|
895
968
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
969
|
+
...(rawFailureCategory ? { rawFailureCategory } : {}),
|
|
896
970
|
failureCategory: mapped.ok
|
|
897
971
|
? writeGuardOk
|
|
898
972
|
? completenessFailure
|
|
@@ -977,6 +1051,15 @@ function parseWriterImplementationOutcome(text) {
|
|
|
977
1051
|
const lines = text.split(/\r?\n/);
|
|
978
1052
|
const candidates = [];
|
|
979
1053
|
for (const [lineIndex, line] of lines.entries()) {
|
|
1054
|
+
const localizedOutcome = localizedWriterImplementationOutcome(line);
|
|
1055
|
+
if (localizedOutcome) {
|
|
1056
|
+
candidates.push({
|
|
1057
|
+
lineIndex,
|
|
1058
|
+
value: localizedOutcome,
|
|
1059
|
+
outcome: localizedOutcome,
|
|
1060
|
+
});
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
980
1063
|
const nextLine = lines[lineIndex + 1];
|
|
981
1064
|
const normalized = normalizeProtocolLine(line, WRITER_OUTCOME_PROTOCOL_LINE, nextLine);
|
|
982
1065
|
if (normalized === undefined)
|
|
@@ -1005,6 +1088,26 @@ function parseWriterImplementationOutcome(text) {
|
|
|
1005
1088
|
candidates,
|
|
1006
1089
|
};
|
|
1007
1090
|
}
|
|
1091
|
+
function localizedWriterImplementationOutcome(line) {
|
|
1092
|
+
const normalized = normalizedProtocolValue(line);
|
|
1093
|
+
const match = /^(?:实现|实施)结果\s*[::]\s*(已变更|已更改|已修改|已满足|无需变更|已阻塞|阻塞)$/u.exec(normalized);
|
|
1094
|
+
if (!match)
|
|
1095
|
+
return undefined;
|
|
1096
|
+
switch (match[1]) {
|
|
1097
|
+
case "已变更":
|
|
1098
|
+
case "已更改":
|
|
1099
|
+
case "已修改":
|
|
1100
|
+
return "changed";
|
|
1101
|
+
case "已满足":
|
|
1102
|
+
case "无需变更":
|
|
1103
|
+
return "already-satisfied";
|
|
1104
|
+
case "已阻塞":
|
|
1105
|
+
case "阻塞":
|
|
1106
|
+
return "blocked";
|
|
1107
|
+
default:
|
|
1108
|
+
return undefined;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1008
1111
|
function isWriterImplementationOutcome(value) {
|
|
1009
1112
|
return (value === "changed" || value === "already-satisfied" || value === "blocked");
|
|
1010
1113
|
}
|
|
@@ -1043,6 +1146,7 @@ export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
|
|
|
1043
1146
|
sdkAttempted: result.sdkAttempted,
|
|
1044
1147
|
tokensUsed: result.tokensUsed,
|
|
1045
1148
|
parsedEvents: result.parsedEvents,
|
|
1149
|
+
stopReason: readWriterThinkingExhaustionEvidence(result).stopReason,
|
|
1046
1150
|
};
|
|
1047
1151
|
}
|
|
1048
1152
|
function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
@@ -51,9 +51,50 @@ export function normalizePiSessionEvent(event) {
|
|
|
51
51
|
}
|
|
52
52
|
return unifyToolPayloadFields(event);
|
|
53
53
|
}
|
|
54
|
+
const TERMINAL_FINISH_REASON_TYPES = new Set([
|
|
55
|
+
'message_end',
|
|
56
|
+
'turn_end',
|
|
57
|
+
'assistant_message',
|
|
58
|
+
]);
|
|
59
|
+
function readExplicitFinishReason(source) {
|
|
60
|
+
const message = isRecord(source.message) ? source.message : undefined;
|
|
61
|
+
const candidates = [
|
|
62
|
+
source.finish_reason,
|
|
63
|
+
source.finishReason,
|
|
64
|
+
source.stop_reason,
|
|
65
|
+
source.stopReason,
|
|
66
|
+
message?.finish_reason,
|
|
67
|
+
message?.finishReason,
|
|
68
|
+
message?.stop_reason,
|
|
69
|
+
message?.stopReason,
|
|
70
|
+
];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function applyFinishReason(original, normalized) {
|
|
79
|
+
const originalType = typeof original.type === 'string' ? original.type : undefined;
|
|
80
|
+
if (!originalType || !TERMINAL_FINISH_REASON_TYPES.has(originalType)) {
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
const existing = readExplicitFinishReason({
|
|
84
|
+
finish_reason: normalized.finish_reason,
|
|
85
|
+
});
|
|
86
|
+
if (existing !== undefined) {
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
const promoted = readExplicitFinishReason(original);
|
|
90
|
+
if (promoted === undefined) {
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
return { ...normalized, finish_reason: promoted };
|
|
94
|
+
}
|
|
54
95
|
/** Serialize one SDK session event as a single JSONL line. */
|
|
55
96
|
export function serializeSessionEvent(event, options) {
|
|
56
|
-
const normalized = normalizePiSessionEvent(event);
|
|
97
|
+
const normalized = applyFinishReason(event, normalizePiSessionEvent(event));
|
|
57
98
|
if (typeof normalized.recordedAt !== 'string') {
|
|
58
99
|
normalized.recordedAt = options?.recordedAt ?? new Date().toISOString();
|
|
59
100
|
}
|
|
@@ -408,6 +408,50 @@ export class BoundedTextPreview {
|
|
|
408
408
|
].join("\n");
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Normalize provider-specific completion-limit labels to the DAG's canonical
|
|
413
|
+
* `length` signal. Other terminal reasons remain observable without being
|
|
414
|
+
* treated as truncation.
|
|
415
|
+
*/
|
|
416
|
+
export function normalizeProviderStopReason(value) {
|
|
417
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
418
|
+
return undefined;
|
|
419
|
+
const trimmed = value.trim();
|
|
420
|
+
const compact = trimmed.toLowerCase().replace(/[\s_-]/g, "");
|
|
421
|
+
if ([
|
|
422
|
+
"length",
|
|
423
|
+
"maxtokens",
|
|
424
|
+
"maxoutputtokens",
|
|
425
|
+
"outputlimit",
|
|
426
|
+
"tokenlimit",
|
|
427
|
+
"tokenlimitreached",
|
|
428
|
+
].includes(compact)) {
|
|
429
|
+
return "length";
|
|
430
|
+
}
|
|
431
|
+
return trimmed;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Extract a terminal stop reason from the provider event shapes used by Pi
|
|
435
|
+
* SDK/CLI bridges. Providers disagree on `stop*` versus `finish*`; keep this
|
|
436
|
+
* boundary centralized so downstream DAG recovery sees one canonical signal.
|
|
437
|
+
*/
|
|
438
|
+
export function extractPiEventStopReason(event) {
|
|
439
|
+
const nested = [event.message, event.response, event.completion].filter(isRecord);
|
|
440
|
+
const containers = [...nested, event];
|
|
441
|
+
for (const container of containers) {
|
|
442
|
+
for (const key of [
|
|
443
|
+
"stopReason",
|
|
444
|
+
"stop_reason",
|
|
445
|
+
"finishReason",
|
|
446
|
+
"finish_reason",
|
|
447
|
+
]) {
|
|
448
|
+
const normalized = normalizeProviderStopReason(container[key]);
|
|
449
|
+
if (normalized)
|
|
450
|
+
return normalized;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
411
455
|
export class PiJsonlStreamCollector {
|
|
412
456
|
lineBuffer = "";
|
|
413
457
|
discardUntilNewline = false;
|
|
@@ -415,6 +459,7 @@ export class PiJsonlStreamCollector {
|
|
|
415
459
|
parsedEvents = 0;
|
|
416
460
|
tokensUsed = 0;
|
|
417
461
|
outputTooLarge = false;
|
|
462
|
+
stopReason;
|
|
418
463
|
subagentTotalCalls = 0;
|
|
419
464
|
subagentFailedCalls = 0;
|
|
420
465
|
subagentModes = new Set();
|
|
@@ -454,6 +499,7 @@ export class PiJsonlStreamCollector {
|
|
|
454
499
|
assistantText: this.assistantText.trim(),
|
|
455
500
|
outputTooLarge: this.outputTooLarge,
|
|
456
501
|
parsedEvents: this.parsedEvents,
|
|
502
|
+
stopReason: this.stopReason,
|
|
457
503
|
subagentStats: this.formatSubagentStats(),
|
|
458
504
|
tokensUsed: this.tokensUsed,
|
|
459
505
|
};
|
|
@@ -472,6 +518,10 @@ export class PiJsonlStreamCollector {
|
|
|
472
518
|
}
|
|
473
519
|
}
|
|
474
520
|
consumeEvent(event) {
|
|
521
|
+
const stopReason = extractPiEventStopReason(event);
|
|
522
|
+
if (stopReason === "length" || !this.stopReason) {
|
|
523
|
+
this.stopReason = stopReason ?? this.stopReason;
|
|
524
|
+
}
|
|
475
525
|
const candidate = extractAssistantTextFromEvent(event);
|
|
476
526
|
if (candidate !== undefined) {
|
|
477
527
|
if (candidate.length > MAX_ASSISTANT_TEXT_CHARS) {
|
|
@@ -865,6 +915,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
865
915
|
stdoutTruncated: stdoutPreview.truncated,
|
|
866
916
|
stderrTruncated: stderrPreview.truncated,
|
|
867
917
|
outputTooLarge: collected.outputTooLarge,
|
|
918
|
+
stopReason: collected.stopReason,
|
|
868
919
|
timedOut,
|
|
869
920
|
tokensUsed: collected.tokensUsed,
|
|
870
921
|
subagentStats: collected.subagentStats,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Pi extension allowlist for backend implementation DAGs (plan D2/D3).
|
|
6
|
+
* DAG specs carry these frozen short ids; the executor resolves them against
|
|
7
|
+
* the Pi agent settings package inventory at run time.
|
|
8
|
+
*/
|
|
9
|
+
export const DAG_PI_EXTENSION_IDS = ["pi-codegraph", "pi-lens"];
|
|
10
|
+
/**
|
|
11
|
+
* Alias table: one DAG short id may resolve to multiple published package
|
|
12
|
+
* names. The first settings spec whose de-`npm:`-prefixed package name is in
|
|
13
|
+
* the alias set wins (project settings override user settings).
|
|
14
|
+
*/
|
|
15
|
+
export const DAG_PI_EXTENSION_PACKAGE_ALIASES = {
|
|
16
|
+
"pi-codegraph": [
|
|
17
|
+
"@estebanforge/pi-codegraph-enhanced",
|
|
18
|
+
"@vndv/pi-codegraph",
|
|
19
|
+
"pi-codegraph",
|
|
20
|
+
],
|
|
21
|
+
"pi-lens": ["pi-lens"],
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Extension tools that must never be activated as extras on any node (D5):
|
|
25
|
+
* they mutate files without the writer path policy. Writer nodes keep only
|
|
26
|
+
* policy-wrapped edit/write; read nodes keep only navigation tools.
|
|
27
|
+
*/
|
|
28
|
+
export const DAG_PI_EXTENSION_WRITE_TOOL_DENY = new Set([
|
|
29
|
+
"ast_grep_replace",
|
|
30
|
+
]);
|
|
31
|
+
/** Builtin tool names extensions must never replace (D5). */
|
|
32
|
+
export const DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS = new Set(["read", "edit", "write", "bash", "grep", "find", "ls"]);
|
|
33
|
+
/** Deny surface extensions must never activate (D5). */
|
|
34
|
+
export const DAG_PI_EXTENSION_DENIED_TOOL_IDS = new Set([
|
|
35
|
+
"apply_patch",
|
|
36
|
+
"apply-patch",
|
|
37
|
+
"full-tools",
|
|
38
|
+
"full_tools",
|
|
39
|
+
"coding-chat",
|
|
40
|
+
"coding_chat",
|
|
41
|
+
"shell",
|
|
42
|
+
]);
|
|
43
|
+
export function isDagPiExtensionId(value) {
|
|
44
|
+
return DAG_PI_EXTENSION_IDS.includes(value);
|
|
45
|
+
}
|
|
46
|
+
function getPiAgentDir() {
|
|
47
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
48
|
+
if (envDir)
|
|
49
|
+
return envDir;
|
|
50
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
51
|
+
}
|
|
52
|
+
function readSettingsPackages(settingsPath) {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(settingsPath))
|
|
55
|
+
return undefined;
|
|
56
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
57
|
+
if (!Array.isArray(parsed.packages))
|
|
58
|
+
return undefined;
|
|
59
|
+
return parsed.packages.filter((spec) => typeof spec === "string");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Strip an `npm:` prefix from a settings package spec, e.g. `npm:pi-lens`. */
|
|
66
|
+
export function packageNameFromSpec(spec) {
|
|
67
|
+
return spec.startsWith("npm:") ? spec.slice(4) : spec;
|
|
68
|
+
}
|
|
69
|
+
function loadPackageInventories(repoRoot) {
|
|
70
|
+
const agentDir = getPiAgentDir();
|
|
71
|
+
const userSpecs = readSettingsPackages(path.join(agentDir, "settings.json"));
|
|
72
|
+
const projectSpecs = readSettingsPackages(path.join(repoRoot, ".pi", "settings.json"));
|
|
73
|
+
const inventories = [];
|
|
74
|
+
if (userSpecs) {
|
|
75
|
+
inventories.push({
|
|
76
|
+
specs: userSpecs,
|
|
77
|
+
nodeModulesRoot: path.join(agentDir, "npm", "node_modules"),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (projectSpecs) {
|
|
81
|
+
inventories.push({
|
|
82
|
+
specs: projectSpecs,
|
|
83
|
+
nodeModulesRoot: path.join(repoRoot, "node_modules"),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return inventories;
|
|
87
|
+
}
|
|
88
|
+
const EXTENSION_FILE_RE = /\.(?:ts|js|mjs|cjs)$/;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a package's `pi.extensions` entries to absolute entry paths.
|
|
91
|
+
* - A file entry maps to itself.
|
|
92
|
+
* - A directory entry expands to every extension file directly inside it
|
|
93
|
+
* (e.g. `@estebanforge/pi-codegraph-enhanced` ships `./extensions` holding
|
|
94
|
+
* `codegraph.ts`).
|
|
95
|
+
* No `pi.extensions` declaration, or a declaration whose paths do not exist,
|
|
96
|
+
* yields no entries (caller records `no-entry`) — no heuristic scanning
|
|
97
|
+
* outside declared paths.
|
|
98
|
+
*/
|
|
99
|
+
export function resolvePackageEntryPaths(packageDir) {
|
|
100
|
+
let manifest;
|
|
101
|
+
try {
|
|
102
|
+
manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { entryPaths: [], reason: "no-entry" };
|
|
106
|
+
}
|
|
107
|
+
const declared = manifest.pi?.extensions;
|
|
108
|
+
if (!Array.isArray(declared)) {
|
|
109
|
+
return { entryPaths: [], reason: "no-entry" };
|
|
110
|
+
}
|
|
111
|
+
const entryPaths = [];
|
|
112
|
+
for (const entry of declared) {
|
|
113
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
114
|
+
continue;
|
|
115
|
+
const resolved = path.resolve(packageDir, entry);
|
|
116
|
+
if (!existsSync(resolved))
|
|
117
|
+
continue;
|
|
118
|
+
let isDirectory = false;
|
|
119
|
+
try {
|
|
120
|
+
isDirectory = statSync(resolved).isDirectory();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!isDirectory) {
|
|
126
|
+
entryPaths.push(resolved);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let names;
|
|
130
|
+
try {
|
|
131
|
+
names = readdirSync(resolved);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (const name of names.sort()) {
|
|
137
|
+
if (!EXTENSION_FILE_RE.test(name))
|
|
138
|
+
continue;
|
|
139
|
+
entryPaths.push(path.join(resolved, name));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (entryPaths.length === 0) {
|
|
143
|
+
return { entryPaths: [], reason: "no-entry" };
|
|
144
|
+
}
|
|
145
|
+
return { entryPaths };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolve frozen DAG short ids against the Pi settings package inventory
|
|
149
|
+
* (user `~/.pi/agent/settings.json`, then project `.pi/settings.json` — a
|
|
150
|
+
* later match on the same alias wins). Pure synchronous read; never throws.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveDagPiExtensions(input) {
|
|
153
|
+
const inventories = loadPackageInventories(input.repoRoot);
|
|
154
|
+
const resolved = [];
|
|
155
|
+
const missing = [];
|
|
156
|
+
for (const id of input.ids) {
|
|
157
|
+
if (!isDagPiExtensionId(id)) {
|
|
158
|
+
missing.push({
|
|
159
|
+
id,
|
|
160
|
+
reason: "package-unreadable",
|
|
161
|
+
detail: `unknown pi extension id: ${id}`,
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const aliases = DAG_PI_EXTENSION_PACKAGE_ALIASES[id];
|
|
166
|
+
let matched;
|
|
167
|
+
// Later inventories (project) override earlier (user).
|
|
168
|
+
for (const inventory of inventories) {
|
|
169
|
+
for (const spec of inventory.specs) {
|
|
170
|
+
const packageName = packageNameFromSpec(spec);
|
|
171
|
+
if (!aliases.includes(packageName))
|
|
172
|
+
continue;
|
|
173
|
+
const packageDir = path.join(inventory.nodeModulesRoot, ...packageName.split("/"));
|
|
174
|
+
if (!existsSync(packageDir))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
const manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
178
|
+
if (typeof manifest.version !== "string")
|
|
179
|
+
continue;
|
|
180
|
+
matched = { packageDir, packageName, version: manifest.version };
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// unreadable manifest: skip this candidate
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!matched) {
|
|
188
|
+
missing.push({ id, reason: "not-installed" });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const entries = resolvePackageEntryPaths(matched.packageDir);
|
|
192
|
+
if (entries.entryPaths.length === 0) {
|
|
193
|
+
missing.push({
|
|
194
|
+
id,
|
|
195
|
+
reason: entries.reason === "no-entry" ? "no-entry" : "package-unreadable",
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
resolved.push({
|
|
200
|
+
id,
|
|
201
|
+
packageName: matched.packageName,
|
|
202
|
+
version: matched.version,
|
|
203
|
+
entryPaths: entries.entryPaths,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return { resolved, missing };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Compute extension tool names that may join a node's active tool allowlist
|
|
210
|
+
* after session creation (D5): new tool names registered by the loaded
|
|
211
|
+
* extensions, minus protected builtins, the deny surface, and write-capable
|
|
212
|
+
* extension tools (both buckets — writer writes stay policy-wrapped only).
|
|
213
|
+
*/
|
|
214
|
+
export function computeExtensionToolAllowlistExtras(sessionTools, baseline) {
|
|
215
|
+
const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
|
|
216
|
+
const extras = [];
|
|
217
|
+
for (const tool of sessionTools) {
|
|
218
|
+
const name = tool && typeof tool.name === "string" ? tool.name : undefined;
|
|
219
|
+
if (!name)
|
|
220
|
+
continue;
|
|
221
|
+
const lower = name.toLowerCase();
|
|
222
|
+
if (baselineLower.has(lower))
|
|
223
|
+
continue;
|
|
224
|
+
if (DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS.has(lower))
|
|
225
|
+
continue;
|
|
226
|
+
if (DAG_PI_EXTENSION_DENIED_TOOL_IDS.has(lower))
|
|
227
|
+
continue;
|
|
228
|
+
if (DAG_PI_EXTENSION_WRITE_TOOL_DENY.has(lower))
|
|
229
|
+
continue;
|
|
230
|
+
extras.push(name);
|
|
231
|
+
}
|
|
232
|
+
return extras;
|
|
233
|
+
}
|
|
@@ -111,6 +111,13 @@ export async function readPlaywrightCliReceipts(runDir, nodeId) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
export function summarizePlaywrightCliReceipts(receipts) {
|
|
114
|
+
// Sequence numbers are assigned at dispatch time, which is the controller's
|
|
115
|
+
// causal order. Receipt lines are appended when each command COMPLETES, so
|
|
116
|
+
// concurrent commands (e.g. parallel find calls issued in one turn) persist
|
|
117
|
+
// out of sequence order. Judge chain contiguity on the dispatch order by
|
|
118
|
+
// sorting on `sequence` first; a genuinely missing receipt (gap) or a
|
|
119
|
+
// duplicated sequence still fails the contiguity check below.
|
|
120
|
+
const orderedReceipts = [...receipts].sort((a, b) => a.sequence - b.sequence);
|
|
114
121
|
let hasSuccessfulOpen = false;
|
|
115
122
|
let hasInteractionOrAssertion = false;
|
|
116
123
|
let hasMeaningfulAssertion = false;
|
|
@@ -122,7 +129,7 @@ export function summarizePlaywrightCliReceipts(receipts) {
|
|
|
122
129
|
let hasContiguousControllerSequence = true;
|
|
123
130
|
let previousSequence = 0;
|
|
124
131
|
const caseIds = new Set();
|
|
125
|
-
for (const receipt of
|
|
132
|
+
for (const receipt of orderedReceipts) {
|
|
126
133
|
caseIds.add(receipt.caseId);
|
|
127
134
|
const succeeded = receipt.exitCode === 0 && !receipt.timedOut;
|
|
128
135
|
const ordered = receipt.sequence === previousSequence + 1;
|