@tea-agent/loop-agent 0.39.0-next.26 → 0.39.0-next.28
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 +3 -0
- package/CHANGELOG.md +148 -96
- package/README.md +8 -4
- package/bin/loop-agent.js +7 -3
- 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/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/executors/dag-pi-executor.js +76 -10
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-sdk-executor.js +79 -2
- package/dist/executors/shell-executor.js +50 -6
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/operator/capabilities.js +7 -0
- 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/worker/cli.js +31 -1
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/routes.js +3 -0
- package/dist/worker/console/security.js +53 -5
- package/dist/worker/console/server.js +10 -4
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-DGn9iNrd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-Dc0C2Nyv.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-COnz_Bhe.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-VhIP6vgf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-D74lpmlb.js} +1 -1
- package/dist/worker/console/static/assets/channel-DWcZjWB3.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BxmiogQE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-C00Oi-2I.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DTrT1Vze.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DdKW7-fL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-D0q1WN6w.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DBa14MSI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-DkP-pEox.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-lgPogpcX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-D-apZer0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-B1T2yAIe.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-IjoI3dqU.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-IjoI3dqU.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-0P6hi3jF.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CntzMSR8.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-C9zmXJtG.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-DfBCZqZw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DLp-bhGL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BksDcwZm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-Bdag7DYP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CoY5uNbk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-KcgrDXHt.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CikErwJb.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DqmwwzvZ.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-35krh9WG.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-CvTF1gJ4.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BiOWWClJ.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-CqWlWE4l.js} +3 -3
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Crha5dcA.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DEtbscOZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-D2YYzx3z.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-C7x18ttQ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-CCLZkz45.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-Dmy4lSGv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CmvIol59.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-DVpEdirN.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-cKQy456v.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-bx06I38m.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-BjfIVIJr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-DcuPGbKM.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-DvgVeDoV.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-DLBG9nm-.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-D33L7FBF.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-hvqD9TpS.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C6MII0D1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-BtjH81Mz.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D_tk0V8D.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-CJ47Kraf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-COVPl-wh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-CCoQJLUY.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-DTWJvfAd.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/observe/node-input.js +72 -3
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/views/dag-inspector.js +54 -1
- package/dist/worker/observe/static/views/session-timeline.js +60 -4
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/failure-category.js +5 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- package/dist/workflows/dag/init-hybrid.js +71 -80
- package/dist/workflows/dag/node-execution.js +139 -10
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/retry-policy.js +91 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +28 -1
- package/dist/workflows/dag/validate.js +39 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +13 -0
- package/docs/templates/init-managed-agents.md +13 -3
- package/package.json +5 -1
- 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/loop-agent/references/command-reference.md +11 -4
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.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
|
+
}
|
|
@@ -2,13 +2,14 @@ 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";
|
|
13
14
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
14
15
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
@@ -327,6 +328,36 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
|
|
|
327
328
|
await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
|
|
328
329
|
return { promptPath, summaryPath };
|
|
329
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
|
+
}
|
|
330
361
|
async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
|
|
331
362
|
try {
|
|
332
363
|
const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
|
|
@@ -550,6 +581,29 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
550
581
|
};
|
|
551
582
|
}
|
|
552
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;
|
|
553
607
|
result = await piStepFn({
|
|
554
608
|
attachedFiles: [],
|
|
555
609
|
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
@@ -564,6 +618,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
564
618
|
stallTimeoutMs: input.stallTimeoutMs,
|
|
565
619
|
abortGraceMs: input.abortGraceMs,
|
|
566
620
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
621
|
+
...(piExtensionPaths && piExtensionPaths.length > 0
|
|
622
|
+
? { piExtensionPaths }
|
|
623
|
+
: {}),
|
|
567
624
|
onActivity: bridgeActivity
|
|
568
625
|
? (activity) => {
|
|
569
626
|
if (activity.kind === "lease" ||
|
|
@@ -885,22 +942,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
885
942
|
if (meta.writeGuardAttribution === "best-effort") {
|
|
886
943
|
stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
|
|
887
944
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const writerCleanTimeout = !mapped.ok &&
|
|
892
|
-
mapped.failureCategory === "timeout" &&
|
|
893
|
-
writeGuardOk &&
|
|
945
|
+
const rawFailureCategory = mapped.failureCategory;
|
|
946
|
+
const writeToolCallCount = readWriterThinkingExhaustionEvidence(result).writeToolCallCount;
|
|
947
|
+
const zeroSideEffects = writeGuardOk &&
|
|
894
948
|
!completenessFailure &&
|
|
895
949
|
!writerThinkingExhausted &&
|
|
896
|
-
|
|
950
|
+
!writerOutcomeViolation &&
|
|
897
951
|
changeManifestChangedFiles !== undefined &&
|
|
898
952
|
changeManifestChangedFiles.length === 0 &&
|
|
899
|
-
|
|
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;
|
|
900
965
|
return {
|
|
901
966
|
...mapped,
|
|
902
967
|
ok: false,
|
|
903
968
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
969
|
+
...(rawFailureCategory ? { rawFailureCategory } : {}),
|
|
904
970
|
failureCategory: mapped.ok
|
|
905
971
|
? writeGuardOk
|
|
906
972
|
? completenessFailure
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js";
|
|
3
4
|
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
|
|
4
5
|
import { serializeSessionEvent } from "./pi-event-serializer.js";
|
|
5
6
|
import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
|
|
@@ -229,6 +230,9 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
229
230
|
noContextFiles: true,
|
|
230
231
|
noSkills: true,
|
|
231
232
|
noExtensions: true,
|
|
233
|
+
...(input.additionalExtensionPaths && input.additionalExtensionPaths.length > 0
|
|
234
|
+
? { additionalExtensionPaths: input.additionalExtensionPaths }
|
|
235
|
+
: {}),
|
|
232
236
|
...(settingsManager ? { settingsManager } : {}),
|
|
233
237
|
appendSystemPromptOverride: (base) => [
|
|
234
238
|
...base,
|
|
@@ -236,6 +240,19 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
236
240
|
],
|
|
237
241
|
});
|
|
238
242
|
await loader.reload();
|
|
243
|
+
// Discover extension-registered tool names BEFORE session creation (plan
|
|
244
|
+
// 2026-08-21 D5): verified against the real SDK — passing extension tool
|
|
245
|
+
// names in createAgentSession `tools` activates them, while a post-create
|
|
246
|
+
// setActiveToolsByName pin leaves the active set unchanged. Merge filtered
|
|
247
|
+
// extras into the create-time allowlist; protected builtins, the deny
|
|
248
|
+
// surface, and write-capable extension tools never join.
|
|
249
|
+
const extensionToolExtras = [];
|
|
250
|
+
if (input.additionalExtensionPaths &&
|
|
251
|
+
input.additionalExtensionPaths.length > 0) {
|
|
252
|
+
const registered = collectLoaderExtensionToolNames(loader);
|
|
253
|
+
extensionToolExtras.push(...computeExtensionToolAllowlistExtras(registered.map((name) => ({ name })), input.toolNames));
|
|
254
|
+
}
|
|
255
|
+
const sessionToolNames = [...input.toolNames, ...extensionToolExtras];
|
|
239
256
|
if (input.requireWriterCustomTools) {
|
|
240
257
|
if (!Array.isArray(input.customTools) || input.customTools.length === 0) {
|
|
241
258
|
throw new Error("pi writer tool policy missing customTools; refusing to create uncontrolled writer session");
|
|
@@ -245,7 +262,7 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
245
262
|
cwd: input.cwd,
|
|
246
263
|
sessionManager: SessionManager.inMemory(input.cwd),
|
|
247
264
|
resourceLoader: loader,
|
|
248
|
-
tools:
|
|
265
|
+
tools: sessionToolNames,
|
|
249
266
|
modelRuntime,
|
|
250
267
|
...(settingsManager ? { settingsManager } : {}),
|
|
251
268
|
...(model ? { model } : {}),
|
|
@@ -254,6 +271,22 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
254
271
|
? { customTools: input.customTools }
|
|
255
272
|
: {}),
|
|
256
273
|
});
|
|
274
|
+
if (extensionToolExtras.length > 0) {
|
|
275
|
+
// Fail closed when merged extension tools did not activate: a silent
|
|
276
|
+
// pin failure must never let the node run without its declared tools.
|
|
277
|
+
const activeTools = created.session.agent?.state?.tools;
|
|
278
|
+
if (!Array.isArray(activeTools)) {
|
|
279
|
+
throw new Error("pi SDK session does not expose agent.state.tools; refusing unverifiable extension-tool activation");
|
|
280
|
+
}
|
|
281
|
+
const activeNames = new Set(activeTools
|
|
282
|
+
.map((tool) => typeof tool?.name === "string" ? tool.name : undefined)
|
|
283
|
+
.filter((name) => Boolean(name)));
|
|
284
|
+
for (const extensionToolName of extensionToolExtras) {
|
|
285
|
+
if (!activeNames.has(extensionToolName)) {
|
|
286
|
+
throw new Error(`pi SDK extension-tool activation mismatch for ${extensionToolName}: tool did not activate at session creation`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
257
290
|
const customToolNames = (input.customTools ?? [])
|
|
258
291
|
.map((tool) => isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined)
|
|
259
292
|
.filter((name) => Boolean(name));
|
|
@@ -266,7 +299,7 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
266
299
|
.map((tool) => typeof tool?.name === "string" ? tool.name : undefined)
|
|
267
300
|
.filter((name) => Boolean(name)));
|
|
268
301
|
for (const customToolName of customToolNames) {
|
|
269
|
-
const expectedActive =
|
|
302
|
+
const expectedActive = sessionToolNames.includes(customToolName);
|
|
270
303
|
if (activeNames.has(customToolName) !== expectedActive) {
|
|
271
304
|
throw new Error(`pi SDK custom-tool activation mismatch for ${customToolName}: expectedActive=${expectedActive}`);
|
|
272
305
|
}
|
|
@@ -274,6 +307,47 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
274
307
|
}
|
|
275
308
|
return created.session;
|
|
276
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Collect tool names registered by loader-resolved extensions. The SDK exposes
|
|
312
|
+
* each extension's `tools` as a Record (name → definition) or Map; both shapes
|
|
313
|
+
* are handled. Missing getExtensions (older SDK) yields no extras — the node
|
|
314
|
+
* then runs with the baseline allowlist only.
|
|
315
|
+
*/
|
|
316
|
+
function collectLoaderExtensionToolNames(loader) {
|
|
317
|
+
const getExtensions = loader?.getExtensions;
|
|
318
|
+
if (typeof getExtensions !== "function")
|
|
319
|
+
return [];
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = getExtensions.call(loader);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
if (!parsed || typeof parsed !== "object")
|
|
328
|
+
return [];
|
|
329
|
+
const extensions = parsed.extensions;
|
|
330
|
+
if (!Array.isArray(extensions))
|
|
331
|
+
return [];
|
|
332
|
+
const names = [];
|
|
333
|
+
for (const extension of extensions) {
|
|
334
|
+
if (!extension || typeof extension !== "object")
|
|
335
|
+
continue;
|
|
336
|
+
const tools = extension.tools;
|
|
337
|
+
if (tools instanceof Map) {
|
|
338
|
+
for (const key of tools.keys()) {
|
|
339
|
+
if (typeof key === "string")
|
|
340
|
+
names.push(key);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (tools && typeof tools === "object") {
|
|
344
|
+
for (const key of Object.keys(tools)) {
|
|
345
|
+
names.push(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
277
351
|
/**
|
|
278
352
|
* Throttle high-noise Pi SDK events from session-events.jsonl.
|
|
279
353
|
* Always keeps tool lifecycle, turn/agent end, assistant messages, and errors.
|
|
@@ -545,6 +619,9 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
545
619
|
? { customTools: writerCustomTools }
|
|
546
620
|
: {}),
|
|
547
621
|
...(requireWriterCustomTools ? { requireWriterCustomTools: true } : {}),
|
|
622
|
+
...(options.piExtensionPaths && options.piExtensionPaths.length > 0
|
|
623
|
+
? { additionalExtensionPaths: options.piExtensionPaths }
|
|
624
|
+
: {}),
|
|
548
625
|
});
|
|
549
626
|
let resolveStall;
|
|
550
627
|
let resolveSettlement;
|
|
@@ -123,6 +123,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
|
|
|
123
123
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
124
124
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
125
125
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
126
|
+
const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
|
|
127
|
+
const TEST_TIMEOUT_MARKERS = [
|
|
128
|
+
/Test timed out in \d+ms/i,
|
|
129
|
+
/Timeout of \d+ms exceeded/i,
|
|
130
|
+
];
|
|
131
|
+
const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
|
|
132
|
+
/AssertionError/i,
|
|
133
|
+
/expected:?\s/i,
|
|
134
|
+
/received:?\s/i,
|
|
135
|
+
/Snapshot /i,
|
|
136
|
+
/toMatchSnapshot/i,
|
|
137
|
+
/SyntaxError/i,
|
|
138
|
+
/TS\d{4}/,
|
|
139
|
+
/error TS/i,
|
|
140
|
+
/eslint/i,
|
|
141
|
+
/compile error/i,
|
|
142
|
+
/Failed to compile/i,
|
|
143
|
+
];
|
|
144
|
+
function isIdentifiedTestCommand(command) {
|
|
145
|
+
return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
|
|
146
|
+
}
|
|
147
|
+
function classifyShellCommandFailure(input) {
|
|
148
|
+
if (input.processCategory !== "nonzero-exit")
|
|
149
|
+
return input.processCategory;
|
|
150
|
+
if (!isIdentifiedTestCommand(input.command))
|
|
151
|
+
return "nonzero-exit";
|
|
152
|
+
const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
|
|
153
|
+
const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
|
|
154
|
+
if (!hasTimeoutMarker)
|
|
155
|
+
return "nonzero-exit";
|
|
156
|
+
const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
|
|
157
|
+
return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
|
|
158
|
+
}
|
|
126
159
|
const NODE_STDOUT_MAX = 12_000;
|
|
127
160
|
const NODE_STDERR_MAX = 8_000;
|
|
128
161
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
@@ -329,16 +362,21 @@ export async function executeShellCommand(input) {
|
|
|
329
362
|
});
|
|
330
363
|
});
|
|
331
364
|
child.on("close", (code) => {
|
|
332
|
-
let
|
|
365
|
+
let processCategory = "nonzero-exit";
|
|
333
366
|
if (timedOut) {
|
|
334
|
-
|
|
367
|
+
processCategory = "timeout";
|
|
335
368
|
}
|
|
336
369
|
else if (code === 0) {
|
|
337
|
-
|
|
370
|
+
processCategory = "success";
|
|
338
371
|
}
|
|
339
372
|
finish({
|
|
340
373
|
exitCode: code,
|
|
341
|
-
failureCategory
|
|
374
|
+
failureCategory: classifyShellCommandFailure({
|
|
375
|
+
command: input.command,
|
|
376
|
+
processCategory,
|
|
377
|
+
stdout: formatBoundedOutput(stdout),
|
|
378
|
+
stderr: formatBoundedOutput(stderr),
|
|
379
|
+
}),
|
|
342
380
|
ok: code === 0 && !timedOut,
|
|
343
381
|
});
|
|
344
382
|
});
|
|
@@ -2878,6 +2916,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2878
2916
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
2879
2917
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2880
2918
|
const failFast = shell.failFast !== false;
|
|
2919
|
+
const attemptNumber = input.attempt ?? 1;
|
|
2881
2920
|
const results = [];
|
|
2882
2921
|
const executedCommandKeys = new Set();
|
|
2883
2922
|
let beforeStatus;
|
|
@@ -2973,8 +3012,8 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2973
3012
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2974
3013
|
reportActivity: input.reportActivity,
|
|
2975
3014
|
outputArtifacts: {
|
|
2976
|
-
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2977
|
-
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
3015
|
+
stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
|
|
3016
|
+
stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
|
|
2978
3017
|
},
|
|
2979
3018
|
});
|
|
2980
3019
|
results.push(result);
|
|
@@ -3033,6 +3072,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
3033
3072
|
schemaVersion: 1,
|
|
3034
3073
|
runId: meta.runId,
|
|
3035
3074
|
nodeId: input.task.id,
|
|
3075
|
+
attempt: attemptNumber,
|
|
3036
3076
|
workspaceFingerprint: workspaceFingerprint ?? null,
|
|
3037
3077
|
failFast,
|
|
3038
3078
|
commands: results.map((result) => ({
|
|
@@ -3051,6 +3091,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
3051
3091
|
subchecks: parseCheckRepoSubcheckDurations(result),
|
|
3052
3092
|
})),
|
|
3053
3093
|
};
|
|
3094
|
+
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
|
|
3054
3095
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
|
|
3055
3096
|
const summaryMarkdown = buildShellResultSummaryMarkdown({
|
|
3056
3097
|
nodeId: input.task.id,
|
|
@@ -3102,6 +3143,9 @@ export async function executeDagShellNode(input, meta) {
|
|
|
3102
3143
|
stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
|
|
3103
3144
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
3104
3145
|
failureCategory,
|
|
3146
|
+
...(firstFailure?.failureCategory
|
|
3147
|
+
? { rawFailureCategory: firstFailure.failureCategory }
|
|
3148
|
+
: {}),
|
|
3105
3149
|
durationMs: Date.now() - started,
|
|
3106
3150
|
// extra field consumed by node-execution if supported; also embedded in stdout marker
|
|
3107
3151
|
...{ commandResults },
|
|
@@ -98,6 +98,13 @@ export function isEphemeralToolCachePath(filePath) {
|
|
|
98
98
|
const normalized = normalizePath(filePath);
|
|
99
99
|
if (!normalized)
|
|
100
100
|
return false;
|
|
101
|
+
// CodeGraph extension sync side effect (plan 2026-08-21 D7): a loaded
|
|
102
|
+
// pi-codegraph extension keeps an existing .codegraph/ index fresh at
|
|
103
|
+
// session start; those index files are extension-owned run state, not
|
|
104
|
+
// writer output, so they never count as write-guard violations.
|
|
105
|
+
if (normalized === ".codegraph" || normalized.startsWith(".codegraph/")) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
101
108
|
if (normalized === ".pytest_cache" ||
|
|
102
109
|
normalized.startsWith(".pytest_cache/")) {
|
|
103
110
|
return true;
|
|
@@ -1822,6 +1822,13 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
1822
1822
|
source: "loop-agent",
|
|
1823
1823
|
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
1824
1824
|
},
|
|
1825
|
+
{
|
|
1826
|
+
command: "loop-agent init model-catalog",
|
|
1827
|
+
coverage: "excluded",
|
|
1828
|
+
action: null,
|
|
1829
|
+
source: "loop-agent",
|
|
1830
|
+
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
1831
|
+
},
|
|
1825
1832
|
{
|
|
1826
1833
|
command: "loop-agent init doctor",
|
|
1827
1834
|
coverage: "excluded",
|