pi-crew 0.9.68 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge of settled dispatch units into the run state for the team-run
|
|
3
|
+
* scheduler loop.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
|
|
6
|
+
* CORE-4 extraction 5). Pure code motion: isRunTerminalPreserved +
|
|
7
|
+
* mergeUnitResult moved verbatim, including the RT-12 / NEW-D1 / R15-2 /
|
|
8
|
+
* CANCEL-1 comments.
|
|
9
|
+
*
|
|
10
|
+
* `isRunTerminalPreserved` lives here (not in finalize-run.ts) because it is
|
|
11
|
+
* shared by mergeUnitResult, finalizeRun (finalize-run.ts imports it from
|
|
12
|
+
* here) and the core loop in team-runner.ts (which imports it from here) —
|
|
13
|
+
* keeping it here avoids a merge-loop ↔ finalize-run import cycle.
|
|
14
|
+
*/
|
|
15
|
+
import { flushPendingAtomicWrites } from "../state/atomic-write.ts";
|
|
16
|
+
import { withRunLock } from "../state/coordination/locks.ts";
|
|
17
|
+
import { loadRunManifestById, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/stores/state-store.ts";
|
|
18
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
19
|
+
import { classifyFatalFsError } from "../utils/fs-errno.ts";
|
|
20
|
+
import { cancelNonTerminalTasks } from "./dispatch-batch.ts";
|
|
21
|
+
import { mergeTaskUpdatesPreservingTerminal } from "./merge-gate.ts";
|
|
22
|
+
import type { SchedulerContext, SchedulerDecision } from "./scheduler-context.ts";
|
|
23
|
+
import { mergeArtifacts } from "./team-runner-artifacts.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CORE-4 extraction 5: wait for one in-flight dispatch unit to settle and
|
|
27
|
+
* merge its result into the run state.
|
|
28
|
+
*
|
|
29
|
+
* Awaits Promise.race on ctx.pendingUnits; the first settled unit is merged
|
|
30
|
+
* into ctx.manifest/tasks under the run lock (flushPendingAtomicWrites +
|
|
31
|
+
* loadRunManifestById + mergeTaskUpdatesPreservingTerminal + save). The settled
|
|
32
|
+
* unit is then deleted from ctx.pendingUnits, and the merge outcome (taskIds
|
|
33
|
+
* + result object) is recorded on ctx.settledMerge for the post-merge inline
|
|
34
|
+
* logic (cancel-during-exec check + batch summary).
|
|
35
|
+
*
|
|
36
|
+
* Returns null to continue to the phase/budget check. A `{ kind: "return" }`
|
|
37
|
+
* decision is reserved for future run-complete/failure detection during merge.
|
|
38
|
+
*
|
|
39
|
+
* Reads ctx.pendingUnits/manifest/tasks. Mutates ctx.pendingUnits (delete),
|
|
40
|
+
* ctx.manifest/tasks, ctx.settledMerge.
|
|
41
|
+
*
|
|
42
|
+
* @param ctx The scheduler context.
|
|
43
|
+
*/
|
|
44
|
+
/** R15-2/R15-1: run statuses that must never be overwritten by an in-memory
|
|
45
|
+
* derived status (mergeUnitResult force-"running", finalizeRun "completed").
|
|
46
|
+
* "blocked" is deliberately EXCLUDED — a blocked run can be unblocked and the
|
|
47
|
+
* finalize chain may legitimately derive blocked from in-memory state. */
|
|
48
|
+
export function isRunTerminalPreserved(status: TeamRunManifest["status"]): boolean {
|
|
49
|
+
return status === "cancelled" || status === "failed" || status === "completed";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function mergeUnitResult(ctx: SchedulerContext): Promise<SchedulerDecision | null> {
|
|
53
|
+
// RT-12: race on pre-created wrapper promises (created once at dispatch
|
|
54
|
+
// time) instead of rebuilding a wrapper-promise array with new async
|
|
55
|
+
// closures every iteration. This reduces allocation from O(C×T) wrapper
|
|
56
|
+
// promises to O(C) total (one per unit, created once at dispatch).
|
|
57
|
+
const settled = await Promise.race([...ctx.pendingUnits.values()].map((u) => u.wrapped));
|
|
58
|
+
const completedUnit = ctx.pendingUnits.get(settled.unitKey)!;
|
|
59
|
+
ctx.pendingUnits.delete(settled.unitKey);
|
|
60
|
+
|
|
61
|
+
// Build the single result to merge. On rejection, synthesize a failed
|
|
62
|
+
// result so the run continues (mirrors the old validResults guard).
|
|
63
|
+
// bug-026 sub-issue B: a rejected unit means runTeamTask itself threw —
|
|
64
|
+
// typically an atomicWriteFile/persistSingleTaskUpdate ENOSPC mid-run.
|
|
65
|
+
// Classify the errno and stamp failureCause on the synthesized failed
|
|
66
|
+
// tasks so the operator sees "failed (disk full)", not a generic write
|
|
67
|
+
// error string.
|
|
68
|
+
const thrownFailureCause = settled.result ? undefined : classifyFatalFsError(settled.error);
|
|
69
|
+
const resultToMerge: { manifest: TeamRunManifest; tasks: TeamTaskState[] } = settled.result ?? {
|
|
70
|
+
manifest: ctx.manifest,
|
|
71
|
+
tasks: cancelNonTerminalTasks(ctx.tasks, "failed", settled.error!.message, (t) => completedUnit.taskIds.includes(t.id)).map((t) =>
|
|
72
|
+
thrownFailureCause && t.status === "failed" ? { ...t, failureCause: thrownFailureCause } : t,
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
const validResults = [resultToMerge];
|
|
76
|
+
// Reconstruct manifest from the last worker's snapshot. The .artifacts field
|
|
77
|
+
// is re-merged from both the team-runner's in-memory state and all workers'
|
|
78
|
+
// snapshots, so artifact writes by task-runner (which individually save manifest
|
|
79
|
+
// after writing artifacts) are safely persisted. The in-memory manifest is only
|
|
80
|
+
// used for the next batch iteration's orchestration — actual persistence is safe.
|
|
81
|
+
// Use updateRunStatus to recompute manifest status from merged tasks rather than
|
|
82
|
+
// relying on the last result's manifest (which is arbitrary due to mapConcurrent
|
|
83
|
+
// returning results in arbitrary order).
|
|
84
|
+
// Use the in-memory manifest as base (not the last-completing worker's snapshot).
|
|
85
|
+
// Recompute status from merged tasks so the manifest reflects actual task state,
|
|
86
|
+
// not the arbitrary order in which mapConcurrent returned results.
|
|
87
|
+
// Read committed manifest from disk inside the lock so artifact merge is based
|
|
88
|
+
// on committed state, not in-memory state that may differ from disk.
|
|
89
|
+
const mergeResult = await withRunLock(ctx.manifest, async () => {
|
|
90
|
+
// NEW-D1: flush any pending coalesced atomic writes before reading from
|
|
91
|
+
// disk. Without this, a worker's async manifest save (coalesced by
|
|
92
|
+
// atomic-write) may not be committed yet, causing a lost-update on the
|
|
93
|
+
// merge read. flushPendingAtomicWrites forces all queued writes to disk.
|
|
94
|
+
flushPendingAtomicWrites();
|
|
95
|
+
const disk = loadRunManifestById(ctx.manifest.cwd, ctx.manifest.runId);
|
|
96
|
+
const diskManifest = disk?.manifest ?? ctx.manifest;
|
|
97
|
+
const diskArtifacts = diskManifest.artifacts;
|
|
98
|
+
const reconciledArtifacts = mergeArtifacts([...diskArtifacts, ...validResults.map((item) => item.manifest.artifacts)].flat());
|
|
99
|
+
// R15-2: only force "running" when the disk manifest is NON-terminal
|
|
100
|
+
// (queued/planning/running/blocked). If the disk status is already terminal
|
|
101
|
+
// (cancelled/failed/completed — an external cancel or reconciler write
|
|
102
|
+
// landing during the batch), PRESERVE that terminal status: forcing
|
|
103
|
+
// "running" would legally erase it (contracts.ts allows cancelled/failed/
|
|
104
|
+
// completed → running) and the loop would never observe the disk-terminal
|
|
105
|
+
// (CANCEL-1/CANCEL-2 only catch worker-reported cancel or signal abort).
|
|
106
|
+
const mergedBase = { ...diskManifest, artifacts: reconciledArtifacts };
|
|
107
|
+
const resultManifest = isRunTerminalPreserved(diskManifest.status)
|
|
108
|
+
? mergedBase
|
|
109
|
+
: updateRunStatus(mergedBase, "running", "Merged task updates from parallel batch.");
|
|
110
|
+
// CANCEL-1: use the freshly-loaded disk tasks as the merge base instead
|
|
111
|
+
// of the in-memory `tasks` closure variable. The in-memory tasks reflect
|
|
112
|
+
// only team-runner's view; an external cancel (handleCancel, background
|
|
113
|
+
// race with SIGTERM arriving after cancel wrote but before merge ran)
|
|
114
|
+
// writes 'cancelled' to disk.tasks — using disk.tasks as base preserves
|
|
115
|
+
// that cancellation through the merge instead of overwriting it with the
|
|
116
|
+
// stale in-memory view. disk was loaded inside this lock, so it reflects
|
|
117
|
+
// the freshest committed state.
|
|
118
|
+
const resultTasks = mergeTaskUpdatesPreservingTerminal(disk?.tasks ?? ctx.tasks, validResults);
|
|
119
|
+
await saveRunManifestAsync(resultManifest);
|
|
120
|
+
await saveRunTasksAsync(resultManifest, resultTasks);
|
|
121
|
+
return { resultManifest, resultTasks };
|
|
122
|
+
});
|
|
123
|
+
ctx.manifest = mergeResult.resultManifest;
|
|
124
|
+
ctx.tasks = mergeResult.resultTasks;
|
|
125
|
+
ctx.settledMerge = { taskIds: completedUnit.taskIds, result: resultToMerge };
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @internal 1.9(b) test export — exercise mergeUnitResult directly. */
|
|
130
|
+
export const __test__mergeUnitResult = mergeUnitResult;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-budget-summary.ts — R8 model-routing transparency (WP-8, T5).
|
|
3
|
+
*
|
|
4
|
+
* One pre-run summary line: the resolved fallback chain + the WORST-CASE
|
|
5
|
+
* spawn budget per task (`attemptModels × (maxAttempts + 1)` — the RT-6
|
|
6
|
+
* formula; always ≥ 1 full attempt above the theoretical max). Cost
|
|
7
|
+
* surprises in multi-model fallback runs were invisible until workers
|
|
8
|
+
* started burning through attempts; this makes the ceiling loud UP FRONT.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { loadConfig } from "../../config/config.ts";
|
|
12
|
+
import { DEFAULT_RETRY_POLICY } from "../recovery/retry-executor.ts";
|
|
13
|
+
import { computeSpawnBudgetMax } from "../task-runner/child-executor.ts";
|
|
14
|
+
import { buildConfiguredModelRouting } from "./model-fallback.ts";
|
|
15
|
+
|
|
16
|
+
export interface ModelBudgetSummary {
|
|
17
|
+
/** Deduped resolved chain (requested first, then fallbacks). */
|
|
18
|
+
chain: string[];
|
|
19
|
+
/** Worst-case child spawns per task: chain.length × (maxAttempts + 1). */
|
|
20
|
+
worstCaseSpawnsPerTask: number;
|
|
21
|
+
/** Configured maxAttempts (or the default when unset/unreadable). */
|
|
22
|
+
maxAttempts: number;
|
|
23
|
+
/** Rendered single-line summary for console output. */
|
|
24
|
+
line: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Compute the pre-run model budget summary. Never throws — degrades to an
|
|
28
|
+
* empty chain + default attempts on any config/catalog read failure. */
|
|
29
|
+
export function summarizeModelBudget(cwd: string): ModelBudgetSummary {
|
|
30
|
+
let maxAttempts = DEFAULT_RETRY_POLICY.maxAttempts;
|
|
31
|
+
let chain: string[] = [];
|
|
32
|
+
try {
|
|
33
|
+
const { config } = loadConfig(cwd);
|
|
34
|
+
// The same routing pipeline tasks use (config catalog + auto tail policy):
|
|
35
|
+
// the summary must describe what would ACTUALLY spawn, not a parallel
|
|
36
|
+
// reconstruction of it.
|
|
37
|
+
const routing = buildConfiguredModelRouting({ cwd, policy: config.runtime?.modelFallback });
|
|
38
|
+
chain = routing.candidates;
|
|
39
|
+
if (config.reliability?.retryPolicy?.maxAttempts !== undefined) {
|
|
40
|
+
maxAttempts = config.reliability.retryPolicy.maxAttempts;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* degrade: defaults below */
|
|
44
|
+
}
|
|
45
|
+
const worst = computeSpawnBudgetMax(Math.max(1, chain.length), maxAttempts);
|
|
46
|
+
const chainText = chain.length ? chain.join(" → ") : "(default pi model)";
|
|
47
|
+
return {
|
|
48
|
+
chain,
|
|
49
|
+
worstCaseSpawnsPerTask: worst,
|
|
50
|
+
maxAttempts,
|
|
51
|
+
line: `[team-tool.run] model routing: ${chainText} · worst-case ${worst} spawns/task (chain=${Math.max(1, chain.length)} × maxAttempts+1=${maxAttempts + 1})`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -273,14 +273,47 @@ export function splitThinkingSuffix(model: string): {
|
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
// WP-8 (R8): loud passthrough warnings — deduped per model so a chain of
|
|
277
|
+
// tasks resolving the same unvalidated model warns once per process. The
|
|
278
|
+
// delegate surface (spawn-policy admission) validates models against the
|
|
279
|
+
// catalog itself (WP-5) and never routes through resolveModelCandidate.
|
|
280
|
+
const passthroughWarned = new Set<string>();
|
|
281
|
+
function warnUnvalidatedPassthrough(model: string, reason: string): void {
|
|
282
|
+
const key = `${model}|${reason}`;
|
|
283
|
+
if (passthroughWarned.has(key)) return;
|
|
284
|
+
passthroughWarned.add(key);
|
|
285
|
+
// Muted: per-model passthrough noise became too loud when many pi-crew
|
|
286
|
+
// child workers resolve their parent model + fallback chain against the
|
|
287
|
+
// full catalog (each worker printed one line). Dedup Set is preserved
|
|
288
|
+
// so re-enabling is a one-line revert.
|
|
289
|
+
// console.warn(
|
|
290
|
+
// `[model-routing] unvalidated passthrough: '${model}' (${reason}) — not confirmed against the configured model catalog; spawn may fail at the provider.`,
|
|
291
|
+
// );
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Test seam: the dedup set is module-global (one warn per model+reason per
|
|
295
|
+
* process); tests reset it for deterministic counting. */
|
|
296
|
+
export function resetPassthroughWarnings(): void {
|
|
297
|
+
passthroughWarned.clear();
|
|
298
|
+
}
|
|
299
|
+
|
|
276
300
|
export function resolveModelCandidate(
|
|
277
301
|
model: string | undefined,
|
|
278
302
|
availableModels: AvailableModelInfo[] | undefined,
|
|
279
303
|
preferredProvider?: string,
|
|
280
304
|
): string | undefined {
|
|
281
305
|
if (!model) return undefined;
|
|
282
|
-
|
|
283
|
-
|
|
306
|
+
// Provider-qualified refs ("provider/model") pass through UNVALIDATED —
|
|
307
|
+
// the caller asserted a full id; there is nothing to resolve. Loud (R8):
|
|
308
|
+
// this is the documented trust path, not a silent one.
|
|
309
|
+
if (model.includes("/")) {
|
|
310
|
+
warnUnvalidatedPassthrough(model, "provider-qualified ref, no catalog check");
|
|
311
|
+
return model;
|
|
312
|
+
}
|
|
313
|
+
if (!availableModels || availableModels.length === 0) {
|
|
314
|
+
warnUnvalidatedPassthrough(model, "no model catalog available");
|
|
315
|
+
return model;
|
|
316
|
+
}
|
|
284
317
|
|
|
285
318
|
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
286
319
|
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
@@ -294,6 +327,7 @@ export function resolveModelCandidate(
|
|
|
294
327
|
// Fuzzy fallback: try to resolve via partial name matching
|
|
295
328
|
const fuzzy = fuzzyResolveModelId(baseModel, availableModels);
|
|
296
329
|
if (fuzzy) return `${fuzzy}${thinkingSuffix}`;
|
|
330
|
+
warnUnvalidatedPassthrough(model, "no exact or fuzzy catalog match");
|
|
297
331
|
return model;
|
|
298
332
|
}
|
|
299
333
|
return `${matches[0]!.fullId}${thinkingSuffix}`;
|
|
@@ -328,6 +328,16 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
328
328
|
) {
|
|
329
329
|
allowed = allowed.filter((ext) => path.resolve(ext) === path.resolve(PROMPT_RUNTIME_EXTENSION_PATH));
|
|
330
330
|
}
|
|
331
|
+
// ADR-5 §8 (governed nesting, T3/WP-5 step 8): every spawn this builder
|
|
332
|
+
// produces runs at depth > 0 (workers are depth 1, delegate grandchildren
|
|
333
|
+
// are 2+; currentCrewDepth(base)+1 is ALWAYS >= 1) — the extension list
|
|
334
|
+
// is an unconditional ALLOWLIST: only the trusted
|
|
335
|
+
// PROMPT_RUNTIME_EXTENSION_PATH passes, REGARDLESS OF SOURCE. User-sourced
|
|
336
|
+
// agent declarations can no longer inject extensions into sub-agents
|
|
337
|
+
// (SEC-1's project-only strip left user/builtin declarations unfiltered —
|
|
338
|
+
// the audit's untested hole). The denylist + SEC-1 strip above remain as
|
|
339
|
+
// defense-in-depth; the allowlist is authoritative.
|
|
340
|
+
allowed = [];
|
|
331
341
|
for (const extension of [PROMPT_RUNTIME_EXTENSION_PATH, ...allowed]) args.push("--extension", extension);
|
|
332
342
|
} else {
|
|
333
343
|
args.push("--extension", PROMPT_RUNTIME_EXTENSION_PATH);
|
|
@@ -44,6 +44,12 @@ export interface DiscoveredProviderExtension {
|
|
|
44
44
|
*/
|
|
45
45
|
const cache = new Map<string, { mtimeMs: number; result: DiscoveredProviderExtension[] }>();
|
|
46
46
|
|
|
47
|
+
// R5-L3 (Round 5 LOW-3): FIFO cap at insertion (mirrors knowledgeCache in
|
|
48
|
+
// knowledge-injection.ts). Entries are only removed when a re-access sees a
|
|
49
|
+
// changed mtime, so distinct settings paths (e.g. per-test temp roots) would
|
|
50
|
+
// otherwise accumulate for the process lifetime.
|
|
51
|
+
const MAX_PROVIDER_EXTENSION_CACHE = 64;
|
|
52
|
+
|
|
47
53
|
function cachedResult(settingsPath: string): { mtimeMs: number; result: DiscoveredProviderExtension[] } | undefined {
|
|
48
54
|
const entry = cache.get(settingsPath);
|
|
49
55
|
if (!entry) return undefined;
|
|
@@ -157,6 +163,10 @@ export function discoverProviderExtensions(settingsPath?: string): DiscoveredPro
|
|
|
157
163
|
// Cache the resolved result keyed on the settings.json path + mtime.
|
|
158
164
|
try {
|
|
159
165
|
cache.set(settingsFile, { mtimeMs: fs.statSync(settingsFile).mtimeMs, result: out });
|
|
166
|
+
if (cache.size > MAX_PROVIDER_EXTENSION_CACHE) {
|
|
167
|
+
const oldest = cache.keys().next().value;
|
|
168
|
+
if (oldest !== undefined) cache.delete(oldest);
|
|
169
|
+
}
|
|
160
170
|
} catch {
|
|
161
171
|
cache.delete(settingsFile);
|
|
162
172
|
}
|
|
@@ -201,7 +201,7 @@ function readRegistry(): OrphanWorkerEntry[] {
|
|
|
201
201
|
}
|
|
202
202
|
// Silent failure is deliberate for robustness (registry read failures
|
|
203
203
|
// shouldn't crash the process), but log at warning level to aid troubleshooting.
|
|
204
|
-
|
|
204
|
+
logInternalError("orphan-worker-registry", new Error(`readRegistry failed: ${error}`), undefined, "warn");
|
|
205
205
|
return [];
|
|
206
206
|
}
|
|
207
207
|
}
|
|
@@ -115,6 +115,51 @@ export function validateWorkerOutput(role: string, output: string): OutputValida
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* bug-026 sub-issue A: strict log-noise line shapes observed in the corrupted
|
|
120
|
+
* `02_explore-core.txt` result artifact (run team_20260815144514) — extension/
|
|
121
|
+
* MCP session-log stderr that the child-executor result fallback chain leaked
|
|
122
|
+
* into the result artifact when the worker payload was corrupted/empty.
|
|
123
|
+
*
|
|
124
|
+
* A trimmed non-empty line counts as log noise ONLY if it matches one of:
|
|
125
|
+
* 1. a bracket-tag extension log line: `[oc-go] ...`, `[pi-qwen-mm] ...`,
|
|
126
|
+
* `[pi-qwen-mm] [core] [stderr] ...` — the leading tag is a lowercase
|
|
127
|
+
* identifier (deliberately excludes capitalized bracketed prose like
|
|
128
|
+
* `[Note] ...`), optionally followed by known sub-tags
|
|
129
|
+
* (core/mcp/stderr/stdout/warn/info/error/debug);
|
|
130
|
+
* 2. a Python `warnings.warn(` line (deprecation-warning continuation);
|
|
131
|
+
* 3. a timestamped logging line: `2026-08-15 21:49:02,986 WARNING ...`.
|
|
132
|
+
*
|
|
133
|
+
* Anything else — prose, markdown, file:line results, `OK done.` — is NOT
|
|
134
|
+
* noise, so a single such line marks the artifact as usable content.
|
|
135
|
+
*
|
|
136
|
+
* NOTE: a `true` return is NOT by itself failure evidence; the caller
|
|
137
|
+
* (post-execution.ts) applies the two-gate rule (authoritative output
|
|
138
|
+
* sources empty AND artifact log-noise-only) before failing a task.
|
|
139
|
+
*/
|
|
140
|
+
const BRACKET_TAG_LOG_LINE = /^\[[a-z0-9][a-z0-9_.-]*\](?:\s*\[(?:core|mcp|stderr|stdout|warn|info|error|debug)\])*(?:\s.*)?$/;
|
|
141
|
+
const PYTHON_WARNING_LINE = /(?:^|\s)warnings\.warn\(/;
|
|
142
|
+
const TIMESTAMPED_LOG_LINE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d{1,6})?(?:Z|[+-]\d{2}:?\d{2})?\s+[A-Za-z]+\b/;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Detect a "stderr-only" result artifact: every non-empty trimmed line is
|
|
146
|
+
* strict log noise (see the pattern docs above). Empty/whitespace-only input
|
|
147
|
+
* returns false — emptiness is the caller's separate, explicit check.
|
|
148
|
+
* Conservative by design: any prose/markdown line makes it return false.
|
|
149
|
+
*/
|
|
150
|
+
export function isStderrOnlyResult(text: string): boolean {
|
|
151
|
+
if (!text?.trim()) return false;
|
|
152
|
+
let noiseLines = 0;
|
|
153
|
+
for (const line of text.split("\n")) {
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
if (!trimmed) continue;
|
|
156
|
+
const isNoise = BRACKET_TAG_LOG_LINE.test(trimmed) || PYTHON_WARNING_LINE.test(trimmed) || TIMESTAMPED_LOG_LINE.test(trimmed);
|
|
157
|
+
if (!isNoise) return false;
|
|
158
|
+
noiseLines++;
|
|
159
|
+
}
|
|
160
|
+
return noiseLines > 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
118
163
|
/**
|
|
119
164
|
* Extract structured findings from reviewer output.
|
|
120
165
|
* Returns array of { file, line, severity, message } objects.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Parent liveness guard for pi-crew background-runner process.
|
|
3
5
|
*
|
|
@@ -70,7 +72,7 @@
|
|
|
70
72
|
* polling approach for near-instantaneous parent-death detection on Unix
|
|
71
73
|
* systems, avoiding the polling overhead entirely.
|
|
72
74
|
*/
|
|
73
|
-
const POLL_INTERVAL_MS = Number(
|
|
75
|
+
const POLL_INTERVAL_MS = Number(getCrewEnv("PI_CREW_PARENT_GUARD_INTERVAL_MS")) || 500;
|
|
74
76
|
|
|
75
77
|
const guardIntervals = new Map<number, ReturnType<typeof setInterval>>();
|
|
76
78
|
|
package/src/runtime/peer-dep.ts
CHANGED
|
@@ -38,6 +38,7 @@ import * as fs from "node:fs";
|
|
|
38
38
|
import * as os from "node:os";
|
|
39
39
|
import * as path from "node:path";
|
|
40
40
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
41
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
41
42
|
import { resolveNpmGlobalRoot } from "./pi-spawn.ts";
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -78,7 +79,7 @@ export function peerDepResolutionBases(): string[] {
|
|
|
78
79
|
const bases: string[] = [];
|
|
79
80
|
|
|
80
81
|
// 0. Parent-provided hint (fastest — no probe). Set by async-runner.
|
|
81
|
-
const envHint =
|
|
82
|
+
const envHint = getCrewEnv(PEER_DEP_DIR_ENV)?.trim();
|
|
82
83
|
if (envHint) bases.push(path.resolve(envHint));
|
|
83
84
|
|
|
84
85
|
// 1. This file's location — works when pi-crew and pi-coding-agent share a
|
|
@@ -87,11 +87,6 @@ function rememberSeen(path: string, content: string): void {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** Test seam: reset the dedup cache between tests. */
|
|
91
|
-
export function resetPerWriteValidatorCache(): void {
|
|
92
|
-
seenContent.clear();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
90
|
/**
|
|
96
91
|
* Replace the validator registry (test seam). Production uses
|
|
97
92
|
* DEFAULT_VALIDATORS; tests inject a custom map to exercise specific extensions.
|
package/src/runtime/pi-spawn.ts
CHANGED
|
@@ -3,6 +3,8 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
7
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
8
|
|
|
7
9
|
export interface PiSpawnCommand {
|
|
8
10
|
command: string;
|
|
@@ -30,8 +32,11 @@ function isWithinAllowedPrefixes(resolvedPath: string): boolean {
|
|
|
30
32
|
const execDir = path.dirname(fs.realpathSync.native(process.execPath));
|
|
31
33
|
allowedPrefixes.push(execDir.toLowerCase());
|
|
32
34
|
allowedPrefixes.push(path.join(path.dirname(execDir), "lib", "node_modules").toLowerCase());
|
|
33
|
-
} catch {
|
|
34
|
-
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// R17-B3 (LOW): execPath realpath failure is genuinely unexpected (the
|
|
37
|
+
// running binary vanished / permission issue) — surface it so the
|
|
38
|
+
// "cannot find pi" fallback is diagnosable.
|
|
39
|
+
logInternalError("pi-spawn.allowlist-prefixes", error, "execPath realpath failed", "warn");
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
// npm global bin via APPDATA
|
|
@@ -49,24 +54,46 @@ function isWithinAllowedPrefixes(resolvedPath: string): boolean {
|
|
|
49
54
|
try {
|
|
50
55
|
const projectBin = path.resolve("node_modules", ".bin");
|
|
51
56
|
allowedPrefixes.push(projectBin.toLowerCase());
|
|
52
|
-
} catch {
|
|
53
|
-
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// R17-B3: probe failure → prefix simply not allowed; keep the cause
|
|
59
|
+
// visible under PI_TEAMS_DEBUG.
|
|
60
|
+
logInternalError("pi-spawn.allowlist-prefixes.project-bin", error, undefined, "debug");
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// User home npm-global
|
|
57
64
|
try {
|
|
58
65
|
const homeNpm = path.join(os.homedir(), ".npm-global", "bin");
|
|
59
66
|
allowedPrefixes.push(homeNpm.toLowerCase());
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// R17-B3: probe failure → prefix simply not allowed; keep the cause
|
|
69
|
+
// visible under PI_TEAMS_DEBUG.
|
|
70
|
+
logInternalError("pi-spawn.allowlist-prefixes.npm-global", error, undefined, "debug");
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
// User home .local/bin
|
|
65
74
|
try {
|
|
66
75
|
const homeLocal = path.join(os.homedir(), ".local", "bin");
|
|
67
76
|
allowedPrefixes.push(homeLocal.toLowerCase());
|
|
68
|
-
} catch {
|
|
69
|
-
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// R17-B3: probe failure → prefix simply not allowed; keep the cause
|
|
79
|
+
// visible under PI_TEAMS_DEBUG.
|
|
80
|
+
logInternalError("pi-spawn.allowlist-prefixes.local-bin", error, undefined, "debug");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Canonicalize prefixes (macOS: /var/folders/... realpath → /private/var/folders/...).
|
|
84
|
+
// validateExplicitBin() compares fs.realpathSync(resolved) against this list —
|
|
85
|
+
// without the canonical forms, any prefix reached through a symlink (macOS
|
|
86
|
+
// tmpdir, ~/.npm-global as symlink) rejects its own realpath'd target
|
|
87
|
+
// (CI incident: run-worker-cap.test.ts on macos-latest). Additive: original
|
|
88
|
+
// forms stay, so Windows \\?\-prefixed realpaths simply never match.
|
|
89
|
+
for (let i = 0; i < allowedPrefixes.length; i++) {
|
|
90
|
+
try {
|
|
91
|
+
const real = fs.realpathSync.native(allowedPrefixes[i]!).toLowerCase();
|
|
92
|
+
if (real !== allowedPrefixes[i] && !allowedPrefixes.includes(real)) allowedPrefixes.push(real);
|
|
93
|
+
} catch {
|
|
94
|
+
// Prefix path doesn't exist (env var points nowhere) — the original form
|
|
95
|
+
// stays; nothing to canonicalize.
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
return allowedPrefixes.some((prefix) => normalized.startsWith(prefix));
|
|
@@ -81,12 +108,20 @@ function resolvePiPackageRoot(): string | undefined {
|
|
|
81
108
|
try {
|
|
82
109
|
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")) as { name?: string };
|
|
83
110
|
if (pkg.name && PI_PACKAGE_NAMES.includes(pkg.name)) return dir;
|
|
84
|
-
} catch {
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// R17-B3: the upward walk EXPECTS ENOENT probes (most dirs have no
|
|
113
|
+
// readable package.json) — debug-gated so the walk stays silent
|
|
114
|
+
// unless PI_TEAMS_DEBUG is set, but EACCES-style causes are visible.
|
|
115
|
+
logInternalError("pi-spawn.resolve-pi-package-root.probe", error, `dir=${dir}`, "debug");
|
|
85
116
|
// Continue walking upward.
|
|
86
117
|
}
|
|
87
118
|
dir = path.dirname(dir);
|
|
88
119
|
}
|
|
89
|
-
} catch {
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// R17-B3: realpath failure on argv[1] is unexpected (not the normal
|
|
122
|
+
// missing-entry case, which is handled by the !entry guard above) —
|
|
123
|
+
// debug-gated visibility for the "cannot find pi" diagnosis.
|
|
124
|
+
logInternalError("pi-spawn.resolve-pi-package-root", error, `argv1=${process.argv[1] ?? ""}`, "debug");
|
|
90
125
|
return undefined;
|
|
91
126
|
}
|
|
92
127
|
return undefined;
|
|
@@ -101,7 +136,10 @@ function packageBinScript(packageJsonPath: string): string | undefined {
|
|
|
101
136
|
if (!binPath) return undefined;
|
|
102
137
|
const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
|
|
103
138
|
return isRunnableNodeScript(candidate) ? candidate : undefined;
|
|
104
|
-
} catch {
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// R17-B3 (LOW): a package.json that EXISTS but fails to parse is corrupt —
|
|
141
|
+
// not the expected ENOENT of the upward-walk probes.
|
|
142
|
+
logInternalError("pi-spawn.package-bin-script", error, `packageJsonPath=${packageJsonPath}`, "warn");
|
|
105
143
|
return undefined;
|
|
106
144
|
}
|
|
107
145
|
}
|
|
@@ -115,7 +153,10 @@ function findPiPackageJsonFrom(startDir: string): string | undefined {
|
|
|
115
153
|
name?: string;
|
|
116
154
|
};
|
|
117
155
|
if (pkg.name && PI_PACKAGE_NAMES.includes(pkg.name)) return direct;
|
|
118
|
-
} catch {
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// R17-B3: same EXPECTED-ENOENT upward-walk probe as
|
|
158
|
+
// resolvePiPackageRoot — debug-gated, keeps walking either way.
|
|
159
|
+
logInternalError("pi-spawn.find-pi-package-json.probe", error, `dir=${dir}`, "debug");
|
|
119
160
|
// Continue searching upward and in node_modules.
|
|
120
161
|
}
|
|
121
162
|
for (const pkgName of PI_PACKAGE_NAMES) {
|
|
@@ -163,7 +204,12 @@ export function resolveNpmGlobalRoot(): string | undefined {
|
|
|
163
204
|
windowsHide: true,
|
|
164
205
|
}).trim();
|
|
165
206
|
resolved = out.length > 0 ? out : undefined;
|
|
166
|
-
} catch {
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// R17-B3 (LOW): `npm root -g` failing (npm not on PATH / timeout) means
|
|
209
|
+
// Windows non-APPDATA installs fall back to the static roots and often
|
|
210
|
+
// fail with ENOENT downstream — surface the root cause here. Memoized,
|
|
211
|
+
// so at most one warn per process.
|
|
212
|
+
logInternalError("pi-spawn.npm-root-g", error, "npm root -g probe failed", "warn");
|
|
167
213
|
resolved = undefined;
|
|
168
214
|
}
|
|
169
215
|
cachedNpmGlobalRoot = resolved ?? null;
|
|
@@ -240,14 +286,14 @@ function validateExplicitBin(explicit: string): string | undefined {
|
|
|
240
286
|
}
|
|
241
287
|
} catch (e) {
|
|
242
288
|
if (e instanceof Error && e.message.includes("allowed prefixes")) throw e;
|
|
243
|
-
|
|
289
|
+
logInternalError("pi-spawn", e, "validateExplicitBin: unexpected realpathSync error", "error");
|
|
244
290
|
return undefined;
|
|
245
291
|
}
|
|
246
292
|
return resolved;
|
|
247
293
|
}
|
|
248
294
|
|
|
249
295
|
export function getPiSpawnCommand(args: string[]): PiSpawnCommand {
|
|
250
|
-
const explicit =
|
|
296
|
+
const explicit = getCrewEnv("PI_TEAMS_PI_BIN")?.trim();
|
|
251
297
|
if (explicit) {
|
|
252
298
|
const validated = validateExplicitBin(explicit);
|
|
253
299
|
if (validated) {
|