pi-crew 0.9.68 → 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 +124 -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 +65838 -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 +4 -2
- 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 +2 -1
- 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/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/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
|
@@ -3,8 +3,10 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { errors } from "../errors.ts";
|
|
5
5
|
import { atomicWriteFile, atomicWriteJson } from "../state/atomic-write.ts";
|
|
6
|
+
import { getCurrentPlanRecord } from "../state/stores/plan-store.ts";
|
|
6
7
|
import { loadManifestWithRecovery, loadTasksWithRecovery, saveRunManifest } from "../state/stores/state-store.ts";
|
|
7
8
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
9
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
10
|
import { recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
9
11
|
import { checkProcessLiveness } from "./process-status.ts";
|
|
10
12
|
|
|
@@ -23,7 +25,7 @@ const ORPHAN_TEMP_SCAN_BATCH_SIZE = 50;
|
|
|
23
25
|
export interface ReconcileResult {
|
|
24
26
|
runId: string;
|
|
25
27
|
/** What was found and what action was taken */
|
|
26
|
-
verdict: "healthy" | "blocked_awaiting_approval" | "result_exists" | "pid_dead" | "pid_alive_stale" | "no_status";
|
|
28
|
+
verdict: "healthy" | "blocked_awaiting_approval" | "waiting_answer" | "result_exists" | "pid_dead" | "pid_alive_stale" | "no_status";
|
|
27
29
|
/** Whether repair was applied */
|
|
28
30
|
repaired: boolean;
|
|
29
31
|
/** Human-readable detail */
|
|
@@ -45,6 +47,50 @@ export function isPlanApprovalPending(manifest: TeamRunManifest): boolean {
|
|
|
45
47
|
return manifest.status === "blocked" && manifest.planApproval?.required === true && manifest.planApproval.status === "pending";
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/** T2/R4 (ADR-4 §2 reader migration): plan-record-first variant — the current
|
|
51
|
+
* PlanRecord's approval (when present) is authoritative; pre-v2 runs fall
|
|
52
|
+
* back to the manifest gate above. The `blocked + pending → protected`
|
|
53
|
+
* invariant is preserved on BOTH paths. */
|
|
54
|
+
export function isPlanApprovalPendingEffective(manifest: TeamRunManifest): boolean {
|
|
55
|
+
if (isPlanApprovalPending(manifest)) return true;
|
|
56
|
+
if (manifest.status !== "blocked") return false;
|
|
57
|
+
return getCurrentPlanRecord(manifest)?.approval?.status === "pending";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** WP-2/R2 (ADR-0 item 9, docs/decisions/2026-08-17-waiting-producer-ask.md):
|
|
61
|
+
* waiting TTL — a manifest.waitState.askedAt younger than this marks an
|
|
62
|
+
* intentional ask-wait; older marks a leaked park marker (worker died without
|
|
63
|
+
* resolving the ask) and the run loses reconciler protection. Leak guard. */
|
|
64
|
+
const WAITING_TTL_MS = 24 * 60 * 60 * 1000;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Is this run parked in the ask tool awaiting a human answer (WP-2/R2)?
|
|
68
|
+
*
|
|
69
|
+
* True when waitState is set and its askedAt is within waitingTtl. The parked
|
|
70
|
+
* worker is blocked polling the mailbox for a response — not crashed — and it
|
|
71
|
+
* typically stops heartbeating while parked, so the run must be protected at
|
|
72
|
+
* run level (before the no-PID/heartbeat staleness phases) exactly like
|
|
73
|
+
* plan-approval runs. askedAt exactly at the TTL boundary still counts as
|
|
74
|
+
* pending (<=, fail-safe toward preservation); a future askedAt (clock skew)
|
|
75
|
+
* also passes. TTL-EXPIRED waitState returns false — the marker leaked and the
|
|
76
|
+
* normal staleness repair paths apply (leak guard).
|
|
77
|
+
*/
|
|
78
|
+
export function isWaitAnswerPending(manifest: TeamRunManifest, now = Date.now()): boolean {
|
|
79
|
+
const askedAtMs = manifest.waitState?.askedAt ? new Date(manifest.waitState.askedAt).getTime() : Number.NaN;
|
|
80
|
+
return Number.isFinite(askedAtMs) && now - askedAtMs <= WAITING_TTL_MS;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generalized intentional-wait predicate (ADR-0 item 9, extended by ADR-4 §2):
|
|
85
|
+
* the run is blocked on a human decision — plan approval (record-first with
|
|
86
|
+
* manifest fallback — pre-v2 semantics preserved on the fallback path) or a
|
|
87
|
+
* pending ask answer within waitingTtl. Such runs are not crashes and must not
|
|
88
|
+
* be stale-repaired or cancelled by reconciliation.
|
|
89
|
+
*/
|
|
90
|
+
export function isIntentionalWait(manifest: TeamRunManifest, now = Date.now()): boolean {
|
|
91
|
+
return isPlanApprovalPendingEffective(manifest) || isWaitAnswerPending(manifest, now);
|
|
92
|
+
}
|
|
93
|
+
|
|
48
94
|
const STALE_ALIVE_PID_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
49
95
|
const ACTIVE_EVIDENCE_TTL_MS = 5 * 60 * 1000;
|
|
50
96
|
/** For no-PID runs, repair when ALL running tasks have heartbeat stale beyond this threshold. */
|
|
@@ -315,7 +361,9 @@ export function reconcileStaleRun(manifest: TeamRunManifest, tasks: TeamTaskStat
|
|
|
315
361
|
// Preserve runs intentionally blocked on human plan approval. These are not
|
|
316
362
|
// crashes even if the owning PID is gone — they are waiting for a decision.
|
|
317
363
|
// Must short-circuit before Phase 1 (result check) and Phase 2 (PID liveness).
|
|
318
|
-
|
|
364
|
+
// ADR-4 §2: record-first predicate — a PlanRecord approval is authoritative,
|
|
365
|
+
// the manifest field remains the pre-v2 fallback (never dropped).
|
|
366
|
+
if (isPlanApprovalPendingEffective(manifest)) {
|
|
319
367
|
return {
|
|
320
368
|
runId,
|
|
321
369
|
verdict: "blocked_awaiting_approval",
|
|
@@ -324,6 +372,22 @@ export function reconcileStaleRun(manifest: TeamRunManifest, tasks: TeamTaskStat
|
|
|
324
372
|
};
|
|
325
373
|
}
|
|
326
374
|
|
|
375
|
+
// WP-2/R2 (ADR-0 item 9): a run parked in the ask tool with a fresh
|
|
376
|
+
// waitState.askedAt is intentionally waiting on a human answer — not stale.
|
|
377
|
+
// Short-circuits before Phase 1/2 like plan-approval above: a parked worker
|
|
378
|
+
// does not heartbeat, so the no-PID/heartbeat staleness phases would
|
|
379
|
+
// otherwise cancel it long before its (server-clamped <=1h) ask deadline.
|
|
380
|
+
// TTL-expired waitState (>24h) falls through — the park marker leaked
|
|
381
|
+
// (worker died without resolving) and normal staleness repair applies.
|
|
382
|
+
if (isWaitAnswerPending(manifest, now)) {
|
|
383
|
+
return {
|
|
384
|
+
runId,
|
|
385
|
+
verdict: "waiting_answer",
|
|
386
|
+
repaired: false,
|
|
387
|
+
detail: "Ask answer pending (manifest.waitState.askedAt within waiting TTL); run is intentionally waiting and must not be stale-repaired",
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
327
391
|
// Phase 1: Check if results already exist
|
|
328
392
|
const phase1 = checkResultFile(manifest, tasks);
|
|
329
393
|
if (phase1.found) {
|
|
@@ -538,7 +602,14 @@ export function reconcileOrphanedTempWorkspaces(
|
|
|
538
602
|
}
|
|
539
603
|
}
|
|
540
604
|
// If still running after reconciliation attempt, mark for dir-preserving
|
|
541
|
-
if (
|
|
605
|
+
if (
|
|
606
|
+
result.verdict === "healthy" ||
|
|
607
|
+
// WP-2/R2: a run intentionally waiting on an ask answer keeps the
|
|
608
|
+
// temp workspace alive (its stateRoot + mailbox must survive for
|
|
609
|
+
// the parked worker to receive the answer).
|
|
610
|
+
result.verdict === "waiting_answer" ||
|
|
611
|
+
(result.verdict === "no_status" && !result.repaired)
|
|
612
|
+
) {
|
|
542
613
|
hasRunning = true;
|
|
543
614
|
}
|
|
544
615
|
} catch (err) {
|
|
@@ -546,14 +617,19 @@ export function reconcileOrphanedTempWorkspaces(
|
|
|
546
617
|
// Note: manifestPath here refers to the variable defined in the
|
|
547
618
|
// for loop at line 442 (outer scope of this catch block).
|
|
548
619
|
const scanManifestPath = manifestPath;
|
|
549
|
-
|
|
620
|
+
logInternalError(
|
|
621
|
+
"stale-reconciler",
|
|
622
|
+
new Error(`Skipping manifest due to parse error: ${scanManifestPath}: ${err}`),
|
|
623
|
+
undefined,
|
|
624
|
+
"warn",
|
|
625
|
+
);
|
|
550
626
|
}
|
|
551
627
|
}
|
|
552
628
|
} catch (err) {
|
|
553
629
|
// Cannot determine running state — treat as if running to prevent
|
|
554
630
|
// premature cleanup of a potentially active workspace.
|
|
555
631
|
hasRunning = true;
|
|
556
|
-
|
|
632
|
+
logInternalError("stale-reconciler", new Error(`Skipping unreadable runs dir: ${stateRunsDir}: ${err}`), undefined, "warn");
|
|
557
633
|
}
|
|
558
634
|
|
|
559
635
|
// Post-loop: check if this workspace dir can be cleaned up.
|
|
@@ -617,7 +693,12 @@ export function reconcileOrphanedTempWorkspaces(
|
|
|
617
693
|
}
|
|
618
694
|
}
|
|
619
695
|
} catch (err) {
|
|
620
|
-
|
|
696
|
+
logInternalError(
|
|
697
|
+
"stale-reconciler",
|
|
698
|
+
new Error(`Skipping unreadable runs dir: ${stateRunsDir}: ${err}`),
|
|
699
|
+
undefined,
|
|
700
|
+
"warn",
|
|
701
|
+
);
|
|
621
702
|
}
|
|
622
703
|
}
|
|
623
704
|
}
|
|
@@ -25,11 +25,26 @@ export interface SubagentSpawnOptions {
|
|
|
25
25
|
/** Optional batch grouping id (Rule 1). Agents sharing a batchId coalesce
|
|
26
26
|
* completion notifications into one. undefined => individual (default). */
|
|
27
27
|
batchId?: string;
|
|
28
|
+
/** WP-1/R1 (H6): owning team-run task id. Set by the one-shot Agent-tool
|
|
29
|
+
* route once the run manifest resolves (taskId is only knowable after the
|
|
30
|
+
* run is dispatched). undefined for unlinked/legacy spawns. */
|
|
31
|
+
taskId?: string;
|
|
32
|
+
/** WP-1/R1 (H6): ownership depth in the task tree (0 for a root one-shot).
|
|
33
|
+
* Optional for back-compat; defaults to 0 at spawn. WP-5 owns depth
|
|
34
|
+
* semantics for nested runs. */
|
|
35
|
+
depth?: number;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export interface SubagentRecord {
|
|
31
39
|
id: string;
|
|
32
40
|
runId?: string;
|
|
41
|
+
/** WP-1/R1 (H6): owning team-run task id (task ⇄ subagentId link).
|
|
42
|
+
* Optional for back-compat — records without it render as today and steer
|
|
43
|
+
* returns the existing "not linked" message (no throw). */
|
|
44
|
+
taskId?: string;
|
|
45
|
+
/** WP-1/R1 (H6): ownership depth (0 for root one-shot). Optional for
|
|
46
|
+
* back-compat — legacy records without it render as today. */
|
|
47
|
+
depth?: number;
|
|
33
48
|
type: string;
|
|
34
49
|
description: string;
|
|
35
50
|
prompt: string;
|
|
@@ -144,6 +159,7 @@ const ALLOWED_RECORD_FIELDS = new Set([
|
|
|
144
159
|
"runId",
|
|
145
160
|
"cwd",
|
|
146
161
|
"taskId",
|
|
162
|
+
"depth",
|
|
147
163
|
"result",
|
|
148
164
|
"error",
|
|
149
165
|
"resultConsumed",
|
|
@@ -185,7 +201,13 @@ export function readPersistedSubagentRecord(cwd: string, id: string): SubagentRe
|
|
|
185
201
|
try {
|
|
186
202
|
const raw = JSON.parse(fs.readFileSync(persistedSubagentPath(cwd, id), "utf-8"));
|
|
187
203
|
return sanitizePersistedRecord(raw);
|
|
188
|
-
} catch {
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// R17-B5: parse/read failures were fully silent, hiding lost records
|
|
206
|
+
// (see NEW-R3 note in savePersistedSubagentRecord). ENOENT stays quiet —
|
|
207
|
+
// a legitimately removed record (cancel) is not an error.
|
|
208
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
209
|
+
logInternalError("subagent-manager.read-persisted", error, `id=${id}`, "warn");
|
|
210
|
+
}
|
|
189
211
|
return undefined;
|
|
190
212
|
}
|
|
191
213
|
}
|
|
@@ -254,6 +276,8 @@ export class SubagentManager {
|
|
|
254
276
|
ownerSessionGeneration: options.ownerSessionGeneration,
|
|
255
277
|
ownerSessionId: options.ownerSessionId,
|
|
256
278
|
batchId: options.batchId,
|
|
279
|
+
taskId: options.taskId,
|
|
280
|
+
depth: options.depth ?? 0,
|
|
257
281
|
};
|
|
258
282
|
this.records.set(record.id, record);
|
|
259
283
|
this.cwdByRecord.set(record.id, options.cwd);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { DEFAULT_OUTPUT_CONTEXT } from "../config/defaults.ts";
|
|
4
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
4
5
|
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
5
6
|
import { writeArtifact } from "../state/stores/artifact-store.ts";
|
|
6
7
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
@@ -124,6 +125,13 @@ export interface ReadIfSmallTeeResult {
|
|
|
124
125
|
content: string;
|
|
125
126
|
/** Set only when tee was actually written (file size > 2× threshold + write succeeded). */
|
|
126
127
|
fullOutputPath?: string;
|
|
128
|
+
/** R10-1 (dep-context cache tee-safety): character length of the RAW file
|
|
129
|
+
* content BEFORE the truncation pipeline ran. Populated on every
|
|
130
|
+
* successful read; lets callers tell whether a cached no-tee read
|
|
131
|
+
* (`readIfSmall`) would have teed
|
|
132
|
+
* (length > TEE_THRESHOLD_MULTIPLIER × MAX_RESULT_INLINE_BYTES) without
|
|
133
|
+
* re-reading the file — no heuristics on the truncated body. */
|
|
134
|
+
originalLength?: number;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
/**
|
|
@@ -211,9 +219,11 @@ export function readIfSmallWithTee(
|
|
|
211
219
|
},
|
|
212
220
|
}),
|
|
213
221
|
]);
|
|
214
|
-
return fullOutputPath
|
|
222
|
+
return fullOutputPath
|
|
223
|
+
? { content: result.text, fullOutputPath, originalLength: content.length }
|
|
224
|
+
: { content: result.text, originalLength: content.length };
|
|
215
225
|
}
|
|
216
|
-
return { content };
|
|
226
|
+
return { content, originalLength: content.length };
|
|
217
227
|
} catch {
|
|
218
228
|
return undefined;
|
|
219
229
|
}
|
|
@@ -401,6 +411,12 @@ export function collectDependencyOutputContext(
|
|
|
401
411
|
tasks: TeamTaskState[],
|
|
402
412
|
task: TeamTaskState,
|
|
403
413
|
step: WorkflowStep,
|
|
414
|
+
/** R10-1 residual: per-run result-artifact read cache (same instance the
|
|
415
|
+
* closeout aggregation uses — created once in executeTeamRunCore, threaded
|
|
416
|
+
* SchedulerContext → baseInput → TaskRunnerInput). Optional: undefined
|
|
417
|
+
* keeps the previous uncached per-dep readIfSmallWithTee behavior
|
|
418
|
+
* byte-for-byte. NEVER pass a per-call cache instance. */
|
|
419
|
+
cache?: ResultArtifactReadCache,
|
|
404
420
|
): DependencyOutputContext {
|
|
405
421
|
const byStep = new Map(tasks.map((item) => [item.stepId, item]).filter((entry): entry is [string, TeamTaskState] => Boolean(entry[0])));
|
|
406
422
|
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
@@ -424,11 +440,19 @@ export function collectDependencyOutputContext(
|
|
|
424
440
|
.filter((item): item is TeamTaskState => Boolean(item))
|
|
425
441
|
.map((item) => {
|
|
426
442
|
const fullOutputPath = item.resultArtifact ? teePathForArtifact(manifest.artifactsRoot, task.id, item.id) : undefined;
|
|
443
|
+
// R10-1 residual: route the dep result read through the same per-run
|
|
444
|
+
// cache the closeout aggregation populated (readTaskResultArtifactWithTee).
|
|
445
|
+
// Without a cache this issues the identical readIfSmallWithTee call as
|
|
446
|
+
// before (byte-identical output); with one, a non-tee-band hit returns
|
|
447
|
+
// the memoized body while tee-band truncations still do the real read
|
|
448
|
+
// (tee file + fullOutputPath preserved per consumer).
|
|
427
449
|
const teeResult = item.resultArtifact
|
|
428
|
-
?
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
450
|
+
? readTaskResultArtifactWithTee(
|
|
451
|
+
item.resultArtifact,
|
|
452
|
+
manifest.artifactsRoot,
|
|
453
|
+
fullOutputPath ? { fullOutputPath } : undefined,
|
|
454
|
+
cache,
|
|
455
|
+
)
|
|
432
456
|
: undefined;
|
|
433
457
|
const resultText = teeResult?.content;
|
|
434
458
|
const inlineBytes = resultText?.length ?? 0;
|
|
@@ -570,12 +594,209 @@ export function writeTaskInputsArtifact(
|
|
|
570
594
|
});
|
|
571
595
|
}
|
|
572
596
|
|
|
573
|
-
|
|
597
|
+
/**
|
|
598
|
+
* R10-1: outcome of one aggregated result-artifact read (the pair of disk
|
|
599
|
+
* ops {@link aggregateTaskOutputs} performs per task: readIfSmall +
|
|
600
|
+
* containedExists). Cached as a unit so a cache hit replaces BOTH ops.
|
|
601
|
+
*/
|
|
602
|
+
export interface ResultArtifactReadOutcome {
|
|
603
|
+
/** Truncated body from readIfSmall; undefined when the read failed/missing. */
|
|
604
|
+
body: string | undefined;
|
|
605
|
+
/** containedExists() result for the same artifact path. */
|
|
606
|
+
exists: boolean;
|
|
607
|
+
/** R10-1 (dep-context tee-safety): character length of the raw file content
|
|
608
|
+
* at miss time. Present whenever the read succeeded; used by the
|
|
609
|
+
* dependency-context seam to decide whether a cache hit is byte-identical
|
|
610
|
+
* (no tee would have been written) or must fall through to a real
|
|
611
|
+
* readIfSmallWithTee. Undefined when the read failed. */
|
|
612
|
+
originalLength?: number;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* R10-1: per-run memoization for result-artifact reads inside
|
|
617
|
+
* {@link aggregateTaskOutputs} (batch summary + group-join both aggregate the
|
|
618
|
+
* SAME settled batch every closeout — the second aggregation re-reads each
|
|
619
|
+
* `results/<taskId>.txt` from disk for no benefit).
|
|
620
|
+
*
|
|
621
|
+
* Cache key = artifact path + descriptor identity (`sizeBytes|contentHash`).
|
|
622
|
+
* writeArtifact computes both on every write (STATE-9: in-memory,
|
|
623
|
+
* post-redaction), so a retry that rewrites `results/<taskId>.txt` produces a
|
|
624
|
+
* NEW descriptor → new key → automatic miss. Descriptors missing BOTH fields
|
|
625
|
+
* bypass the cache entirely (cannot distinguish a rewrite from unchanged
|
|
626
|
+
* content). "Missing" outcomes are cached too (result artifacts use the
|
|
627
|
+
* default `retention: "run"` and are not pruned mid-run, so a miss is stable
|
|
628
|
+
* within a run). Path is used as-is (producer/taskId-keyed, stable absolute
|
|
629
|
+
* path from the manifest — no per-lookup realpath needed).
|
|
630
|
+
*
|
|
631
|
+
* Lifetime: one run (closure-level in executeTeamRunCore) — no cross-run
|
|
632
|
+
* leakage, no invalidation beyond the descriptor-identity key.
|
|
633
|
+
*
|
|
634
|
+
* Env bypass: `PI_CREW_DISABLE_RESULT_READ_CACHE=1` (read ONCE at creation)
|
|
635
|
+
* returns a cache whose lookups always miss — control mode for benches/tests
|
|
636
|
+
* that need the uncached behavior while still routing through the counting
|
|
637
|
+
* wrapper below.
|
|
638
|
+
*/
|
|
639
|
+
export interface ResultArtifactReadCache {
|
|
640
|
+
/** Cached outcome for the artifact identity, or undefined on miss/bypass. */
|
|
641
|
+
lookup(descriptor: ArtifactDescriptor): ResultArtifactReadOutcome | undefined;
|
|
642
|
+
/** Store an outcome. No-op when the descriptor lacks identity fields. */
|
|
643
|
+
store(descriptor: ArtifactDescriptor, outcome: ResultArtifactReadOutcome): void;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* R10-1 test/bench counters for the result-artifact read path. Track disk ops
|
|
648
|
+
* ACTUALLY ISSUED through {@link readTaskResultArtifact} (incremented in the
|
|
649
|
+
* miss branch right before the real readIfSmall/containedExists calls) plus
|
|
650
|
+
* cache hits/misses. The disabled-control mode still routes through the
|
|
651
|
+
* counting wrapper (map simply not consulted) so cached-vs-uncached
|
|
652
|
+
* comparisons are honest.
|
|
653
|
+
*/
|
|
654
|
+
export interface ResultArtifactReadStats {
|
|
655
|
+
readFile: number;
|
|
656
|
+
exists: number;
|
|
657
|
+
hits: number;
|
|
658
|
+
misses: number;
|
|
659
|
+
reset(): void;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export const __test__resultReadStats: ResultArtifactReadStats = {
|
|
663
|
+
readFile: 0,
|
|
664
|
+
exists: 0,
|
|
665
|
+
hits: 0,
|
|
666
|
+
misses: 0,
|
|
667
|
+
reset() {
|
|
668
|
+
this.readFile = 0;
|
|
669
|
+
this.exists = 0;
|
|
670
|
+
this.hits = 0;
|
|
671
|
+
this.misses = 0;
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
function resultArtifactCacheKey(descriptor: ArtifactDescriptor): string | undefined {
|
|
676
|
+
if (descriptor.sizeBytes === undefined && descriptor.contentHash === undefined) return undefined;
|
|
677
|
+
return `${descriptor.path}|${descriptor.sizeBytes ?? ""}|${descriptor.contentHash ?? ""}`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function createResultArtifactReadCache(): ResultArtifactReadCache {
|
|
681
|
+
// Read once at creation — the bypass decision is fixed for the cache's
|
|
682
|
+
// (per-run) lifetime, matching PI_CREW_USE_BUNDLE-style one-shot env reads.
|
|
683
|
+
if (getCrewEnv("PI_CREW_DISABLE_RESULT_READ_CACHE") === "1") {
|
|
684
|
+
return {
|
|
685
|
+
lookup: () => undefined,
|
|
686
|
+
store: () => undefined,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const entries = new Map<string, ResultArtifactReadOutcome>();
|
|
690
|
+
return {
|
|
691
|
+
lookup(descriptor) {
|
|
692
|
+
const key = resultArtifactCacheKey(descriptor);
|
|
693
|
+
if (key === undefined) return undefined;
|
|
694
|
+
const hit = entries.get(key);
|
|
695
|
+
if (hit === undefined) {
|
|
696
|
+
__test__resultReadStats.misses += 1;
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
__test__resultReadStats.hits += 1;
|
|
700
|
+
return hit;
|
|
701
|
+
},
|
|
702
|
+
store(descriptor, outcome) {
|
|
703
|
+
const key = resultArtifactCacheKey(descriptor);
|
|
704
|
+
if (key !== undefined) entries.set(key, outcome);
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Single read seam for {@link aggregateTaskOutputs}. Without a cache this is
|
|
711
|
+
* byte-for-byte the old behavior: readIfSmall + containedExists issued in the
|
|
712
|
+
* same order. With a cache, a hit returns the memoized pair (zero disk ops);
|
|
713
|
+
* a miss issues the same disk ops as the uncached path, counts them, and
|
|
714
|
+
* memoizes the outcome.
|
|
715
|
+
*/
|
|
716
|
+
function readTaskResultArtifact(
|
|
717
|
+
descriptor: ArtifactDescriptor,
|
|
718
|
+
baseDir: string | undefined,
|
|
719
|
+
cache: ResultArtifactReadCache | undefined,
|
|
720
|
+
): ResultArtifactReadOutcome {
|
|
721
|
+
const cached = cache?.lookup(descriptor);
|
|
722
|
+
if (cached !== undefined) return cached;
|
|
723
|
+
__test__resultReadStats.readFile += 1;
|
|
724
|
+
__test__resultReadStats.exists += 1;
|
|
725
|
+
// readIfSmall is a thin wrapper over readIfSmallWithTee(...).content — the
|
|
726
|
+
// direct call is byte-identical and also captures originalLength (R10-1
|
|
727
|
+
// tee-safety metadata consumed by readTaskResultArtifactWithTee).
|
|
728
|
+
const raw = readIfSmallWithTee(descriptor.path, { baseDir });
|
|
729
|
+
const outcome: ResultArtifactReadOutcome = {
|
|
730
|
+
body: raw?.content,
|
|
731
|
+
exists: containedExists(descriptor.path, baseDir),
|
|
732
|
+
...(raw?.originalLength !== undefined ? { originalLength: raw.originalLength } : {}),
|
|
733
|
+
};
|
|
734
|
+
cache?.store(descriptor, outcome);
|
|
735
|
+
return outcome;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* R10-1 residual: cached read seam for the dependency-context path
|
|
740
|
+
* ({@link collectDependencyOutputContext}). Mirrors {@link readTaskResultArtifact}
|
|
741
|
+
* while preserving the tee-recovery semantics of the direct
|
|
742
|
+
* `readIfSmallWithTee(path, { tee })` call it replaces:
|
|
743
|
+
*
|
|
744
|
+
* - A cache hit is reusable ONLY when the memoized read proves the uncached
|
|
745
|
+
* call would NOT have teed: `originalLength` is known AND
|
|
746
|
+
* `originalLength <= TEE_THRESHOLD_MULTIPLIER × MAX_RESULT_INLINE_BYTES`.
|
|
747
|
+
* In that band readIfSmallWithTee().content is byte-identical to the
|
|
748
|
+
* memoized body (same file, same truncation pipeline) and fullOutputPath is
|
|
749
|
+
* unset, so returning the memoized body cannot change the rendered prompt.
|
|
750
|
+
* - A memoized read that FAILED (body undefined) is also returned as-is: the
|
|
751
|
+
* direct readIfSmallWithTee would have thrown and returned undefined for
|
|
752
|
+
* the same descriptor identity.
|
|
753
|
+
* - Otherwise (miss, bypass, or tee-band truncation) the real
|
|
754
|
+
* readIfSmallWithTee runs — WITH tee, so the per-consumer
|
|
755
|
+
* `tee/<taskId>-<depId>.full.txt` write and `fullOutputPath` survive — and
|
|
756
|
+
* the disk ops are counted in `__test__resultReadStats` exactly like
|
|
757
|
+
* readTaskResultArtifact's miss branch (honest cached-vs-bypass benches).
|
|
758
|
+
* When a cache is provided the miss ALSO populates it with the SAME
|
|
759
|
+
* outcome readTaskResultArtifact would have stored (body = teeResult.content
|
|
760
|
+
* — byte-identical to readIfSmall's output since tee never alters content —
|
|
761
|
+
* exists via containedExists, originalLength), so the next dep collect /
|
|
762
|
+
* closeout aggregation hits. Truncation checks in later consumers use
|
|
763
|
+
* originalLength, never heuristics on the truncated body.
|
|
764
|
+
*/
|
|
765
|
+
function readTaskResultArtifactWithTee(
|
|
766
|
+
descriptor: ArtifactDescriptor,
|
|
767
|
+
baseDir: string | undefined,
|
|
768
|
+
tee: TeeRecoveryOptions | undefined,
|
|
769
|
+
cache: ResultArtifactReadCache | undefined,
|
|
770
|
+
): ReadIfSmallTeeResult | undefined {
|
|
771
|
+
const cached = cache?.lookup(descriptor);
|
|
772
|
+
if (cached !== undefined) {
|
|
773
|
+
if (cached.body === undefined) return undefined;
|
|
774
|
+
if (cached.originalLength !== undefined && cached.originalLength <= MAX_RESULT_INLINE_BYTES * TEE_THRESHOLD_MULTIPLIER) {
|
|
775
|
+
return { content: cached.body };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
__test__resultReadStats.readFile += 1;
|
|
779
|
+
const result = readIfSmallWithTee(descriptor.path, { baseDir, ...(tee ? { tee } : {}) });
|
|
780
|
+
if (cache) {
|
|
781
|
+
// Populate with the outcome the closeout seam would have stored. The op
|
|
782
|
+
// order (read → existsSync) matches readTaskResultArtifact's miss branch.
|
|
783
|
+
__test__resultReadStats.exists += 1;
|
|
784
|
+
cache.store(descriptor, {
|
|
785
|
+
body: result?.content,
|
|
786
|
+
exists: containedExists(descriptor.path, baseDir),
|
|
787
|
+
...(result?.originalLength !== undefined ? { originalLength: result.originalLength } : {}),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function aggregateTaskOutputs(tasks: TeamTaskState[], manifest?: TeamRunManifest, cache?: ResultArtifactReadCache): string {
|
|
574
794
|
return tasks
|
|
575
795
|
.map((task, index) => {
|
|
576
|
-
const
|
|
796
|
+
const read = task.resultArtifact ? readTaskResultArtifact(task.resultArtifact, manifest?.artifactsRoot, cache) : undefined;
|
|
797
|
+
const body = read?.body;
|
|
577
798
|
const hasBody = Boolean(body?.trim());
|
|
578
|
-
const expectedMissing =
|
|
799
|
+
const expectedMissing = read ? !read.exists : undefined;
|
|
579
800
|
const status =
|
|
580
801
|
task.status === "skipped"
|
|
581
802
|
? "SKIPPED"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { freezeSpecSnapshot, loadSpecRecord } from "../state/stores/spec-store.ts";
|
|
3
|
+
import type { SpecRecord, TaskPacket, TaskScope, TeamRunManifest, VerificationContract } from "../state/types.ts";
|
|
3
4
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
5
|
import { generateTaskHashId } from "./task-id.ts";
|
|
5
6
|
|
|
@@ -70,6 +71,10 @@ export function sanitizeTaskText(task: string): string {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export interface BuildTaskPacketInput {
|
|
74
|
+
/** T4/R6 (ADR-6 §2): workspace spec ids — loaded + FROZEN into the packet. */
|
|
75
|
+
specRefs?: string[];
|
|
76
|
+
/** T4/R6 (ADR-6 §7): strict mode — coverage AND machine-check (§4). */
|
|
77
|
+
specStrict?: boolean;
|
|
73
78
|
manifest: TeamRunManifest;
|
|
74
79
|
step: WorkflowStep;
|
|
75
80
|
taskId: string;
|
|
@@ -98,6 +103,20 @@ export function defaultVerificationContract(step: WorkflowStep): VerificationCon
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
106
|
+
// T4/R6 (ADR-6 §1/§2): the ONLY packet-creation path is also the freeze
|
|
107
|
+
// point — specRefs load from the workspace store and freeze into immutable
|
|
108
|
+
// snapshots embedded in the packet (later spec edits never rewrite them).
|
|
109
|
+
// Round-1 review (fail-closed freeze): declared-but-unresolvable ids are
|
|
110
|
+
// kept on the packet as unresolvedSpecRefs instead of being dropped — the
|
|
111
|
+
// gate surfaces them (badge non-strict / failure strict), never a silent
|
|
112
|
+
// no-op. Snapshots freeze trust via the store at THIS moment.
|
|
113
|
+
const declaredSpecRefs = input.specRefs ?? [];
|
|
114
|
+
const resolvedRecords = declaredSpecRefs
|
|
115
|
+
.map((id) => loadSpecRecord(input.manifest.cwd, id))
|
|
116
|
+
.filter((r): r is SpecRecord => r !== undefined);
|
|
117
|
+
const specSnapshots = resolvedRecords.map((r) => freezeSpecSnapshot(r, input.manifest.cwd));
|
|
118
|
+
const unresolvedSpecRefs = declaredSpecRefs.filter((id) => !specSnapshots.some((s) => s.specId === id));
|
|
119
|
+
|
|
101
120
|
const scope = inferTaskScope(input.step);
|
|
102
121
|
const reads = input.step.reads === false ? [] : (input.step.reads ?? []);
|
|
103
122
|
const scopePath = reads.length === 1 ? reads[0] : reads.length > 1 ? reads.join(", ") : undefined;
|
|
@@ -112,6 +131,9 @@ export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
|
112
131
|
|
|
113
132
|
return {
|
|
114
133
|
objective: sanitizedTask.replaceAll("{goal}", sanitizedGoal),
|
|
134
|
+
...(declaredSpecRefs.length > 0 ? { specRefs: declaredSpecRefs, specSnapshots } : {}),
|
|
135
|
+
...(unresolvedSpecRefs.length > 0 ? { unresolvedSpecRefs } : {}),
|
|
136
|
+
...(input.specStrict === true ? { specStrict: true } : {}),
|
|
115
137
|
scope,
|
|
116
138
|
scopePath,
|
|
117
139
|
repo: path.basename(input.manifest.cwd) || input.manifest.cwd,
|