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
|
@@ -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,8 +12,10 @@ 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";
|
|
18
|
+
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
15
19
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
16
20
|
|
|
17
21
|
let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
|
|
@@ -99,7 +103,7 @@ function tailFile(filePath: string, maxBytes = 4096): string | undefined {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: number | undefined, logPath: string): void {
|
|
102
|
-
if (
|
|
106
|
+
if (getCrewEnv("PI_CREW_ASYNC_EARLY_EXIT_GUARD") === "0") return;
|
|
103
107
|
const timer = setTimeout(() => {
|
|
104
108
|
const loaded = loadRunManifestById(cwd, runId);
|
|
105
109
|
if (!loaded || !isActiveRunStatus(loaded.manifest.status)) return;
|
|
@@ -112,15 +116,38 @@ function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: numbe
|
|
|
112
116
|
return;
|
|
113
117
|
const liveness = checkProcessLiveness(pid);
|
|
114
118
|
if (liveness.alive) return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
// R14-3 (Round 14): stale-snapshot RMW — the failed-status write used the
|
|
120
|
+
// pre-lock `loaded` snapshot, so a concurrent non-terminal write (e.g.
|
|
121
|
+
// runtime.resolved) could be clobbered. Re-read fresh inside the run lock
|
|
122
|
+
// and re-verify the guard checks so a run that completed/cancelled since
|
|
123
|
+
// the initial load is not flipped to failed.
|
|
124
|
+
try {
|
|
125
|
+
withRunLockSync(loaded.manifest, () => {
|
|
126
|
+
const fresh = loadRunManifestById(cwd, runId);
|
|
127
|
+
if (!fresh) return;
|
|
128
|
+
if (!isActiveRunStatus(fresh.manifest.status)) return;
|
|
129
|
+
if (hasAsyncStartMarker(fresh.manifest)) return;
|
|
130
|
+
if (
|
|
131
|
+
readEventsCursor(fresh.manifest.eventsPath).events.some(
|
|
132
|
+
(event) => event.type === "async.started" || event.type === "async.completed" || event.type === "async.failed",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return;
|
|
136
|
+
const tail = tailFile(logPath);
|
|
137
|
+
const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
|
|
138
|
+
const failed = updateRunStatus(fresh.manifest, "failed", "Background runner exited within 3s; see background.log");
|
|
139
|
+
void appendEventAsync(failed.eventsPath, {
|
|
140
|
+
type: "async.failed",
|
|
141
|
+
runId: failed.runId,
|
|
142
|
+
message,
|
|
143
|
+
data: { pid, detail: liveness.detail },
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// One-shot unref'd timer — a lock-contention error must not escape and
|
|
148
|
+
// crash the process; the owning session's poll will surface the failure.
|
|
149
|
+
logInternalError("team-tool.run.earlyExitGuard", error instanceof Error ? error : new Error(String(error)), `runId=${runId}`);
|
|
150
|
+
}
|
|
124
151
|
}, 3000);
|
|
125
152
|
timer.unref();
|
|
126
153
|
}
|
|
@@ -255,8 +282,12 @@ function formatRunResult(manifest: TeamRunManifest, options: FormatRunResultOpti
|
|
|
255
282
|
}
|
|
256
283
|
const shortResult = resultExcerpt.slice(0, 500);
|
|
257
284
|
const statusTag = task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
|
|
285
|
+
// bug-026 sub-issue B: surface the classified fatal-fs cause in the
|
|
286
|
+
// final report — "failed (disk full)" instead of a bare "failed".
|
|
287
|
+
const statusText =
|
|
288
|
+
task.status === "failed" && task.failureCause ? `${task.status} (${fsFailureLabel(task.failureCause)})` : task.status;
|
|
258
289
|
taskLines.push(
|
|
259
|
-
`- ${statusTag} ${task.id} [${task.role}]: ${
|
|
290
|
+
`- ${statusTag} ${task.id} [${task.role}]: ${statusText}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
260
291
|
);
|
|
261
292
|
if (task.status === "failed" || task.status === "needs_attention") {
|
|
262
293
|
failedCount++;
|
|
@@ -471,7 +502,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
471
502
|
const runtime = await resolveCrewRuntime(executedConfig);
|
|
472
503
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
473
504
|
// DEBUG: log what we received (gated to avoid stdout pollution in production)
|
|
474
|
-
if (
|
|
505
|
+
if (getCrewEnv("PI_CREW_DEBUG_BUDGET") === "1") {
|
|
475
506
|
console.log(
|
|
476
507
|
"[DEBUG budget] params keys:",
|
|
477
508
|
Object.keys(params),
|
|
@@ -558,8 +589,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
558
589
|
diagnostics: {
|
|
559
590
|
requestedMode: effectiveRuntime.requestedMode,
|
|
560
591
|
workersDisabled: executedConfig.executeWorkers === false,
|
|
561
|
-
envCrew:
|
|
562
|
-
envTeams:
|
|
592
|
+
envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
|
|
593
|
+
envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
|
|
563
594
|
},
|
|
564
595
|
},
|
|
565
596
|
});
|
|
@@ -570,7 +601,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
570
601
|
`Runtime: ${effectiveRuntime.kind} (requested ${effectiveRuntime.requestedMode})`,
|
|
571
602
|
`Reason: ${effectiveRuntime.reason ?? "unknown"}`,
|
|
572
603
|
`Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
|
|
573
|
-
`Env: PI_CREW_EXECUTE_WORKERS=${
|
|
604
|
+
`Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
|
|
574
605
|
].join("\n"),
|
|
575
606
|
{
|
|
576
607
|
action: "run",
|
|
@@ -653,8 +684,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
653
684
|
diagnostics: {
|
|
654
685
|
requestedMode: runtime.requestedMode,
|
|
655
686
|
workersDisabled: executedConfig.executeWorkers === false,
|
|
656
|
-
envCrew:
|
|
657
|
-
envTeams:
|
|
687
|
+
envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
|
|
688
|
+
envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
|
|
658
689
|
},
|
|
659
690
|
},
|
|
660
691
|
});
|
|
@@ -665,7 +696,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
665
696
|
`Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
|
|
666
697
|
`Reason: ${runtime.reason ?? "unknown"}`,
|
|
667
698
|
`Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
|
|
668
|
-
`Env: PI_CREW_EXECUTE_WORKERS=${
|
|
699
|
+
`Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
|
|
669
700
|
"",
|
|
670
701
|
"To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
|
|
671
702
|
"Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
|
|
@@ -692,12 +723,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
692
723
|
// so cancel-via-abortForegroundRun propagates to executeTeamRun.
|
|
693
724
|
fgCallbackSignal = signal;
|
|
694
725
|
if (signal && signal !== fgSignal) {
|
|
695
|
-
if (signal.aborted) fgDeadline.controller.
|
|
726
|
+
if (signal.aborted) safeAbort(fgDeadline.controller, "fg-run.caller-preaborted");
|
|
696
727
|
else {
|
|
697
728
|
// RC-03: keep the ref so we can removeEventListener on completion (the
|
|
698
729
|
// {once:true} alone leaks on the success path — the listener stays
|
|
699
730
|
// attached to the long-lived callback signal if the deadline never fires).
|
|
700
|
-
|
|
731
|
+
// safe-abort: the deadline signal may be linked to spawned children;
|
|
732
|
+
// aborting after they exited throws AbortError from Node's listener.
|
|
733
|
+
fgAbortListener = () => safeAbort(fgDeadline.controller, "fg-run.caller-abort");
|
|
701
734
|
signal.addEventListener("abort", fgAbortListener, { once: true });
|
|
702
735
|
}
|
|
703
736
|
}
|
|
@@ -741,6 +774,27 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
741
774
|
// Wait for the foreground run to complete and return actual results.
|
|
742
775
|
try {
|
|
743
776
|
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: fgDeadline.deadlineMs });
|
|
777
|
+
if (completed.detached) {
|
|
778
|
+
// The waiter was released so this turn can settle (agent view
|
|
779
|
+
// switch); the run keeps executing and reports on completion.
|
|
780
|
+
return result(
|
|
781
|
+
[
|
|
782
|
+
`pi-crew run detached to background: ${updatedManifest.runId}`,
|
|
783
|
+
`Team: ${team.name}`,
|
|
784
|
+
`Workflow: ${workflow.name}`,
|
|
785
|
+
`Goal: ${goal}`,
|
|
786
|
+
"",
|
|
787
|
+
"The run keeps executing — you will be notified when it finishes.",
|
|
788
|
+
`Check status with: team status runId=${updatedManifest.runId}`,
|
|
789
|
+
].join("\n"),
|
|
790
|
+
{
|
|
791
|
+
action: "run",
|
|
792
|
+
status: "ok",
|
|
793
|
+
runId: updatedManifest.runId,
|
|
794
|
+
artifactsRoot: updatedManifest.artifactsRoot,
|
|
795
|
+
},
|
|
796
|
+
);
|
|
797
|
+
}
|
|
744
798
|
return formatRunResult(completed.manifest, {
|
|
745
799
|
tasks: completed.tasks,
|
|
746
800
|
metrics: collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId),
|
|
@@ -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
|
|