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
|
@@ -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,43 @@ 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
|
+
console.warn(
|
|
286
|
+
`[model-routing] unvalidated passthrough: '${model}' (${reason}) — not confirmed against the configured model catalog; spawn may fail at the provider.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Test seam: the dedup set is module-global (one warn per model+reason per
|
|
291
|
+
* process); tests reset it for deterministic counting. */
|
|
292
|
+
export function resetPassthroughWarnings(): void {
|
|
293
|
+
passthroughWarned.clear();
|
|
294
|
+
}
|
|
295
|
+
|
|
276
296
|
export function resolveModelCandidate(
|
|
277
297
|
model: string | undefined,
|
|
278
298
|
availableModels: AvailableModelInfo[] | undefined,
|
|
279
299
|
preferredProvider?: string,
|
|
280
300
|
): string | undefined {
|
|
281
301
|
if (!model) return undefined;
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
// Provider-qualified refs ("provider/model") pass through UNVALIDATED —
|
|
303
|
+
// the caller asserted a full id; there is nothing to resolve. Loud (R8):
|
|
304
|
+
// this is the documented trust path, not a silent one.
|
|
305
|
+
if (model.includes("/")) {
|
|
306
|
+
warnUnvalidatedPassthrough(model, "provider-qualified ref, no catalog check");
|
|
307
|
+
return model;
|
|
308
|
+
}
|
|
309
|
+
if (!availableModels || availableModels.length === 0) {
|
|
310
|
+
warnUnvalidatedPassthrough(model, "no model catalog available");
|
|
311
|
+
return model;
|
|
312
|
+
}
|
|
284
313
|
|
|
285
314
|
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
286
315
|
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
@@ -294,6 +323,7 @@ export function resolveModelCandidate(
|
|
|
294
323
|
// Fuzzy fallback: try to resolve via partial name matching
|
|
295
324
|
const fuzzy = fuzzyResolveModelId(baseModel, availableModels);
|
|
296
325
|
if (fuzzy) return `${fuzzy}${thinkingSuffix}`;
|
|
326
|
+
warnUnvalidatedPassthrough(model, "no exact or fuzzy catalog match");
|
|
297
327
|
return model;
|
|
298
328
|
}
|
|
299
329
|
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) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-approval gate helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
|
|
5
|
+
* plan-approval family). Pure code motion: requiresPlanApproval /
|
|
6
|
+
* isPlanApprovalPending / isMutatingTask / ensurePlanApprovalRequested moved
|
|
7
|
+
* verbatim from team-runner.ts. cancelPlanTasks stays in team-runner.ts
|
|
8
|
+
* (RT-14 structural source-pin test requires it defined there).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import type { CrewRuntimeConfig } from "../config/config.ts";
|
|
13
|
+
import { parsePlannerPlanOutput } from "../extension/plan-orchestrate.ts";
|
|
14
|
+
import { appendEvent } from "../state/event-log/event-log.ts";
|
|
15
|
+
import { appendPlanRevision, getCurrentPlanRecord, setPlanApproval } from "../state/stores/plan-store.ts";
|
|
16
|
+
import { saveRunManifestAsync } from "../state/stores/state-store.ts";
|
|
17
|
+
import type { PlanRecord, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
18
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
19
|
+
import { permissionForRole } from "./role-permission.ts";
|
|
20
|
+
|
|
21
|
+
export function requiresPlanApproval(_workflow: WorkflowConfig, runtimeConfig: CrewRuntimeConfig | undefined): boolean {
|
|
22
|
+
// ROADMAP T1.2: plan-level HITL applies to ANY workflow when
|
|
23
|
+
// config.runtime.requirePlanApproval === true (not just 'implementation').
|
|
24
|
+
// The gate fires at the read-only → mutating (plan → execute) boundary.
|
|
25
|
+
return runtimeConfig?.requirePlanApproval === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** State-level predicate shared by every render surface (widget badge,
|
|
29
|
+
* progress banner, powerbar segment) so gating is byte-identical to the
|
|
30
|
+
* action side: `required === true && status === "pending"`. A malformed
|
|
31
|
+
* manifest (`pending` without `required`) must NOT light up surfaces while
|
|
32
|
+
* keys/backends refuse — single source of truth (WP-3 review F2). */
|
|
33
|
+
export function isPlanApprovalStatePending(approval: TeamRunManifest["planApproval"]): boolean {
|
|
34
|
+
return approval?.required === true && approval.status === "pending";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isPlanApprovalPending(manifest: TeamRunManifest): boolean {
|
|
38
|
+
return isPlanApprovalStatePending(manifest.planApproval);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* T2/R4 (ADR-4 §2, reader migration): plan-record-first approval predicate
|
|
43
|
+
* with manifest fallback. The current revision's approval state, when present,
|
|
44
|
+
* is authoritative; otherwise the legacy manifest gate decides. Pre-v2 runs
|
|
45
|
+
* (no plans.json) take exactly the old path — pinned by the migration
|
|
46
|
+
* negative AC in plan-store.test.ts.
|
|
47
|
+
*/
|
|
48
|
+
export function isPlanApprovalPendingEffective(manifest: TeamRunManifest): boolean {
|
|
49
|
+
const current = getCurrentPlanRecord(manifest);
|
|
50
|
+
if (current?.approval) return current.approval.status === "pending";
|
|
51
|
+
return isPlanApprovalPending(manifest);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* T2/R4 (ADR-4 §8 vocabulary mapping): a deny is `cancelled` on the manifest
|
|
56
|
+
* side and `rejected` on the record side — either means "decided negative"
|
|
57
|
+
* to the scheduler cancel paths (team-runner.ts readers).
|
|
58
|
+
*/
|
|
59
|
+
export function isPlanApprovalDenied(manifest: TeamRunManifest): boolean {
|
|
60
|
+
if (manifest.planApproval?.status === "cancelled") return true;
|
|
61
|
+
return getCurrentPlanRecord(manifest)?.approval?.status === "rejected";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isMutatingTask(task: TeamTaskState): boolean {
|
|
65
|
+
return permissionForRole(task.role) !== "read_only";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function ensurePlanApprovalRequested(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<TeamRunManifest> {
|
|
69
|
+
if (manifest.planApproval) return manifest;
|
|
70
|
+
const assessTask = tasks.find((task) => task.stepId === "assess" && task.status === "completed");
|
|
71
|
+
// ROADMAP T1.2: for non-adaptive workflows, fall back to the most recent
|
|
72
|
+
// completed read-only (planning) task as the plan reference.
|
|
73
|
+
const planTask = assessTask ?? [...tasks].reverse().find((t) => t.status === "completed" && !isMutatingTask(t));
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
|
|
76
|
+
// T2/R4 (ADR-4 §6 producer 3 + §8 dual-write): make sure a PlanRecord
|
|
77
|
+
// exists before the gate lights up, so approval names plan id+version.
|
|
78
|
+
// - Record already present (adaptive producer ran, or a re-request after
|
|
79
|
+
// crash between the record append and the manifest save) → reuse it.
|
|
80
|
+
// - No record → try the planner-tagged `<plan>` contract on the plan
|
|
81
|
+
// artifact; if that fails → manifest-only gate (pre-v2 behavior, the
|
|
82
|
+
// dual-read fallback in plan-store keeps readers correct).
|
|
83
|
+
let record = getCurrentPlanRecord(manifest);
|
|
84
|
+
if (!record && planTask?.resultArtifact?.path) {
|
|
85
|
+
try {
|
|
86
|
+
const text = fs.readFileSync(planTask.resultArtifact.path, "utf-8");
|
|
87
|
+
const parsed: PlanRecord | undefined = parsePlannerPlanOutput(text, manifest.runId, planTask.id);
|
|
88
|
+
if (parsed) record = appendPlanRevision(manifest, parsed);
|
|
89
|
+
} catch {
|
|
90
|
+
// Unreadable/unparsable artifact → manifest-only gate (below); the
|
|
91
|
+
// gate must still light up — never a throw here.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (record) {
|
|
95
|
+
setPlanApproval(manifest, { status: "pending", planVersion: record.version });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const updated: TeamRunManifest = {
|
|
99
|
+
...manifest,
|
|
100
|
+
updatedAt: now,
|
|
101
|
+
planApproval: {
|
|
102
|
+
required: true,
|
|
103
|
+
status: "pending",
|
|
104
|
+
requestedAt: now,
|
|
105
|
+
updatedAt: now,
|
|
106
|
+
planTaskId: planTask?.id,
|
|
107
|
+
planArtifactPath: planTask?.resultArtifact?.path,
|
|
108
|
+
},
|
|
109
|
+
plan: record ? { id: record.id, version: record.version } : manifest.plan,
|
|
110
|
+
};
|
|
111
|
+
await saveRunManifestAsync(updated);
|
|
112
|
+
appendEvent(updated.eventsPath, {
|
|
113
|
+
type: "plan.approval_required",
|
|
114
|
+
runId: updated.runId,
|
|
115
|
+
taskId: planTask?.id,
|
|
116
|
+
message: "Plan requires explicit approval before mutating tasks run. Use: team api op=approve-plan runId=...",
|
|
117
|
+
data: { planArtifactPath: planTask?.resultArtifact?.path, planId: record?.id, planVersion: record?.version },
|
|
118
|
+
});
|
|
119
|
+
return updated;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @internal 1.9(b) test export — exercise requiresPlanApproval directly. */
|
|
123
|
+
export const __test__requiresPlanApproval = requiresPlanApproval;
|
|
124
|
+
/** @internal 1.9(b) test export — exercise ensurePlanApprovalRequested directly. */
|
|
125
|
+
export const __test__ensurePlanApprovalRequested = ensurePlanApprovalRequested;
|