pi-crew 0.9.67 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -0
- 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 +65847 -60331
- 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 +9 -6
- package/schema.json +15 -1
- 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 +36 -14
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -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 +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- 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 +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- 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-spawn.ts +60 -14
- 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/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 +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- 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 +7 -1
- 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 +32 -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/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- 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/scratchpad/README.md +10 -5
- package/src/runtime/scratchpad/guest.ts +103 -2
- package/src/runtime/scratchpad/protocol.ts +13 -1
- package/src/runtime/scratchpad/transform.ts +206 -12
- 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 +129 -1671
- 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 +65 -25
- 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 +66 -229
- package/src/state/types.ts +197 -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/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- 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/widget-renderer.ts +9 -1
- 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/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -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/scratchpad/snapshot-hmac.ts +0 -167
- 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
|
@@ -16,6 +16,7 @@ import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
|
16
16
|
import { loadConfig } from "../../config/config.ts";
|
|
17
17
|
import { sanitizeTaskText } from "../../runtime/task-packet.ts";
|
|
18
18
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
19
|
+
import { loadSpecRecord } from "../../state/stores/spec-store.ts";
|
|
19
20
|
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
20
21
|
import type { TeamConfig } from "../../teams/team-config.ts";
|
|
21
22
|
import { errorMessage } from "../../utils/guards.ts";
|
|
@@ -23,6 +24,38 @@ import { logInternalError } from "../../utils/internal-error.ts";
|
|
|
23
24
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
24
25
|
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
25
26
|
import type { WorkflowConfig } from "../../workflows/workflow-config.ts";
|
|
27
|
+
|
|
28
|
+
/** T4/R6 (ADR-6 §5 + round-1): reject-start reason for strict usage —
|
|
29
|
+
* exported for tests. undefined = start allowed. Covers: workflow-level OR
|
|
30
|
+
* step-level specStrict without a verifier-role step (per-step flags bypass
|
|
31
|
+
* the same no-silent-self-certification rule), DWF (structurally verifier-
|
|
32
|
+
* less in v0.10.x), and — when cwd is provided — declared specRefs that
|
|
33
|
+
* resolve to nothing at start (fail-closed freeze, round-1 P2). */
|
|
34
|
+
export function specStrictRejectReason(workflow: WorkflowConfig, cwd?: string): string | undefined {
|
|
35
|
+
const strict = workflow.specStrict === true || workflow.steps.some((s) => s.specStrict === true);
|
|
36
|
+
if (!strict) return undefined;
|
|
37
|
+
if (workflow.runtime === "dynamic") {
|
|
38
|
+
return `Workflow '${workflow.name}' uses specStrict, but dynamic workflows do not support strict spec mode in v0.10.x (ctx.agent() tasks have no verifier-role gate). Drop specStrict for DWF runs (ADR-6 erratum §11).`;
|
|
39
|
+
}
|
|
40
|
+
if (!workflow.steps.some((s) => s.role === "verifier")) {
|
|
41
|
+
return `Workflow '${workflow.name}' enables specStrict (workflow or step level) but has no verifier-role step — strict mode requires independent verification (ADR-6 §5). Add a verifier step or drop specStrict.`;
|
|
42
|
+
}
|
|
43
|
+
if (cwd) {
|
|
44
|
+
// Round-1 P2: fail-closed at START — a strict step whose specRefs resolve
|
|
45
|
+
// to nothing would otherwise silently degrade to ungated at dispatch.
|
|
46
|
+
const declared = [...(workflow.specStrict === true ? workflow.steps : workflow.steps.filter((s) => s.specStrict === true))]
|
|
47
|
+
.flatMap((s) => s.specRefs ?? [])
|
|
48
|
+
.map((id) => ({ id, resolved: loadSpecRecord(cwd, id) !== undefined }))
|
|
49
|
+
.filter((x) => !x.resolved);
|
|
50
|
+
if (declared.length > 0) {
|
|
51
|
+
return `Workflow '${workflow.name}' is strict but these specRefs resolve to nothing in state/specs (typo, not imported, or corrupted): ${declared
|
|
52
|
+
.map((x) => x.id)
|
|
53
|
+
.join(", ")}. Import them first (scripts/spec-import.mjs) or fix the refs (ADR-6 erratum §11).`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
import { assertCleanLeaderAsync, findGitRootAsync } from "../../worktree/worktree-manager.ts";
|
|
27
60
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
28
61
|
import type { TeamContext } from "./context.ts";
|
|
@@ -274,6 +307,36 @@ export async function validateRunIntent(
|
|
|
274
307
|
);
|
|
275
308
|
}
|
|
276
309
|
|
|
310
|
+
// T4/R6 (ADR-6 §5): REJECT-START — a strict-mode workflow without a
|
|
311
|
+
// verifier-role step fails at start (no silent self-certification).
|
|
312
|
+
// Non-strict workflows are unaffected.
|
|
313
|
+
const rejectReason = !directAgent ? specStrictRejectReason(workflow, resolvedCtx.cwd) : undefined;
|
|
314
|
+
if (rejectReason) {
|
|
315
|
+
return {
|
|
316
|
+
kind: "error",
|
|
317
|
+
result: result(rejectReason, { action: "run", status: "error" }, true),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Platform honesty (ADR §4): loud warning — NOT best-effort. Where the
|
|
321
|
+
// re-run sandbox is unavailable every strict check fails closed.
|
|
322
|
+
if (!directAgent && workflow.specStrict === true && process.platform !== "linux") {
|
|
323
|
+
console.warn(
|
|
324
|
+
`⚠️ [team-tool.run] specStrict is enabled but this platform (${process.platform}) has no unshare -rn equivalent: every strict machine-check will FAIL CLOSED (ADR-6 §4).`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// WP-8 (R8): model-routing transparency — one pre-run line: resolved
|
|
329
|
+
// chain + worst-case spawns/task (attemptModels × (maxAttempts+1)).
|
|
330
|
+
if (!directAgent) {
|
|
331
|
+
try {
|
|
332
|
+
// LAZY: defer the routing-pipeline import to run start.
|
|
333
|
+
const { summarizeModelBudget } = await import("../../runtime/model/model-budget-summary.ts");
|
|
334
|
+
console.warn(summarizeModelBudget(resolvedCtx.cwd).line);
|
|
335
|
+
} catch {
|
|
336
|
+
/* advisory only — never block a run on the summary */
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
277
340
|
// PREFLIGHT (advisory only, since v0.9.15) — informational notes per the
|
|
278
341
|
// rule in .crew/knowledge.md "pi-crew USAGE THRESHOLD RULE". Never blocks.
|
|
279
342
|
if (!directAgent) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/config.ts";
|
|
2
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
2
3
|
// Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
|
|
3
4
|
import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
|
|
4
5
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
5
6
|
import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
7
|
+
import { withRunLockSync } from "../../state/coordination/locks.ts";
|
|
6
8
|
import { registerActiveRun, unregisterActiveRun } from "../../state/stores/active-run-registry.ts";
|
|
7
9
|
import { writeArtifact } from "../../state/stores/artifact-store.ts";
|
|
8
10
|
import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
@@ -10,6 +12,7 @@ import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../s
|
|
|
10
12
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
|
|
11
13
|
const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
|
|
12
14
|
|
|
15
|
+
import { fsFailureLabel } from "../../utils/fs-errno.ts";
|
|
13
16
|
import { errorMessage } from "../../utils/guards.ts";
|
|
14
17
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
15
18
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
@@ -99,7 +102,7 @@ function tailFile(filePath: string, maxBytes = 4096): string | undefined {
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: number | undefined, logPath: string): void {
|
|
102
|
-
if (
|
|
105
|
+
if (getCrewEnv("PI_CREW_ASYNC_EARLY_EXIT_GUARD") === "0") return;
|
|
103
106
|
const timer = setTimeout(() => {
|
|
104
107
|
const loaded = loadRunManifestById(cwd, runId);
|
|
105
108
|
if (!loaded || !isActiveRunStatus(loaded.manifest.status)) return;
|
|
@@ -112,15 +115,38 @@ function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: numbe
|
|
|
112
115
|
return;
|
|
113
116
|
const liveness = checkProcessLiveness(pid);
|
|
114
117
|
if (liveness.alive) return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
// R14-3 (Round 14): stale-snapshot RMW — the failed-status write used the
|
|
119
|
+
// pre-lock `loaded` snapshot, so a concurrent non-terminal write (e.g.
|
|
120
|
+
// runtime.resolved) could be clobbered. Re-read fresh inside the run lock
|
|
121
|
+
// and re-verify the guard checks so a run that completed/cancelled since
|
|
122
|
+
// the initial load is not flipped to failed.
|
|
123
|
+
try {
|
|
124
|
+
withRunLockSync(loaded.manifest, () => {
|
|
125
|
+
const fresh = loadRunManifestById(cwd, runId);
|
|
126
|
+
if (!fresh) return;
|
|
127
|
+
if (!isActiveRunStatus(fresh.manifest.status)) return;
|
|
128
|
+
if (hasAsyncStartMarker(fresh.manifest)) return;
|
|
129
|
+
if (
|
|
130
|
+
readEventsCursor(fresh.manifest.eventsPath).events.some(
|
|
131
|
+
(event) => event.type === "async.started" || event.type === "async.completed" || event.type === "async.failed",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
return;
|
|
135
|
+
const tail = tailFile(logPath);
|
|
136
|
+
const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
|
|
137
|
+
const failed = updateRunStatus(fresh.manifest, "failed", "Background runner exited within 3s; see background.log");
|
|
138
|
+
void appendEventAsync(failed.eventsPath, {
|
|
139
|
+
type: "async.failed",
|
|
140
|
+
runId: failed.runId,
|
|
141
|
+
message,
|
|
142
|
+
data: { pid, detail: liveness.detail },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// One-shot unref'd timer — a lock-contention error must not escape and
|
|
147
|
+
// crash the process; the owning session's poll will surface the failure.
|
|
148
|
+
logInternalError("team-tool.run.earlyExitGuard", error instanceof Error ? error : new Error(String(error)), `runId=${runId}`);
|
|
149
|
+
}
|
|
124
150
|
}, 3000);
|
|
125
151
|
timer.unref();
|
|
126
152
|
}
|
|
@@ -255,8 +281,12 @@ function formatRunResult(manifest: TeamRunManifest, options: FormatRunResultOpti
|
|
|
255
281
|
}
|
|
256
282
|
const shortResult = resultExcerpt.slice(0, 500);
|
|
257
283
|
const statusTag = task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
|
|
284
|
+
// bug-026 sub-issue B: surface the classified fatal-fs cause in the
|
|
285
|
+
// final report — "failed (disk full)" instead of a bare "failed".
|
|
286
|
+
const statusText =
|
|
287
|
+
task.status === "failed" && task.failureCause ? `${task.status} (${fsFailureLabel(task.failureCause)})` : task.status;
|
|
258
288
|
taskLines.push(
|
|
259
|
-
`- ${statusTag} ${task.id} [${task.role}]: ${
|
|
289
|
+
`- ${statusTag} ${task.id} [${task.role}]: ${statusText}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
260
290
|
);
|
|
261
291
|
if (task.status === "failed" || task.status === "needs_attention") {
|
|
262
292
|
failedCount++;
|
|
@@ -471,7 +501,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
471
501
|
const runtime = await resolveCrewRuntime(executedConfig);
|
|
472
502
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
473
503
|
// DEBUG: log what we received (gated to avoid stdout pollution in production)
|
|
474
|
-
if (
|
|
504
|
+
if (getCrewEnv("PI_CREW_DEBUG_BUDGET") === "1") {
|
|
475
505
|
console.log(
|
|
476
506
|
"[DEBUG budget] params keys:",
|
|
477
507
|
Object.keys(params),
|
|
@@ -558,8 +588,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
558
588
|
diagnostics: {
|
|
559
589
|
requestedMode: effectiveRuntime.requestedMode,
|
|
560
590
|
workersDisabled: executedConfig.executeWorkers === false,
|
|
561
|
-
envCrew:
|
|
562
|
-
envTeams:
|
|
591
|
+
envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
|
|
592
|
+
envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
|
|
563
593
|
},
|
|
564
594
|
},
|
|
565
595
|
});
|
|
@@ -570,7 +600,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
570
600
|
`Runtime: ${effectiveRuntime.kind} (requested ${effectiveRuntime.requestedMode})`,
|
|
571
601
|
`Reason: ${effectiveRuntime.reason ?? "unknown"}`,
|
|
572
602
|
`Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
|
|
573
|
-
`Env: PI_CREW_EXECUTE_WORKERS=${
|
|
603
|
+
`Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
|
|
574
604
|
].join("\n"),
|
|
575
605
|
{
|
|
576
606
|
action: "run",
|
|
@@ -653,8 +683,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
653
683
|
diagnostics: {
|
|
654
684
|
requestedMode: runtime.requestedMode,
|
|
655
685
|
workersDisabled: executedConfig.executeWorkers === false,
|
|
656
|
-
envCrew:
|
|
657
|
-
envTeams:
|
|
686
|
+
envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
|
|
687
|
+
envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
|
|
658
688
|
},
|
|
659
689
|
},
|
|
660
690
|
});
|
|
@@ -665,7 +695,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
665
695
|
`Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
|
|
666
696
|
`Reason: ${runtime.reason ?? "unknown"}`,
|
|
667
697
|
`Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
|
|
668
|
-
`Env: PI_CREW_EXECUTE_WORKERS=${
|
|
698
|
+
`Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
|
|
669
699
|
"",
|
|
670
700
|
"To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
|
|
671
701
|
"Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
|
|
@@ -8,6 +8,7 @@ import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-s
|
|
|
8
8
|
import { formatTaskGraphLines, waitingReason } from "../../runtime/task-display.ts";
|
|
9
9
|
import { verifyTaskCompletion } from "../../runtime/verification/completion-guard.ts";
|
|
10
10
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
11
|
+
import { withRunLockSync } from "../../state/coordination/locks.ts";
|
|
11
12
|
import { readDeliveryState, readMailbox } from "../../state/coordination/mailbox.ts";
|
|
12
13
|
import { appendEvent, readEventsCursor } from "../../state/event-log/event-log.ts";
|
|
13
14
|
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
@@ -38,6 +39,71 @@ function markStaleAsync(runId: string): boolean {
|
|
|
38
39
|
return true;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* R14-1 (Phase 3.4): the ONLY state mutation on the status read path — the
|
|
44
|
+
* stale-async transition — executed under the run lock with a FRESH re-read
|
|
45
|
+
* (respond.ts:43 pattern). handleStatus's `loaded` snapshot is best-effort
|
|
46
|
+
* (no lock): a concurrent writer can commit a terminal status between that
|
|
47
|
+
* load and this lock, and `updateRunStatus`'s transition guard would validate
|
|
48
|
+
* against the STALE status (running→failed legal) and clobber the fresh
|
|
49
|
+
* terminal state. Inside the lock we re-read and short-circuit when the fresh
|
|
50
|
+
* state no longer warrants the transition (run gone, async entry gone, status
|
|
51
|
+
* no longer active, process alive).
|
|
52
|
+
*
|
|
53
|
+
* Returns the transitioned manifest+tasks so the caller's read view reflects
|
|
54
|
+
* the write (same-poll visibility, matching pre-fix behavior), or undefined
|
|
55
|
+
* when the transition was skipped.
|
|
56
|
+
*
|
|
57
|
+
* Exported for unit testing (R14-1 regression: a run that completed on disk
|
|
58
|
+
* between load and lock must NOT be re-flipped to failed).
|
|
59
|
+
*/
|
|
60
|
+
export function transitionStaleAsyncUnderLock(
|
|
61
|
+
loaded: NonNullable<ReturnType<typeof loadRunManifestById>>,
|
|
62
|
+
runCwd: string,
|
|
63
|
+
runId: string,
|
|
64
|
+
):
|
|
65
|
+
| {
|
|
66
|
+
manifest: NonNullable<ReturnType<typeof loadRunManifestById>>["manifest"];
|
|
67
|
+
tasks: NonNullable<ReturnType<typeof loadRunManifestById>>["tasks"];
|
|
68
|
+
}
|
|
69
|
+
| undefined {
|
|
70
|
+
let transitioned:
|
|
71
|
+
| {
|
|
72
|
+
manifest: NonNullable<ReturnType<typeof loadRunManifestById>>["manifest"];
|
|
73
|
+
tasks: NonNullable<ReturnType<typeof loadRunManifestById>>["tasks"];
|
|
74
|
+
}
|
|
75
|
+
| undefined;
|
|
76
|
+
withRunLockSync(loaded.manifest, () => {
|
|
77
|
+
const fresh = loadRunManifestById(runCwd, runId); // NOTE: inside withRunLockSync - consistent read
|
|
78
|
+
if (!fresh?.manifest.async || !isActiveRunStatus(fresh.manifest.status)) return;
|
|
79
|
+
const freshLiveness = checkProcessLiveness(fresh.manifest.async.pid);
|
|
80
|
+
if (freshLiveness.alive) return;
|
|
81
|
+
const failed = updateRunStatus(fresh.manifest, "failed", `Async process stale: ${freshLiveness.detail}`);
|
|
82
|
+
const freshTasks = fresh.tasks.map((task) =>
|
|
83
|
+
task.status === "running"
|
|
84
|
+
? {
|
|
85
|
+
...task,
|
|
86
|
+
status: "cancelled" as const,
|
|
87
|
+
finishedAt: new Date().toISOString(),
|
|
88
|
+
error: "Async process died; task was not completed.",
|
|
89
|
+
}
|
|
90
|
+
: task,
|
|
91
|
+
);
|
|
92
|
+
saveRunTasks(failed, freshTasks);
|
|
93
|
+
// async.stale (2026-08-10): sync append (byte-identical to pre-extract
|
|
94
|
+
// api.ts) so callers reading eventsPath immediately after status see
|
|
95
|
+
// the event — async fire-and-forget would race.
|
|
96
|
+
appendEvent(failed.eventsPath, {
|
|
97
|
+
type: "async.stale",
|
|
98
|
+
runId: failed.runId,
|
|
99
|
+
message: freshLiveness.detail,
|
|
100
|
+
data: { pid: fresh.manifest.async.pid },
|
|
101
|
+
});
|
|
102
|
+
transitioned = { manifest: failed, tasks: freshTasks };
|
|
103
|
+
});
|
|
104
|
+
return transitioned;
|
|
105
|
+
}
|
|
106
|
+
|
|
41
107
|
export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
42
108
|
if (!params.runId)
|
|
43
109
|
return result(
|
|
@@ -60,31 +126,23 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
60
126
|
const liveness = checkProcessLiveness(asyncState.pid);
|
|
61
127
|
asyncLivenessLine = `Async: pid=${asyncState.pid ?? "unknown"} alive=${liveness.alive ? "true" : "false"} detail=${liveness.detail} log=${asyncState.logPath} spawnedAt=${asyncState.spawnedAt}`;
|
|
62
128
|
if (!liveness.alive && isActiveRunStatus(manifest.status)) {
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// api.ts) so callers reading eventsPath immediately after status see
|
|
81
|
-
// the event — async fire-and-forget would race.
|
|
82
|
-
appendEvent(manifest.eventsPath, {
|
|
83
|
-
type: "async.stale",
|
|
84
|
-
runId: manifest.runId,
|
|
85
|
-
message: liveness.detail,
|
|
86
|
-
data: { pid: asyncState.pid },
|
|
87
|
-
});
|
|
129
|
+
// R14-1 (Phase 3.4): ownership gate — a foreign session must NOT
|
|
130
|
+
// trigger the stale-async transition on a read path; the owning
|
|
131
|
+
// session's poll handles it. Mirrors handleResume (team-tool.ts R1).
|
|
132
|
+
// Backward compat: only applies when ownerSessionId is a non-empty
|
|
133
|
+
// string — legacy runs without an owner are not gated.
|
|
134
|
+
const foreignRun = typeof manifest.ownerSessionId === "string" && manifest.ownerSessionId !== ctx.sessionId;
|
|
135
|
+
if (!foreignRun || params.force) {
|
|
136
|
+
// H5: only transition once per runId. Repeated status polls of a
|
|
137
|
+
// dead async run previously re-ran saveRunTasks + sync appendEvent
|
|
138
|
+
// on every call (~30ms blocking each).
|
|
139
|
+
if (markStaleAsync(manifest.runId)) {
|
|
140
|
+
const transitioned = transitionStaleAsyncUnderLock(loaded, runCwd, params.runId);
|
|
141
|
+
if (transitioned) {
|
|
142
|
+
manifest = transitioned.manifest;
|
|
143
|
+
tasks = transitioned.tasks;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
88
146
|
}
|
|
89
147
|
}
|
|
90
148
|
}
|
|
@@ -193,7 +251,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
193
251
|
`- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.agentProgress?.activityState ? ` activityState=${task.agentProgress.activityState}` : ""}${(() => {
|
|
194
252
|
const t = extractCommandTrace(task.agentProgress?.recentTools);
|
|
195
253
|
return t.summary ? ` ${t.summary}` : "";
|
|
196
|
-
})()}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${String(attentionByTask.get(task.id)?.data?.reason)}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`,
|
|
254
|
+
})()}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${String(attentionByTask.get(task.id)?.data?.reason)}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}${task.failureCause ? ` failureCause=${task.failureCause}` : ""}`,
|
|
197
255
|
)
|
|
198
256
|
: ["- (none)"]),
|
|
199
257
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
4
4
|
import { allAgents, discoverAgents, listDynamicAgents, registerDynamicAgent, unregisterDynamicAgent } from "../agents/discover-agents.ts";
|
|
5
5
|
import { loadConfig } from "../config/config.ts";
|
|
6
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
6
7
|
// Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
|
|
7
8
|
// executeTeamRun is only called when a team run actually executes.
|
|
8
9
|
import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
|
|
@@ -373,8 +374,8 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
373
374
|
if (!executedConfig.runtime?.mode && resumeManifest.runtimeResolution?.safety === "explicit_dry_run") {
|
|
374
375
|
const workersDisabled =
|
|
375
376
|
executedConfig.executeWorkers === false ||
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
getCrewEnv("PI_CREW_EXECUTE_WORKERS") === "0" ||
|
|
378
|
+
getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") === "0";
|
|
378
379
|
if (!workersDisabled)
|
|
379
380
|
executedConfig.runtime = {
|
|
380
381
|
...executedConfig.runtime,
|
|
@@ -387,6 +388,14 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
387
388
|
...resumeManifest,
|
|
388
389
|
runtimeResolution,
|
|
389
390
|
updatedAt: new Date().toISOString(),
|
|
391
|
+
// B1 battery 2026-08-18 (case b root-cause fix): resume ADOPTS the run —
|
|
392
|
+
// ownerSessionId moves to the resuming session. Pre-fix, a force-resumed
|
|
393
|
+
// run kept its DEAD original owner id, so any THIRD session's startup
|
|
394
|
+
// orphan-scan saw "owner no longer exists" and cancelled the live,
|
|
395
|
+
// freshly-resumed run (observed live: orphan_cancelled 5m into a parked
|
|
396
|
+
// ask that the resume itself had just dispatched). Same-session resume
|
|
397
|
+
// is a no-op (id equality).
|
|
398
|
+
...(ctx.sessionId ? { ownerSessionId: ctx.sessionId } : {}),
|
|
390
399
|
};
|
|
391
400
|
await saveRunManifestAsync(runtimeManifest);
|
|
392
401
|
await appendEventAsync(runtimeManifest.eventsPath, {
|
package/src/hooks/registry.ts
CHANGED
|
@@ -23,13 +23,8 @@ export function registerHook(definition: HookDefinition): number {
|
|
|
23
23
|
return hookId;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function clearHooks(): void {
|
|
27
|
-
registry.clear();
|
|
28
|
-
_piCrewHookIds.clear();
|
|
29
|
-
_nextHookId = 1;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
// Scope-aware hook clearing: only removes hooks registered by pi-crew
|
|
27
|
+
// (keeps hooks registered by other extensions/hosts intact).
|
|
33
28
|
export function clearHooksScoped(): void {
|
|
34
29
|
for (const [name, hooks] of registry) {
|
|
35
30
|
const remaining = hooks.filter((h) => !("_hookId" in h && _piCrewHookIds.has((h as { _hookId?: number })._hookId ?? -1)));
|
package/src/i18n.ts
CHANGED
|
@@ -122,6 +122,11 @@ const translations: Record<string, Partial<Record<Key, string>>> = {
|
|
|
122
122
|
let currentLocale: string | undefined;
|
|
123
123
|
const warnedMissing = new Set<string>();
|
|
124
124
|
|
|
125
|
+
// R5-L5/MISSED-1 (Round 5): warn-once dedupe Set — bounded naturally by the
|
|
126
|
+
// translation-key count, but cap FIFO so a pathological locale/key space
|
|
127
|
+
// cannot grow it unbounded in a long-lived host process.
|
|
128
|
+
const MAX_WARNED_MISSING = 128;
|
|
129
|
+
|
|
125
130
|
// --- Helpers ---
|
|
126
131
|
|
|
127
132
|
function format(template: string, params: Params = {}): string {
|
|
@@ -132,6 +137,10 @@ function warnOnce(key: string): void {
|
|
|
132
137
|
const tag = `${currentLocale}:${key}`;
|
|
133
138
|
if (warnedMissing.has(tag)) return;
|
|
134
139
|
warnedMissing.add(tag);
|
|
140
|
+
if (warnedMissing.size > MAX_WARNED_MISSING) {
|
|
141
|
+
const oldest = warnedMissing.values().next().value;
|
|
142
|
+
if (oldest !== undefined) warnedMissing.delete(oldest);
|
|
143
|
+
}
|
|
135
144
|
logInternalError("i18n.missing", new Error(`Missing translation`), `key="${key}" locale="${currentLocale}"`);
|
|
136
145
|
}
|
|
137
146
|
|