@mgiles/perk 3.1.0 → 3.3.0
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/README.md +5 -0
- package/extension/authoring/gist/draft.ts +198 -0
- package/extension/authoring/gist/prose.ts +46 -0
- package/extension/authoring/gist/review.ts +133 -0
- package/extension/authoring/gist/save.ts +118 -0
- package/extension/authoring/objective/draft.ts +345 -0
- package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
- package/extension/authoring/objective/planning.ts +124 -0
- package/extension/authoring/objective/prose.ts +103 -0
- package/extension/authoring/objective/review.ts +128 -0
- package/extension/authoring/objective/save.ts +224 -0
- package/extension/authoring/plan/draft.ts +84 -0
- package/extension/authoring/plan/prose.ts +41 -0
- package/extension/authoring/plan/review.ts +269 -0
- package/extension/authoring/plan/save.ts +256 -0
- package/extension/authoring/plan/source.ts +82 -0
- package/extension/authoring/refinement/context.ts +468 -0
- package/extension/authoring/refinement/draft.ts +261 -0
- package/extension/authoring/refinement/prose.ts +79 -0
- package/extension/authoring/refinement/review.ts +111 -0
- package/extension/authoring/refinement/save.ts +119 -0
- package/extension/authoring/review/approvalGate.ts +34 -0
- package/extension/authoring/review/draftContext.ts +68 -0
- package/extension/codeReview/automated.ts +352 -0
- package/extension/codeReview/submission.ts +229 -0
- package/extension/delivery/address.ts +295 -0
- package/extension/delivery/ci.ts +355 -0
- package/extension/delivery/commitCompact.ts +93 -0
- package/extension/delivery/conflictResolution.ts +247 -0
- package/extension/delivery/ready.ts +193 -0
- package/extension/delivery/stackConflict.ts +361 -0
- package/extension/delivery/stackObjective.ts +16 -0
- package/extension/delivery/stackReconcile.ts +165 -0
- package/extension/delivery/submit.ts +171 -0
- package/extension/index.ts +369 -359
- package/extension/learning/analystWave.ts +324 -0
- package/extension/learning/audit.ts +667 -0
- package/extension/learning/capture.ts +92 -0
- package/extension/learning/containment.ts +104 -0
- package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
- package/extension/learning/dreamAnalysis.ts +435 -0
- package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
- package/extension/{waves → learning}/dreamReport.ts +35 -31
- package/extension/learning/harvest.ts +491 -0
- package/extension/learning/prose.ts +66 -0
- package/extension/learning/routing.ts +79 -0
- package/extension/pi/v1/bashScanTimeout.ts +64 -0
- package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -310
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
- package/extension/pi/v1/codeReview/stack.ts +606 -0
- package/extension/pi/v1/codeReview/submit.ts +354 -0
- package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
- package/extension/pi/v1/contextEvidence.ts +80 -0
- package/extension/pi/v1/contextInjection.ts +207 -0
- package/extension/{doors → pi/v1/delivery}/address.ts +163 -265
- package/extension/pi/v1/delivery/ci.ts +570 -0
- package/extension/pi/v1/delivery/commitCompact.ts +201 -0
- package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
- package/extension/{doors → pi/v1/delivery}/land.ts +128 -60
- package/extension/pi/v1/delivery/ready.ts +322 -0
- package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
- package/extension/pi/v1/delivery/stackDrive.ts +120 -0
- package/extension/pi/v1/delivery/stackLand.ts +223 -0
- package/extension/pi/v1/delivery/stackRecover.ts +265 -0
- package/extension/pi/v1/delivery/stackStatus.ts +237 -0
- package/extension/pi/v1/delivery/stackSync.ts +658 -0
- package/extension/pi/v1/delivery/submit.ts +389 -0
- package/extension/pi/v1/delivery/submitConflict.ts +186 -0
- package/extension/pi/v1/draftReview.ts +431 -0
- package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
- package/extension/pi/v1/gist.ts +794 -0
- package/extension/pi/v1/learning/audit.ts +186 -0
- package/extension/pi/v1/learning/dream.ts +207 -0
- package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
- package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
- package/extension/pi/v1/learning/learn.ts +585 -0
- package/extension/{doors → pi/v1}/lifecycleGates.ts +14 -59
- package/extension/{factories → pi/v1}/objective.ts +53 -33
- package/extension/pi/v1/objectiveAuthoring.ts +672 -0
- package/extension/pi/v1/objectiveDreamGate.ts +160 -0
- package/extension/pi/v1/objectivePlanning.ts +776 -0
- package/extension/pi/v1/objectiveRefinement.ts +1320 -0
- package/extension/pi/v1/objectiveReview.ts +451 -0
- package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
- package/extension/pi/v1/plan.ts +812 -0
- package/extension/pi/v1/planReview.ts +820 -0
- package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
- package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
- package/extension/pi/v1/providers/plannotator.ts +487 -0
- package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +152 -34
- package/extension/pi/v1/providers/selection.ts +43 -0
- package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
- package/extension/pi/v1/review.ts +538 -0
- package/extension/pi/v1/reviewOutcome.ts +9 -0
- package/extension/pi/v1/scoutWave.ts +318 -0
- package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
- package/extension/session/branchWorkflowSession.ts +60 -0
- package/extension/session/lifecycle.ts +644 -0
- package/extension/session/lifecycleGates.ts +64 -0
- package/extension/session/saveDestination.ts +87 -0
- package/extension/session/workflowSession.ts +971 -0
- package/extension/substrate/agentScratch.ts +27 -54
- package/extension/substrate/bashScanTimeout.ts +181 -0
- package/extension/substrate/bindingDelivery.ts +38 -30
- package/extension/substrate/bindings.ts +4 -5
- package/extension/substrate/cache.ts +64 -12
- package/extension/substrate/childRestrictions.ts +39 -0
- package/extension/substrate/coldDoor.ts +17 -1
- package/extension/substrate/config.ts +157 -19
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +364 -0
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +279 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +191 -166
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +103 -48
- package/extension/waves/blockedReports.ts +59 -0
- package/extension/waves/draftReviewWave.ts +42 -42
- package/extension/waves/laneIdentity.ts +77 -0
- package/extension/waves/objectiveExplorerWave.ts +24 -24
- package/extension/waves/prReviewWave.ts +89 -77
- package/extension/waves/reportWave.ts +438 -578
- package/extension/waves/reviewClassifierWave.ts +22 -22
- package/extension/waves/rpcAdapter.ts +100 -15
- package/extension/waves/scoutWave.ts +192 -0
- package/extension/waves/transport.ts +480 -0
- package/extension/worker/sdkAdapter.ts +494 -0
- package/extension/worker/stageExecution.ts +679 -0
- package/extension/workerMain.ts +18 -19
- package/package.json +6 -4
- package/prompts/_fixtures/live.yaml +98 -10
- package/prompts/contexts/adapters/plannotator-gist.md +6 -0
- package/prompts/contexts/adapters/plannotator-objective.md +6 -0
- package/prompts/contexts/adapters/plannotator-plan.md +8 -1
- package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -0
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- package/prompts/stages/objective-plan/guidance.md +2 -2
- package/prompts/stages/objective-plan/seed.md +9 -1
- package/prompts/stages/objective-reconcile-ready.md +7 -0
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-refine/seed.md +18 -0
- package/prompts/stages/objective-review-browser.md +4 -4
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +4 -4
- package/prompts/stages/pr-review-browser/active.md +3 -4
- package/prompts/stages/pr-review-browser/foreign.md +3 -4
- package/prompts/stages/pr-review-terminal/active.md +3 -3
- package/prompts/stages/pr-review-terminal/foreign.md +3 -3
- package/prompts/stages/pr-review.md +3 -3
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +22 -0
- package/shared/README.md +8 -3
- package/shared/bindings.yaml +6 -3
- package/shared/contracts.md +4499 -2147
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +29 -1
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
- package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
- package/extension/adapters/planAdapterPlannotator.ts +0 -362
- package/extension/doors/auditWaveTools.ts +0 -352
- package/extension/doors/ciExecutor.ts +0 -756
- package/extension/doors/commitCompact.ts +0 -251
- package/extension/doors/dreamWaveTools.ts +0 -475
- package/extension/doors/learn.ts +0 -655
- package/extension/doors/objectiveStack.ts +0 -1143
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -87
- package/extension/doors/submit.ts +0 -347
- package/extension/doors/submitPrReview.ts +0 -408
- package/extension/factories/gistAuthor.ts +0 -94
- package/extension/factories/gistDraft.ts +0 -265
- package/extension/factories/gistSave.ts +0 -251
- package/extension/factories/implementHere.ts +0 -116
- package/extension/factories/objectiveAuthor.ts +0 -98
- package/extension/factories/objectiveDraft.ts +0 -466
- package/extension/factories/objectivePlan.ts +0 -975
- package/extension/factories/objectiveSave.ts +0 -363
- package/extension/factories/planDraft.ts +0 -140
- package/extension/factories/planMode.ts +0 -205
- package/extension/factories/planReview.ts +0 -1237
- package/extension/factories/planSave.ts +0 -604
- package/extension/factories/planTitle.ts +0 -141
- package/extension/substrate/structuredOutput.ts +0 -202
- package/extension/waves/auditWave.ts +0 -312
- package/extension/waves/harvestWave.ts +0 -399
- package/extension/waves/learnWave.ts +0 -155
- package/extension/waves/memoryAdapter.ts +0 -139
- package/extension/waves/prReviewDynamicWave.ts +0 -777
- package/extension/worker/readOnlySession.ts +0 -294
- package/extension/worker/worker.ts +0 -899
- package/prompts/stages/pr-review-dynamic.md +0 -7
- package/shared/contracts-history.md +0 -605
|
@@ -1,56 +1,88 @@
|
|
|
1
1
|
// The Perk-owned report-wave module: bounded sets of fresh-context, report-only children with
|
|
2
|
-
// typed outcomes under stable
|
|
3
|
-
// mechanics (a script skeleton the parent model had to transcribe faithfully — the known
|
|
2
|
+
// typed outcomes under stable assignment keys. Report waves were previously model-authored
|
|
3
|
+
// prompt mechanics (a script skeleton the parent model had to transcribe faithfully — the known
|
|
4
4
|
// prompt-drift risk); this module makes the mechanics CODE. It renders the complete, tested
|
|
5
5
|
// `workflowScript`, launches it through a `WaveAdapter` (async-only, `mission: false`), waits on
|
|
6
6
|
// the run's async-complete event with a module-owned timeout, reads the durable `status.json`
|
|
7
7
|
// `workflow.value` aggregate, and normalizes `{complete, reports[], failures[]}` under a
|
|
8
8
|
// flow-specific completeness policy. Each launch additionally records an OUTPUT-FREE
|
|
9
9
|
// `WaveScriptReceipt` (run handle + per-child identity/artifact trail from the completion
|
|
10
|
-
// payload) —
|
|
11
|
-
//
|
|
12
|
-
// selection (
|
|
10
|
+
// payload) — correlation telemetry, not policy input. Complete runs use the durable aggregate;
|
|
11
|
+
// explicit native partial settlement may retain keyed child reports, always beside its wave
|
|
12
|
+
// failure. Receipt absence never changes verdict/completeness or retry selection (§8.35).
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// await. `startReportWave`/`runReportWave` are the lane-level pair over the same split. The
|
|
20
|
-
// blocking runner is live under the per-flow entrypoints (`prReviewWave.ts`, `learnWave.ts`,
|
|
21
|
-
// `prReviewDynamicWave.ts`); the streaming sibling serves flows whose parent must return from
|
|
22
|
-
// the launch and hold a model-held `subagent_wait` relay loop open (`adversarialReviewWave.ts`,
|
|
23
|
-
// behind the `start_review_wave`/`collect_review_wave` pair).
|
|
14
|
+
// This is the LOGICAL tier: assignments (`ReportAssignment`), preflight partitioning,
|
|
15
|
+
// aggregate normalization, and the completeness policy. The TRANSPORT tier — the adapter seam,
|
|
16
|
+
// receipt primitives, and the script runner — lives in `transport.ts`, imported one-directionally
|
|
17
|
+
// from here; the script text itself (`renderWaveScript`) is module-private, so nothing outside
|
|
18
|
+
// `waves/` can observe or operate on transport.
|
|
24
19
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
20
|
+
// THE CALLER SURFACE IS THE OPAQUE `ReportWave` LIFECYCLE: `start` launches non-blocking and
|
|
21
|
+
// returns an opaque `ReportWaveRef` (plus identity telemetry — never an operable handle),
|
|
22
|
+
// `collect` drains a started wave's settled outcome under the module-owned grace, and `run` is
|
|
23
|
+
// start + await (the blocking form). Callers supply assignments and consume typed outcomes —
|
|
24
|
+
// never adapters, run handles, or result promises. Pending execution is WAVE-OWNED: each
|
|
25
|
+
// instance holds its launched-but-uncollected records in an instance-owned WeakMap keyed by ref
|
|
26
|
+
// (a foreign instance's ref collects `"none"` structurally), and a settled collect's
|
|
27
|
+
// delete-as-claim makes drain-once exact even under overlapping collectors. The blocking form
|
|
28
|
+
// serves the per-flow entrypoints (`prReviewWave.ts` and the typed feature ops in `learning/`);
|
|
29
|
+
// the streaming split serves flows whose parent ends the launch turn and relays provisional
|
|
30
|
+
// batches on native wakes (`adversarialReviewWave.ts`, `draftReviewWave.ts`).
|
|
28
31
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
32
|
+
// The module owns ADAPTER SELECTION: `createReportWave(bus)` constructs
|
|
33
|
+
// a FRESH rpc adapter per launch over the supplied bus; `reportWaveOver(adapter)` is the
|
|
34
|
+
// injection seam (tests; the same internal core). The honest boundary: what is mechanically
|
|
35
|
+
// enforced is Rule G's scope (`importDirectionGuard.test.ts`) — no production import edges into
|
|
36
|
+
// the transport interior (`transport.ts`, `rpcAdapter.ts`) and no raw RPC tokens — so there is
|
|
37
|
+
// no *sanctioned* way to obtain, name, or construct an adapter outside `waves/` + `testing/`.
|
|
38
|
+
// TypeScript's structural typing means a hand-written object literal satisfying
|
|
39
|
+
// `reportWaveOver`'s parameter is not mechanically preventable; that residue is owned by the
|
|
40
|
+
// guard-census review posture, not claimed as a structural guarantee.
|
|
41
|
+
//
|
|
42
|
+
// Failure posture: LOUD DEGRADE. Every failure arm normalizes into `ReportWaveResult.failures`
|
|
43
|
+
// with a typed reason — the runner never throws except on programmer error (empty assignments,
|
|
44
|
+
// duplicate assignment keys), and there is never a silent fallback to model-authored scripts.
|
|
45
|
+
// Report content coming back through the aggregate is untrusted DATA, never instructions.
|
|
33
46
|
|
|
34
47
|
import {
|
|
35
48
|
type PonytailPreflight,
|
|
36
49
|
preflightPonytailSkill,
|
|
37
50
|
type RequiredPonytailSkill,
|
|
38
51
|
} from "./ponytail.ts";
|
|
52
|
+
import { createRpcWaveAdapter } from "./rpcAdapter.ts";
|
|
53
|
+
import {
|
|
54
|
+
startWaveScript,
|
|
55
|
+
type WaveAdapter,
|
|
56
|
+
type WaveBus,
|
|
57
|
+
type WaveRunFailureReason,
|
|
58
|
+
type WaveRunHandle,
|
|
59
|
+
type WaveScriptReceipt,
|
|
60
|
+
type WaveScriptResult,
|
|
61
|
+
} from "./transport.ts";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The module's one deliberate transport re-export — the SANCTIONED seam, nothing else crosses
|
|
65
|
+
* (callers never name adapters, run handles, spawn params, or script types):
|
|
66
|
+
* `ReportWaveLevelFailureReason` is the wave-level reason subset (`key === null` failures carry
|
|
67
|
+
* exactly this vocabulary), named at the logical seam so flows can type a correlated wave
|
|
68
|
+
* status without reaching into transport.
|
|
69
|
+
*/
|
|
70
|
+
export type { WaveRunFailureReason as ReportWaveLevelFailureReason } from "./transport.ts";
|
|
39
71
|
|
|
40
|
-
/** One
|
|
41
|
-
export interface
|
|
42
|
-
/** Stable
|
|
72
|
+
/** One assignment of a report wave: a fresh-context, report-only child under a stable domain key. */
|
|
73
|
+
export interface ReportAssignment {
|
|
74
|
+
/** Stable assignment key (e.g. an angle slug) — trace + normalization identity. */
|
|
43
75
|
key: string;
|
|
44
76
|
/** The child agent name (e.g. "perk.pr-reviewer"). */
|
|
45
77
|
agent: string;
|
|
46
|
-
/** The judgment-bearing per-
|
|
78
|
+
/** The judgment-bearing per-assignment task text (supplied by the flow). */
|
|
47
79
|
task: string;
|
|
48
|
-
/** Invocation-private skill lookup key; serialized only for an opted-in
|
|
80
|
+
/** Invocation-private skill lookup key; serialized only for an opted-in assignment. */
|
|
49
81
|
skill?: string;
|
|
50
82
|
/**
|
|
51
83
|
* Exact source requirement for a source-bound skill. This metadata is preflight-only and is
|
|
52
|
-
* NEVER serialized into the workflow script; a failed requirement skips this
|
|
53
|
-
* allowing pi-subagents to resolve a hostile same-named global/project skill.
|
|
84
|
+
* NEVER serialized into the workflow script; a failed requirement skips this assignment
|
|
85
|
+
* instead of allowing pi-subagents to resolve a hostile same-named global/project skill.
|
|
54
86
|
*/
|
|
55
87
|
requiredSkill?: RequiredPonytailSkill;
|
|
56
88
|
/** Trace metadata; defaults to `key`. */
|
|
@@ -58,8 +90,8 @@ export interface WaveLane {
|
|
|
58
90
|
/** Trace metadata. */
|
|
59
91
|
phase?: string;
|
|
60
92
|
/**
|
|
61
|
-
* Per-
|
|
62
|
-
* workflow-level default (the established per-item mechanic). Omitted
|
|
93
|
+
* Per-assignment report schema — rendered as the item's `outputSchema`, overriding the
|
|
94
|
+
* workflow-level default (the established per-item mechanic). Omitted assignments render
|
|
63
95
|
* byte-identically to before the field existed.
|
|
64
96
|
*/
|
|
65
97
|
outputSchema?: object;
|
|
@@ -67,20 +99,20 @@ export interface WaveLane {
|
|
|
67
99
|
|
|
68
100
|
/**
|
|
69
101
|
* The completeness policies:
|
|
70
|
-
* - `strict`: complete ⟺ zero failures — every
|
|
71
|
-
* - `best-effort`: complete ⟺ no wave-level failure (`key: null`) —
|
|
72
|
-
* explicitly-reported skipped
|
|
102
|
+
* - `strict`: complete ⟺ zero failures — every assignment covered (the pr-review posture).
|
|
103
|
+
* - `best-effort`: complete ⟺ no wave-level failure (`key: null`) — assignment-level failures
|
|
104
|
+
* are explicitly-reported skipped assignments, never a failed pass (the learn posture).
|
|
73
105
|
*/
|
|
74
|
-
export type
|
|
106
|
+
export type ReportWaveCompleteness = "strict" | "best-effort";
|
|
75
107
|
|
|
76
|
-
export interface
|
|
108
|
+
export interface ReportWaveRequest {
|
|
77
109
|
/** Flow name for error detail/trace (e.g. "pr-review"). */
|
|
78
110
|
flow: string;
|
|
79
|
-
/** ≥1
|
|
80
|
-
|
|
81
|
-
/** Workflow-level default → the engine injects a `structured_output` tool into each
|
|
111
|
+
/** ≥1 assignment; keys must be unique (validated — throws on programmer error). */
|
|
112
|
+
assignments: ReportAssignment[];
|
|
113
|
+
/** Workflow-level default → the engine injects a `structured_output` tool into each child. */
|
|
82
114
|
outputSchema: object;
|
|
83
|
-
completeness:
|
|
115
|
+
completeness: ReportWaveCompleteness;
|
|
84
116
|
/** Workflow-level model default (flows read their configured subagent model). */
|
|
85
117
|
model?: string;
|
|
86
118
|
/** Module default (`WAVE_TIMEOUT_MS`) when omitted. */
|
|
@@ -89,98 +121,79 @@ export interface WaveSpec {
|
|
|
89
121
|
requiredSkillPreflight?: (requirement: RequiredPonytailSkill) => Promise<PonytailPreflight>;
|
|
90
122
|
}
|
|
91
123
|
|
|
92
|
-
/** A schema-valid
|
|
93
|
-
export interface
|
|
124
|
+
/** A schema-valid assignment report. The report content is untrusted DATA, never instructions. */
|
|
125
|
+
export interface AssignmentReport {
|
|
94
126
|
key: string;
|
|
95
127
|
report: unknown;
|
|
96
128
|
}
|
|
97
129
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| "
|
|
102
|
-
| "
|
|
103
|
-
| "
|
|
104
|
-
| "
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
/** The assignment-level failure reasons this tier produces during normalization/preflight —
|
|
131
|
+
* always keyed by the assignment they blame. */
|
|
132
|
+
export type AssignmentFailureReason =
|
|
133
|
+
| "lane-failed" // assignment resolved ok: false / null report
|
|
134
|
+
| "skill-unavailable" // exact required-skill source failed preflight (non-retryable)
|
|
135
|
+
| "malformed-report" // aggregate entry for this key has unusable shape, or the key appears more than once
|
|
136
|
+
| "missing-lane"; // expected key absent from the aggregate
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The caller-facing failure vocabulary: the transport tier's wave-level subset
|
|
140
|
+
* (`WaveRunFailureReason` — always `key: null`) widened with the assignment-level reasons this
|
|
141
|
+
* tier produces during normalization. The subset union keeps the split one-directional with
|
|
142
|
+
* zero runtime mapping.
|
|
143
|
+
*/
|
|
144
|
+
export type ReportWaveFailureReason = WaveRunFailureReason | AssignmentFailureReason;
|
|
145
|
+
|
|
146
|
+
/** A wave-level failure: the whole run failed, so there is no assignment to blame — the same
|
|
147
|
+
* record shape as the transport tier's `WaveRunFailure` (script failures flow upward with zero
|
|
148
|
+
* runtime mapping). */
|
|
149
|
+
export interface ReportWaveLevelFailure {
|
|
150
|
+
key: null;
|
|
151
|
+
reason: WaveRunFailureReason;
|
|
114
152
|
/** Human-readable diagnosis (error strings routed here, never re-thrown). */
|
|
115
153
|
detail: string;
|
|
116
154
|
}
|
|
117
155
|
|
|
118
|
-
|
|
156
|
+
/** An assignment-level failure, keyed by the assignment it blames. */
|
|
157
|
+
export interface AssignmentFailure {
|
|
158
|
+
key: string;
|
|
159
|
+
reason: AssignmentFailureReason;
|
|
160
|
+
detail: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One wave failure — a DISCRIMINATED union on `key`: `key === null` narrows the reason to
|
|
165
|
+
* exactly the wave-level subset (and a string key to the assignment-level reasons), so a
|
|
166
|
+
* wave-level failure carrying an assignment reason — or vice versa — is unrepresentable and
|
|
167
|
+
* every flow inherits the correlation without reimplementing transport knowledge.
|
|
168
|
+
*/
|
|
169
|
+
export type ReportWaveFailure = ReportWaveLevelFailure | AssignmentFailure;
|
|
170
|
+
|
|
171
|
+
export interface ReportWaveResult {
|
|
119
172
|
complete: boolean;
|
|
120
|
-
reports:
|
|
121
|
-
failures:
|
|
173
|
+
reports: AssignmentReport[];
|
|
174
|
+
failures: ReportWaveFailure[];
|
|
122
175
|
/** The launch's output-free attempt receipt — write-only telemetry, never a decision input. */
|
|
123
176
|
receipt: WaveScriptReceipt;
|
|
124
177
|
}
|
|
125
178
|
|
|
126
|
-
/** The truthful preflight partition reported by every
|
|
127
|
-
export type
|
|
128
|
-
/** The complete logical
|
|
179
|
+
/** The truthful preflight partition reported by every report-wave start. */
|
|
180
|
+
export type ReportWaveLaunchManifest = {
|
|
181
|
+
/** The complete logical assignment manifest, in `request.assignments` order. */
|
|
129
182
|
requested: string[];
|
|
130
183
|
/** The ordered subset rendered into the static workflow after required-skill preflight. */
|
|
131
184
|
runnable: string[];
|
|
132
|
-
/** One ordered keyed `skill-unavailable` failure per preflight-omitted
|
|
133
|
-
preflightFailures:
|
|
185
|
+
/** One ordered keyed `skill-unavailable` failure per preflight-omitted assignment. */
|
|
186
|
+
preflightFailures: ReportWaveFailure[];
|
|
134
187
|
};
|
|
135
188
|
|
|
136
189
|
// -------------------------------------------------------------------- the attempt receipts
|
|
137
190
|
|
|
138
|
-
/**
|
|
139
|
-
* The terminal disposition of ONE top-level workflow launch, as the runner observed it. Every
|
|
140
|
-
* launch reaches exactly one of these arms (`"running"` is unreachable — the runner always
|
|
141
|
-
* settles); `"unavailable"` preserves even a pre-spawn capability failure as an attempt.
|
|
142
|
-
*/
|
|
143
|
-
export type WaveReceiptState =
|
|
144
|
-
| "unavailable" // ping failed/incomplete — nothing launched
|
|
145
|
-
| "spawn-failed" // spawn rejected/threw — no run handle
|
|
146
|
-
| "complete" // completion observed, durable state "complete"
|
|
147
|
-
| "failed" // completion observed, durable/observed failure
|
|
148
|
-
| "timed-out" // module timeout expired (handle preserved)
|
|
149
|
-
| "cancelled"; // AbortSignal honored (handle preserved when spawned)
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* One child lane's identity/artifact trail from the completion payload — OUTPUT-FREE by
|
|
153
|
-
* invariant: reports, summaries, and structured output never enter a receipt (they stay in the
|
|
154
|
-
* durable `status.json.workflow.value`, the sole report authority).
|
|
155
|
-
*/
|
|
156
|
-
export interface WaveChildReceipt {
|
|
157
|
-
/** The Perk lane key (mapped FROM the upstream row's overloaded `agent` field). */
|
|
158
|
-
key: string;
|
|
159
|
-
/** The child agent name, enriched from the Perk-owned lane spec where known. */
|
|
160
|
-
agent?: string;
|
|
161
|
-
/** The child's opaque run id — never parsed or synthesized from paths. */
|
|
162
|
-
runId?: string;
|
|
163
|
-
success?: boolean;
|
|
164
|
-
outputState?: "present" | "absent" | "unknown";
|
|
165
|
-
/** String path fields only; output-free. */
|
|
166
|
-
artifactPaths?: Record<string, string>;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** One script launch's receipt: the run handle (where known) + the observed children. */
|
|
170
|
-
export interface WaveScriptReceipt {
|
|
171
|
-
/** The top-level async run id (the spawn handle's asyncId). */
|
|
172
|
-
runId?: string;
|
|
173
|
-
asyncDir?: string;
|
|
174
|
-
state: WaveReceiptState;
|
|
175
|
-
children: WaveChildReceipt[];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
191
|
/**
|
|
179
192
|
* A flow-attributed attempt: one receipt per top-level workflow launch, ordered by `attempt`
|
|
180
193
|
* (one-based, assigned by the flow entrypoint that owns retry policy). `requestedKeys` is the
|
|
181
|
-
*
|
|
194
|
+
* assignment manifest BEFORE launch — never reconstructed from the observed children.
|
|
182
195
|
*/
|
|
183
|
-
export interface
|
|
196
|
+
export interface ReportWaveAttemptReceipt extends WaveScriptReceipt {
|
|
184
197
|
flow: string;
|
|
185
198
|
attempt: number;
|
|
186
199
|
requestedKeys: string[];
|
|
@@ -190,91 +203,12 @@ export interface WaveAttemptReceipt extends WaveScriptReceipt {
|
|
|
190
203
|
export function toAttemptReceipt(
|
|
191
204
|
flow: string,
|
|
192
205
|
attempt: number,
|
|
193
|
-
requestedKeys: string[],
|
|
206
|
+
requestedKeys: readonly string[],
|
|
194
207
|
receipt: WaveScriptReceipt,
|
|
195
|
-
):
|
|
208
|
+
): ReportWaveAttemptReceipt {
|
|
196
209
|
return { flow, attempt, requestedKeys: [...requestedKeys], ...receipt };
|
|
197
210
|
}
|
|
198
211
|
|
|
199
|
-
// ------------------------------------------------------------------------- the adapter seam
|
|
200
|
-
|
|
201
|
-
/** The minimal pi event-bus surface an adapter needs (mirrors pi's EventBus, whose `on` returns an unsubscribe function). */
|
|
202
|
-
export interface WaveBus {
|
|
203
|
-
emit(channel: string, data: unknown): void;
|
|
204
|
-
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** A successful capability ping; `asyncCompleteEvent` is the ADVERTISED async-complete channel. */
|
|
208
|
-
export interface WavePing {
|
|
209
|
-
asyncCompleteEvent: string;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** The detached async run a spawn launched. */
|
|
213
|
-
export interface WaveRunHandle {
|
|
214
|
-
asyncId: string;
|
|
215
|
-
asyncDir: string;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* An async-complete notification; at least one identifier is present on real payloads. The
|
|
220
|
-
* observability fields are optional — an identity-only completion stays valid (receipt absence
|
|
221
|
-
* degrades correlation, never behavior). The adapter normalizes them output-free and leaves
|
|
222
|
-
* each child's `agent` unset (enrichment happens against Perk-owned lane specs).
|
|
223
|
-
*/
|
|
224
|
-
export interface WaveCompletion {
|
|
225
|
-
asyncId?: string;
|
|
226
|
-
asyncDir?: string;
|
|
227
|
-
/** The run's raw terminal state string, when the payload carries one. */
|
|
228
|
-
state?: string;
|
|
229
|
-
success?: boolean;
|
|
230
|
-
children?: WaveChildReceipt[];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* The explicit acceptance-disable every wave spawn carries. Without it, pi-subagents
|
|
235
|
-
* auto-infers a generic acceptance contract for reviewer/analyst-named or read-only children
|
|
236
|
-
* and injects a fenced `acceptance-report` completion instruction into each lane — a COMPETING
|
|
237
|
-
* completion contract observed steering children into invalid `structured_output` attempts.
|
|
238
|
-
* `{level: "none"}` is the sanctioned disable shape (pi-subagents `explicitAcceptanceCanDisable`);
|
|
239
|
-
* `formatAcceptancePrompt` emits nothing at level none, so no contract block reaches a lane.
|
|
240
|
-
* Deliberately module-wide with no opt-out: every report-wave child's sole completion contract
|
|
241
|
-
* is the engine-validated `structured_output` report.
|
|
242
|
-
*/
|
|
243
|
-
export const WAVE_ACCEPTANCE = {
|
|
244
|
-
level: "none",
|
|
245
|
-
reason: "perk report-wave lanes complete via the engine-validated structured_output report",
|
|
246
|
-
} as const;
|
|
247
|
-
|
|
248
|
-
/** The full spawn params the runner fixes: async-only, ephemeral, fresh-context by definition. */
|
|
249
|
-
export interface WaveSpawnParams {
|
|
250
|
-
workflowScript: string;
|
|
251
|
-
async: true;
|
|
252
|
-
/** Waves are ephemeral by explicit decision — never mission-attached. */
|
|
253
|
-
mission: false;
|
|
254
|
-
/** A report wave is by definition fresh-context. */
|
|
255
|
-
context: "fresh";
|
|
256
|
-
/** The fixed acceptance disable (`WAVE_ACCEPTANCE`) — pi-subagents' workflow-defaults spread
|
|
257
|
-
* delivers it onto every lane child, suppressing the auto-inferred acceptance contract. */
|
|
258
|
-
acceptance: { level: "none"; reason: string };
|
|
259
|
-
outputSchema: object;
|
|
260
|
-
model?: string;
|
|
261
|
-
/** Orphan insurance: the run enforces the same deadline even if the parent session dies. */
|
|
262
|
-
timeoutMs: number;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export interface WaveAdapter {
|
|
266
|
-
/** Capability-checked ping; null ⇒ unavailable (loud degrade upstream). Must be called first. */
|
|
267
|
-
ping(): Promise<WavePing | null>;
|
|
268
|
-
/** Launch the async workflowScript run; throws ⇒ spawn-failed. */
|
|
269
|
-
spawn(params: WaveSpawnParams): Promise<WaveRunHandle>;
|
|
270
|
-
/** Subscribe to run completions (any run — the runner matches the handle); returns unsubscribe. */
|
|
271
|
-
onComplete(handler: (completion: WaveCompletion) => void): () => void;
|
|
272
|
-
/** Best-effort stop of a live run (timeout/cancel path); never throws. */
|
|
273
|
-
stop(handle: WaveRunHandle): Promise<void>;
|
|
274
|
-
/** Read the run's durable aggregate; throws ⇒ aggregate-unreadable. */
|
|
275
|
-
readAggregate(handle: WaveRunHandle): Promise<{ state: string; error?: string; value: unknown }>;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
212
|
// ---------------------------------------------------------------------------- the renderer
|
|
279
213
|
|
|
280
214
|
/**
|
|
@@ -287,41 +221,55 @@ export interface WaveAdapter {
|
|
|
287
221
|
export const RUN_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
288
222
|
|
|
289
223
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
224
|
+
* The report restriction packet (contracts.md §8.35): a constant every report child receives.
|
|
225
|
+
* With the runner bit it is the whole authorization input for the child's read-only floor.
|
|
226
|
+
*/
|
|
227
|
+
const REPORT_CHILD_RESTRICTIONS = { "perk.parent-restrictions/1": { readOnly: true } } as const;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validate the assignment manifest (throws on programmer error: empty, duplicate keys, or a key
|
|
231
|
+
* outside the run-key contract). Module-private — the script surface never leaves `waves/`.
|
|
296
232
|
*/
|
|
297
|
-
function
|
|
298
|
-
if (
|
|
233
|
+
function validateAssignments(assignments: ReportAssignment[]): void {
|
|
234
|
+
if (assignments.length === 0) {
|
|
299
235
|
throw new Error("renderWaveScript: a report wave needs at least one lane");
|
|
300
236
|
}
|
|
301
237
|
const seen = new Set<string>();
|
|
302
|
-
for (const
|
|
303
|
-
if (seen.has(
|
|
304
|
-
throw new Error(`renderWaveScript: duplicate lane key '${
|
|
238
|
+
for (const assignment of assignments) {
|
|
239
|
+
if (seen.has(assignment.key)) {
|
|
240
|
+
throw new Error(`renderWaveScript: duplicate lane key '${assignment.key}'`);
|
|
305
241
|
}
|
|
306
|
-
if (!RUN_KEY_PATTERN.test(
|
|
242
|
+
if (!RUN_KEY_PATTERN.test(assignment.key)) {
|
|
307
243
|
throw new Error(
|
|
308
|
-
`renderWaveScript: lane key '${
|
|
244
|
+
`renderWaveScript: lane key '${assignment.key}' violates the pi-subagents run-key contract`,
|
|
309
245
|
);
|
|
310
246
|
}
|
|
311
|
-
seen.add(
|
|
247
|
+
seen.add(assignment.key);
|
|
312
248
|
}
|
|
313
249
|
}
|
|
314
250
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Render the wave `workflowScript`: an explicit-return, all-settled `runs.all` over the
|
|
253
|
+
* assignment items, projected to the compact typed aggregate only (assignment key, outcome,
|
|
254
|
+
* error, and the schema-validated report — children's prose never enters the aggregate beyond
|
|
255
|
+
* `error`/`output` on failure). Every report child is read-only under perk's floor and runs in
|
|
256
|
+
* the caller checkout; hostile task/assignment fields cannot override either (explicit field
|
|
257
|
+
* selection + `JSON.stringify`, so hostile task text — quotes, newlines, backticks, `${}` —
|
|
258
|
+
* cannot escape the array literal either). Module-private: the script bytes are observable
|
|
259
|
+
* outside `waves/` only through the adapter seam's spawn params.
|
|
260
|
+
*/
|
|
261
|
+
function renderWaveScript(assignments: ReportAssignment[]): string {
|
|
262
|
+
validateAssignments(assignments);
|
|
263
|
+
const items = assignments.map((assignment) => ({
|
|
264
|
+
key: assignment.key,
|
|
265
|
+
agent: assignment.agent,
|
|
266
|
+
task: assignment.task,
|
|
267
|
+
extensionBindings: REPORT_CHILD_RESTRICTIONS,
|
|
268
|
+
worktree: false,
|
|
269
|
+
...(assignment.skill !== undefined ? { skill: assignment.skill } : {}),
|
|
270
|
+
label: assignment.label ?? assignment.key,
|
|
271
|
+
...(assignment.phase !== undefined ? { phase: assignment.phase } : {}),
|
|
272
|
+
...(assignment.outputSchema !== undefined ? { outputSchema: assignment.outputSchema } : {}),
|
|
325
273
|
}));
|
|
326
274
|
return (
|
|
327
275
|
`const reports = await runs.all(${JSON.stringify(items, null, 2)});\n` +
|
|
@@ -332,126 +280,86 @@ export function renderWaveScript(lanes: WaveLane[]): string {
|
|
|
332
280
|
|
|
333
281
|
// ------------------------------------------------------------------------------- the runner
|
|
334
282
|
|
|
335
|
-
/**
|
|
336
|
-
* The module-owned wave timeout default: a deliberate tightening vs the 30-minute foreground
|
|
337
|
-
* default the prompt-mechanics wave rode. Per-flow `spec.timeoutMs` overrides; the default is
|
|
338
|
-
* overridable for tests via PERK_WAVE_TIMEOUT_MS.
|
|
339
|
-
*/
|
|
340
|
-
export const WAVE_TIMEOUT_MS = 15 * 60_000;
|
|
341
|
-
|
|
342
|
-
function waveTimeoutMs(): number {
|
|
343
|
-
const raw = Number(process.env.PERK_WAVE_TIMEOUT_MS ?? "");
|
|
344
|
-
return Number.isFinite(raw) && raw > 0 ? raw : WAVE_TIMEOUT_MS;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
283
|
function waveFailure(
|
|
348
|
-
reason:
|
|
284
|
+
reason: WaveRunFailureReason,
|
|
349
285
|
detail: string,
|
|
350
286
|
receipt: WaveScriptReceipt,
|
|
351
|
-
):
|
|
287
|
+
): ReportWaveResult {
|
|
352
288
|
return { complete: false, reports: [], failures: [{ key: null, reason, detail }], receipt };
|
|
353
289
|
}
|
|
354
290
|
|
|
355
|
-
/** The judgment-bearing pieces a script run needs (the lane-free slice of `WaveSpec`). */
|
|
356
|
-
export interface WaveScriptSpec {
|
|
357
|
-
/** Flow name for error detail/trace (e.g. "pr-review-dynamic"). */
|
|
358
|
-
flow: string;
|
|
359
|
-
/** The complete, module-rendered workflowScript (never model-authored). */
|
|
360
|
-
workflowScript: string;
|
|
361
|
-
/** Workflow-level default → the engine injects a `structured_output` tool into each child. */
|
|
362
|
-
outputSchema: object;
|
|
363
|
-
/** Workflow-level model default (per-item `model` fields in the script override it). */
|
|
364
|
-
model?: string;
|
|
365
|
-
/** Module default (`WAVE_TIMEOUT_MS`) when omitted. */
|
|
366
|
-
timeoutMs?: number;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** A script run's outcome: the raw `workflow.value` on success, one wave-level failure otherwise. */
|
|
370
|
-
export type WaveScriptResult =
|
|
371
|
-
| { ok: true; value: unknown; receipt: WaveScriptReceipt }
|
|
372
|
-
| { ok: false; failure: WaveFailure; receipt: WaveScriptReceipt };
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* A launched (or launch-refused) script run. On `ok: true` the run is LIVE: `handle` is the
|
|
376
|
-
* detached async run, and `result` settles when the back half finishes (completion wait under
|
|
377
|
-
* the module-owned timeout, AbortSignal honor, best-effort stop on timeout/cancel, the durable
|
|
378
|
-
* aggregate read, receipt assembly, unsubscribe-on-settle). `result` NEVER rejects — every arm
|
|
379
|
-
* normalizes into `WaveScriptResult`, so an uncollected wave can never become an unhandled
|
|
380
|
-
* rejection. Pre-spawn failures (aborted-before-launch, ping fail/null, spawn throw) take the
|
|
381
|
-
* `ok: false` arm with the same failure/receipt values the blocking runner reports, and the
|
|
382
|
-
* completion subscription is released immediately.
|
|
383
|
-
*/
|
|
384
|
-
export type WaveScriptStart =
|
|
385
|
-
| { ok: true; handle: WaveRunHandle; result: Promise<WaveScriptResult> }
|
|
386
|
-
| { ok: false; failure: WaveFailure; receipt: WaveScriptReceipt };
|
|
387
|
-
|
|
388
|
-
function errorDetail(error: unknown): string {
|
|
389
|
-
return error instanceof Error ? error.message : String(error);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
291
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
393
292
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
394
293
|
}
|
|
395
294
|
|
|
396
295
|
/**
|
|
397
|
-
* Normalize the aggregate's entries against the expected
|
|
398
|
-
* rendered the script, but the aggregate crossed a process boundary). Unknown extra keys
|
|
399
|
-
* ignored: the module owns the script, so extras cannot occur without upstream drift, and
|
|
400
|
-
* per-
|
|
401
|
-
*
|
|
402
|
-
*
|
|
296
|
+
* Normalize the aggregate's entries against the expected assignment keys (defensive — the
|
|
297
|
+
* module rendered the script, but the aggregate crossed a process boundary). Unknown extra keys
|
|
298
|
+
* are ignored: the module owns the script, so extras cannot occur without upstream drift, and
|
|
299
|
+
* the per-assignment reasons below already make the wave incomplete under `strict`. A key
|
|
300
|
+
* appearing more than once is ambiguous identity: `malformed-report`, every row withheld (§8.35).
|
|
301
|
+
* Module-private: the wave's settle is the only consumer.
|
|
403
302
|
*/
|
|
404
|
-
|
|
303
|
+
function normalizeAssignments(
|
|
405
304
|
keys: string[],
|
|
406
305
|
entries: unknown[],
|
|
407
|
-
): { reports:
|
|
408
|
-
const reports:
|
|
409
|
-
const failures:
|
|
306
|
+
): { reports: AssignmentReport[]; failures: ReportWaveFailure[] } {
|
|
307
|
+
const reports: AssignmentReport[] = [];
|
|
308
|
+
const failures: ReportWaveFailure[] = [];
|
|
410
309
|
for (const key of keys) {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
310
|
+
const rows = entries.filter((e): e is Record<string, unknown> => isRecord(e) && e.key === key);
|
|
311
|
+
if (rows.length > 1) {
|
|
312
|
+
// Duplicate expected keys are ambiguous identity: withhold every row (§8.35).
|
|
313
|
+
failures.push({
|
|
314
|
+
key,
|
|
315
|
+
reason: "malformed-report",
|
|
316
|
+
detail: `lane '${key}' appears ${rows.length} times in the wave aggregate — ambiguous identity, evidence withheld`,
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const entry = rows[0];
|
|
321
|
+
if (entry === undefined) {
|
|
414
322
|
failures.push({
|
|
415
|
-
key
|
|
323
|
+
key,
|
|
416
324
|
reason: "missing-lane",
|
|
417
|
-
detail: `lane '${
|
|
325
|
+
detail: `lane '${key}' is absent from the wave aggregate`,
|
|
418
326
|
});
|
|
419
327
|
continue;
|
|
420
328
|
}
|
|
421
329
|
if (entry.ok === true) {
|
|
422
330
|
const report = entry.report;
|
|
423
331
|
if (isRecord(report)) {
|
|
424
|
-
reports.push({ key
|
|
332
|
+
reports.push({ key, report });
|
|
425
333
|
} else if (report === null || report === undefined) {
|
|
426
334
|
failures.push({
|
|
427
|
-
key
|
|
335
|
+
key,
|
|
428
336
|
reason: "lane-failed",
|
|
429
337
|
detail:
|
|
430
338
|
typeof entry.error === "string" && entry.error !== ""
|
|
431
339
|
? entry.error
|
|
432
|
-
: `lane '${
|
|
340
|
+
: `lane '${key}' resolved without a schema-valid report`,
|
|
433
341
|
});
|
|
434
342
|
} else {
|
|
435
343
|
failures.push({
|
|
436
|
-
key
|
|
344
|
+
key,
|
|
437
345
|
reason: "malformed-report",
|
|
438
|
-
detail: `lane '${
|
|
346
|
+
detail: `lane '${key}' carries a non-object report (${Array.isArray(report) ? "array" : typeof report})`,
|
|
439
347
|
});
|
|
440
348
|
}
|
|
441
349
|
} else if (entry.ok === false) {
|
|
442
350
|
failures.push({
|
|
443
|
-
key
|
|
351
|
+
key,
|
|
444
352
|
reason: "lane-failed",
|
|
445
353
|
detail:
|
|
446
354
|
typeof entry.error === "string" && entry.error !== ""
|
|
447
355
|
? entry.error
|
|
448
|
-
: `lane '${
|
|
356
|
+
: `lane '${key}' failed without error detail`,
|
|
449
357
|
});
|
|
450
358
|
} else {
|
|
451
359
|
failures.push({
|
|
452
|
-
key
|
|
360
|
+
key,
|
|
453
361
|
reason: "malformed-report",
|
|
454
|
-
detail: `lane '${
|
|
362
|
+
detail: `lane '${key}' aggregate entry has no boolean 'ok'`,
|
|
455
363
|
});
|
|
456
364
|
}
|
|
457
365
|
}
|
|
@@ -459,258 +367,32 @@ export function normalizeLanes(
|
|
|
459
367
|
}
|
|
460
368
|
|
|
461
369
|
/**
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* abortable → best-effort stop on timeout/cancel → read the durable aggregate → the
|
|
466
|
-
* `state !== "complete"` / unreadable arms) runs behind the returned `result` promise, which
|
|
467
|
-
* never rejects. The shared operational core under every runner; per-flow value normalization
|
|
468
|
-
* stays with the caller.
|
|
469
|
-
*/
|
|
470
|
-
export async function startWaveScript(
|
|
471
|
-
adapter: WaveAdapter,
|
|
472
|
-
spec: WaveScriptSpec,
|
|
473
|
-
signal?: AbortSignal,
|
|
474
|
-
): Promise<WaveScriptStart> {
|
|
475
|
-
// The receipt is assembled in EVERY terminal arm — write-only telemetry: nothing below reads
|
|
476
|
-
// it back into the ok/failure decision.
|
|
477
|
-
const receiptOf = (
|
|
478
|
-
state: WaveReceiptState,
|
|
479
|
-
spawned: WaveRunHandle | null,
|
|
480
|
-
completion?: WaveCompletion,
|
|
481
|
-
): WaveScriptReceipt => ({
|
|
482
|
-
...(spawned !== null ? { runId: spawned.asyncId, asyncDir: spawned.asyncDir } : {}),
|
|
483
|
-
state,
|
|
484
|
-
children: completion?.children ?? [],
|
|
485
|
-
});
|
|
486
|
-
const startFailure = (
|
|
487
|
-
reason: WaveFailureReason,
|
|
488
|
-
detail: string,
|
|
489
|
-
receipt: WaveScriptReceipt,
|
|
490
|
-
): WaveScriptStart => ({
|
|
491
|
-
ok: false,
|
|
492
|
-
failure: { key: null, reason, detail },
|
|
493
|
-
receipt,
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// Read through a closure so TS's readonly-property narrowing never staples the first
|
|
497
|
-
// check's `false` onto the post-await re-check (the signal CAN flip during an await).
|
|
498
|
-
const aborted = (): boolean => signal?.aborted === true;
|
|
499
|
-
const cancelledBeforeLaunch = (): WaveScriptStart =>
|
|
500
|
-
startFailure(
|
|
501
|
-
"cancelled",
|
|
502
|
-
`wave '${spec.flow}' was cancelled before launch`,
|
|
503
|
-
receiptOf("cancelled", null),
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
if (aborted()) return cancelledBeforeLaunch();
|
|
507
|
-
|
|
508
|
-
// 1. Capability check — the loud-degrade arm: the result explicitly names the wave
|
|
509
|
-
// unavailable; callers surface it, never silently fall back to model-authored scripts.
|
|
510
|
-
let ping: WavePing | null;
|
|
511
|
-
try {
|
|
512
|
-
ping = await adapter.ping();
|
|
513
|
-
} catch (error) {
|
|
514
|
-
return startFailure(
|
|
515
|
-
"unavailable",
|
|
516
|
-
`subagent RPC ping failed: ${errorDetail(error)}`,
|
|
517
|
-
receiptOf("unavailable", null),
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
if (ping === null) {
|
|
521
|
-
return startFailure(
|
|
522
|
-
"unavailable",
|
|
523
|
-
"pi-subagents did not advertise the report-wave capabilities (ping failed or incomplete)",
|
|
524
|
-
receiptOf("unavailable", null),
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// An abort can arrive WHILE the ping await is pending — re-check before subscribe/spawn so a
|
|
529
|
-
// cancelled wave never launches (the pre-launch check alone leaves this window open).
|
|
530
|
-
if (aborted()) return cancelledBeforeLaunch();
|
|
531
|
-
|
|
532
|
-
// 2. Subscribe BEFORE spawn: a completion can arrive before the spawn reply resolves (the
|
|
533
|
-
// completion-before-reply race) — every completion is buffered and re-checked once the
|
|
534
|
-
// handle is known.
|
|
535
|
-
let handle: WaveRunHandle | null = null;
|
|
536
|
-
let notifyMatch: (() => void) | null = null;
|
|
537
|
-
const buffered: WaveCompletion[] = [];
|
|
538
|
-
const matchesHandle = (completion: WaveCompletion): boolean =>
|
|
539
|
-
handle !== null &&
|
|
540
|
-
((completion.asyncDir !== undefined && completion.asyncDir === handle.asyncDir) ||
|
|
541
|
-
(completion.asyncId !== undefined && completion.asyncId === handle.asyncId));
|
|
542
|
-
const unsubscribe = adapter.onComplete((completion) => {
|
|
543
|
-
buffered.push(completion);
|
|
544
|
-
if (matchesHandle(completion) && notifyMatch !== null) notifyMatch();
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
// 3. Spawn: async-only, ephemeral, fresh-context — the module fixes those; the flow's spec
|
|
548
|
-
// supplies the judgment-bearing pieces (lanes, schema, model, policy).
|
|
549
|
-
const timeoutMs = spec.timeoutMs ?? waveTimeoutMs();
|
|
550
|
-
try {
|
|
551
|
-
handle = await adapter.spawn({
|
|
552
|
-
workflowScript: spec.workflowScript,
|
|
553
|
-
async: true,
|
|
554
|
-
mission: false,
|
|
555
|
-
context: "fresh",
|
|
556
|
-
acceptance: WAVE_ACCEPTANCE,
|
|
557
|
-
outputSchema: spec.outputSchema,
|
|
558
|
-
...(spec.model !== undefined ? { model: spec.model } : {}),
|
|
559
|
-
timeoutMs,
|
|
560
|
-
});
|
|
561
|
-
} catch (error) {
|
|
562
|
-
unsubscribe();
|
|
563
|
-
return startFailure(
|
|
564
|
-
"spawn-failed",
|
|
565
|
-
`wave spawn failed: ${errorDetail(error)}`,
|
|
566
|
-
receiptOf("spawn-failed", null),
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
const spawned = handle;
|
|
570
|
-
|
|
571
|
-
const scriptFailure = (
|
|
572
|
-
reason: WaveFailureReason,
|
|
573
|
-
detail: string,
|
|
574
|
-
receipt: WaveScriptReceipt,
|
|
575
|
-
): WaveScriptResult => ({
|
|
576
|
-
ok: false,
|
|
577
|
-
failure: { key: null, reason, detail },
|
|
578
|
-
receipt,
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// The back half: every arm below RETURNS a normalized `WaveScriptResult` (never throws), so
|
|
582
|
-
// `result` never rejects; the subscription is released exactly when it settles.
|
|
583
|
-
const settle = async (): Promise<WaveScriptResult> => {
|
|
584
|
-
try {
|
|
585
|
-
// 4. Block on completion with the module-owned timeout; honor the caller's AbortSignal.
|
|
586
|
-
const outcome = await new Promise<"complete" | "timeout" | "cancelled">((resolve) => {
|
|
587
|
-
if (buffered.some(matchesHandle)) {
|
|
588
|
-
resolve("complete");
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
const settleOutcome = (value: "complete" | "timeout" | "cancelled"): void => {
|
|
592
|
-
clearTimeout(timer);
|
|
593
|
-
signal?.removeEventListener("abort", onAbort);
|
|
594
|
-
notifyMatch = null;
|
|
595
|
-
resolve(value);
|
|
596
|
-
};
|
|
597
|
-
const timer = setTimeout(() => settleOutcome("timeout"), timeoutMs);
|
|
598
|
-
const onAbort = (): void => settleOutcome("cancelled");
|
|
599
|
-
notifyMatch = () => settleOutcome("complete");
|
|
600
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
601
|
-
if (signal?.aborted === true) settleOutcome("cancelled");
|
|
602
|
-
});
|
|
603
|
-
if (outcome !== "complete") {
|
|
604
|
-
// Best-effort stop — adapters never throw here by contract, but a broken adapter's error
|
|
605
|
-
// is still swallowed into the detail rather than re-thrown.
|
|
606
|
-
let stopNote = "";
|
|
607
|
-
try {
|
|
608
|
-
await adapter.stop(spawned);
|
|
609
|
-
} catch (error) {
|
|
610
|
-
stopNote = ` (stop failed: ${errorDetail(error)})`;
|
|
611
|
-
}
|
|
612
|
-
return outcome === "timeout"
|
|
613
|
-
? scriptFailure(
|
|
614
|
-
"timeout",
|
|
615
|
-
`wave '${spec.flow}' timed out after ${timeoutMs}ms${stopNote}`,
|
|
616
|
-
receiptOf("timed-out", spawned),
|
|
617
|
-
)
|
|
618
|
-
: scriptFailure(
|
|
619
|
-
"cancelled",
|
|
620
|
-
`wave '${spec.flow}' was cancelled${stopNote}`,
|
|
621
|
-
receiptOf("cancelled", spawned),
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// The MATCHED completion (retained for the receipt — its normalized children are the
|
|
626
|
-
// child-lane identity/artifact trail; an identity-only completion yields empty children).
|
|
627
|
-
const matched = buffered.find(matchesHandle);
|
|
628
|
-
|
|
629
|
-
// 5. Read the durable aggregate; surface the terminal-state arms.
|
|
630
|
-
let aggregate: { state: string; error?: string; value: unknown };
|
|
631
|
-
try {
|
|
632
|
-
aggregate = await adapter.readAggregate(spawned);
|
|
633
|
-
} catch (error) {
|
|
634
|
-
// Aggregate-unreadable: the completion identity is retained — the receipt state derives
|
|
635
|
-
// from the OBSERVED completion (a correlation label, not a verdict; the authoritative
|
|
636
|
-
// failure reason stays in the wave failure).
|
|
637
|
-
return scriptFailure(
|
|
638
|
-
"aggregate-unreadable",
|
|
639
|
-
`wave aggregate unreadable: ${errorDetail(error)}`,
|
|
640
|
-
receiptOf(matched?.success === false ? "failed" : "complete", spawned, matched),
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
if (aggregate.state !== "complete") {
|
|
644
|
-
const detail = aggregate.error !== undefined ? `: ${aggregate.error}` : "";
|
|
645
|
-
return scriptFailure(
|
|
646
|
-
"run-failed",
|
|
647
|
-
`wave run ended '${aggregate.state}'${detail}`,
|
|
648
|
-
receiptOf("failed", spawned, matched),
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
return { ok: true, value: aggregate.value, receipt: receiptOf("complete", spawned, matched) };
|
|
652
|
-
} finally {
|
|
653
|
-
unsubscribe();
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
return { ok: true, handle: spawned, result: settle() };
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Run one module-rendered workflowScript to completion — the blocking form: `startWaveScript` +
|
|
661
|
-
* await its `result` (one operational core, behavior identical to the historical blocking
|
|
662
|
-
* runner).
|
|
663
|
-
*/
|
|
664
|
-
export async function runWaveScript(
|
|
665
|
-
adapter: WaveAdapter,
|
|
666
|
-
spec: WaveScriptSpec,
|
|
667
|
-
signal?: AbortSignal,
|
|
668
|
-
): Promise<WaveScriptResult> {
|
|
669
|
-
const start = await startWaveScript(adapter, spec, signal);
|
|
670
|
-
if (!start.ok) return { ok: false, failure: start.failure, receipt: start.receipt };
|
|
671
|
-
return await start.result;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Enrich receipt children's `agent` from the Perk-owned lane specs by key. Children are never
|
|
676
|
-
* synthesized from lanes — an identity-only completion keeps its empty children (receipt absence
|
|
677
|
-
* degrades correlation, never behavior).
|
|
370
|
+
* Enrich receipt children's `agent` from the Perk-owned assignment specs by key. Children are
|
|
371
|
+
* never synthesized from assignments — an identity-only completion keeps its empty children
|
|
372
|
+
* (receipt absence degrades correlation, never behavior).
|
|
678
373
|
*/
|
|
679
|
-
function enrichReceipt(
|
|
374
|
+
function enrichReceipt(
|
|
375
|
+
receipt: WaveScriptReceipt,
|
|
376
|
+
assignments: ReportAssignment[],
|
|
377
|
+
): WaveScriptReceipt {
|
|
680
378
|
return {
|
|
681
379
|
...receipt,
|
|
682
380
|
children: receipt.children.map((child) => {
|
|
683
381
|
if (child.agent !== undefined) return child;
|
|
684
|
-
const agent =
|
|
382
|
+
const agent = assignments.find((assignment) => assignment.key === child.key)?.agent;
|
|
685
383
|
return agent === undefined ? child : { ...child, agent };
|
|
686
384
|
}),
|
|
687
385
|
};
|
|
688
386
|
}
|
|
689
387
|
|
|
690
388
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* the launch failure is already normalized into a `WaveResult` (receipt included) — no promise
|
|
695
|
-
* to await, nothing left running.
|
|
696
|
-
*/
|
|
697
|
-
export type ReportWaveStart =
|
|
698
|
-
| {
|
|
699
|
-
ok: true;
|
|
700
|
-
handle: WaveRunHandle;
|
|
701
|
-
result: Promise<WaveResult>;
|
|
702
|
-
launch: WaveLaunchManifest;
|
|
703
|
-
}
|
|
704
|
-
| { ok: false; result: WaveResult; launch: WaveLaunchManifest };
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Settle one script outcome into the lane-level `WaveResult`: receipt enrichment, the
|
|
708
|
-
* workflow.value array check, per-lane-key normalization, and the completeness policy — the
|
|
709
|
-
* single back half both the blocking runner and the streaming sibling apply.
|
|
389
|
+
* Settle one script outcome into the assignment-level `ReportWaveResult`: receipt enrichment,
|
|
390
|
+
* the workflow.value array check, per-key normalization, and the completeness policy — the
|
|
391
|
+
* single back half both the blocking and streaming lifecycles apply.
|
|
710
392
|
*/
|
|
711
|
-
function settleReportWave(run: WaveScriptResult,
|
|
712
|
-
const receipt = enrichReceipt(run.receipt,
|
|
713
|
-
if (!run.ok) {
|
|
393
|
+
function settleReportWave(run: WaveScriptResult, request: ReportWaveRequest): ReportWaveResult {
|
|
394
|
+
const receipt = enrichReceipt(run.receipt, request.assignments);
|
|
395
|
+
if (!run.ok && !Array.isArray(run.value)) {
|
|
714
396
|
return { complete: false, reports: [], failures: [run.failure], receipt };
|
|
715
397
|
}
|
|
716
398
|
if (!Array.isArray(run.value)) {
|
|
@@ -721,64 +403,176 @@ function settleReportWave(run: WaveScriptResult, spec: WaveSpec): WaveResult {
|
|
|
721
403
|
);
|
|
722
404
|
}
|
|
723
405
|
|
|
724
|
-
const
|
|
725
|
-
|
|
406
|
+
const normalized = normalizeAssignments(
|
|
407
|
+
request.assignments.map((assignment) => assignment.key),
|
|
726
408
|
run.value,
|
|
727
409
|
);
|
|
410
|
+
const { reports } = normalized;
|
|
411
|
+
// Retained evidence does not imply completion under either policy. Keep the transport
|
|
412
|
+
// failure first, followed by keyed normalization failures in request order.
|
|
413
|
+
const failures = run.ok ? normalized.failures : [run.failure, ...normalized.failures];
|
|
728
414
|
const complete =
|
|
729
|
-
|
|
415
|
+
request.completeness === "strict"
|
|
730
416
|
? failures.length === 0
|
|
731
417
|
: failures.every((failure) => failure.key !== null);
|
|
732
418
|
return { complete, reports, failures, receipt };
|
|
733
419
|
}
|
|
734
420
|
|
|
421
|
+
// -------------------------------------------------------------------- the opaque lifecycle
|
|
422
|
+
|
|
423
|
+
declare const REPORT_WAVE_REF: unique symbol;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The opaque handle to one started, uncollected report wave — NOMINAL by a declared
|
|
427
|
+
* (value-less) unique-symbol brand: no structural forgery can satisfy it, and it exposes no
|
|
428
|
+
* operational members. Minted at exactly one internal site (immediately after the runtime
|
|
429
|
+
* evidence of a successful launch); meaningful only to the instance that minted it — a foreign
|
|
430
|
+
* instance's `collect` answers `"none"` structurally.
|
|
431
|
+
*/
|
|
432
|
+
export interface ReportWaveRef {
|
|
433
|
+
readonly [REPORT_WAVE_REF]: true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The optional per-call controls; `signal` cancels pre-launch and best-effort stops post-launch. */
|
|
437
|
+
export interface WaveControl {
|
|
438
|
+
signal?: AbortSignal;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* A started (or launch-failed) report wave. On `ok: true` the wave is LIVE behind the opaque
|
|
443
|
+
* `ref` — `runId`/`asyncDir` are identity telemetry only (receipt vocabulary), never an
|
|
444
|
+
* operable handle. On `ok: false` the launch failure is already normalized into a
|
|
445
|
+
* `ReportWaveResult` (receipt included) — nothing left running, nothing to collect.
|
|
446
|
+
*/
|
|
447
|
+
export type StartWaveResult =
|
|
448
|
+
| {
|
|
449
|
+
ok: true;
|
|
450
|
+
ref: ReportWaveRef;
|
|
451
|
+
/** Identity telemetry only (receipt vocabulary) — never an operable handle. */
|
|
452
|
+
runId: string;
|
|
453
|
+
asyncDir: string;
|
|
454
|
+
launch: ReportWaveLaunchManifest;
|
|
455
|
+
}
|
|
456
|
+
| { ok: false; result: ReportWaveResult; launch: ReportWaveLaunchManifest };
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* A collect's outcome:
|
|
460
|
+
* - `"none"`: unknown ref — never started here, already drained, or a foreign instance's.
|
|
461
|
+
* - `"running"`: unsettled after the grace — the ref stays pending. A premature collector
|
|
462
|
+
* yields until matching workflow completion; expiry after observed completion is a lifecycle
|
|
463
|
+
* contradiction for owner diagnosis, not a polling cue. The module-owned timeout stays.
|
|
464
|
+
* - `"settled"`: this collector won the drain — `keys` is the launch's frozen requested
|
|
465
|
+
* manifest snapshot, `result` the normalized outcome. Drain-once is exact even under
|
|
466
|
+
* overlapping collectors (delete-as-claim).
|
|
467
|
+
*/
|
|
468
|
+
export type CollectWaveResult =
|
|
469
|
+
| { kind: "none" }
|
|
470
|
+
| { kind: "running" }
|
|
471
|
+
| { kind: "settled"; keys: readonly string[]; result: ReportWaveResult };
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The deep seam: callers supply assignments and consume typed outcomes — never adapters, run
|
|
475
|
+
* handles, or result promises. `start`/`collect` are the streaming split (the parent returns
|
|
476
|
+
* from the launch, ends its turn, and resumes on native wakes); `run` is the blocking form (start + await, no
|
|
477
|
+
* ref escapes). The only throws are programmer errors (empty assignments, duplicate keys, keys
|
|
478
|
+
* outside `RUN_KEY_PATTERN`); every operational failure normalizes into `ReportWaveResult`.
|
|
479
|
+
*/
|
|
480
|
+
export interface ReportWave {
|
|
481
|
+
start(request: ReportWaveRequest, control?: WaveControl): Promise<StartWaveResult>;
|
|
482
|
+
collect(ref: ReportWaveRef): Promise<CollectWaveResult>;
|
|
483
|
+
run(request: ReportWaveRequest, control?: WaveControl): Promise<ReportWaveResult>;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The grace a collect allows a not-yet-settled wave before answering `"running"`: long enough
|
|
488
|
+
* to absorb ordering skew between the native completion notice and aggregate resolution,
|
|
489
|
+
* bounded so a premature call can yield again. The `PERK_WAVE_COLLECT_GRACE_MS` env knob is the ONE grace seam
|
|
490
|
+
* (module-private — there is no per-call grace parameter); invalid values fall back.
|
|
491
|
+
*/
|
|
492
|
+
const WAVE_COLLECT_GRACE_MS = 15_000;
|
|
493
|
+
|
|
494
|
+
function collectGraceMs(): number {
|
|
495
|
+
const raw = Number(process.env.PERK_WAVE_COLLECT_GRACE_MS ?? "");
|
|
496
|
+
return Number.isFinite(raw) && raw > 0 ? raw : WAVE_COLLECT_GRACE_MS;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* One pending (started, uncollected) wave: the frozen pre-launch key manifest snapshot (copied
|
|
501
|
+
* from the launch manifest at start — caller mutation of the returned `StartWaveResult.launch`
|
|
502
|
+
* can never change a later collect's keys) plus the never-rejecting result promise. NO drained
|
|
503
|
+
* flag: presence in the instance's map IS pending. After settlement the normalized promise
|
|
504
|
+
* owns retained partial reports until collection; no later artifact read or resume recovers them.
|
|
505
|
+
*/
|
|
506
|
+
interface PendingRecord {
|
|
507
|
+
keys: readonly string[];
|
|
508
|
+
result: Promise<ReportWaveResult>;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* A launched (or launch-failed) wave as the internal core reports it — the module-private
|
|
513
|
+
* predecessor shape the opaque lifecycle wraps (the run handle and result promise never leave
|
|
514
|
+
* the module).
|
|
515
|
+
*/
|
|
516
|
+
type InternalStart =
|
|
517
|
+
| {
|
|
518
|
+
ok: true;
|
|
519
|
+
handle: WaveRunHandle;
|
|
520
|
+
result: Promise<ReportWaveResult>;
|
|
521
|
+
launch: ReportWaveLaunchManifest;
|
|
522
|
+
}
|
|
523
|
+
| { ok: false; result: ReportWaveResult; launch: ReportWaveLaunchManifest };
|
|
524
|
+
|
|
735
525
|
/**
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
* the
|
|
740
|
-
*
|
|
526
|
+
* The internal launch core: validate the COMPLETE requested manifest (the programmer-error
|
|
527
|
+
* throws — empty/duplicate/invalid keys — are preserved even for an unavailable required
|
|
528
|
+
* skill), run the required-skill preflight partition, render the all-settled assignment script
|
|
529
|
+
* over the runnable subset, and launch it via `startWaveScript`. On success the never-rejecting
|
|
530
|
+
* `result` promise applies the shared settle (normalization + completeness + receipt
|
|
531
|
+
* enrichment + preflight-failure merge) when the run finishes; a launch failure comes back as
|
|
532
|
+
* an already-settled, normalized `ReportWaveResult`.
|
|
741
533
|
*/
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
534
|
+
async function startWave(
|
|
535
|
+
supplyAdapter: () => WaveAdapter,
|
|
536
|
+
request: ReportWaveRequest,
|
|
745
537
|
signal?: AbortSignal,
|
|
746
|
-
): Promise<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
validateWaveLanes(spec.lanes);
|
|
750
|
-
const preflight = spec.requiredSkillPreflight ?? preflightPonytailSkill;
|
|
538
|
+
): Promise<InternalStart> {
|
|
539
|
+
validateAssignments(request.assignments);
|
|
540
|
+
const preflight = request.requiredSkillPreflight ?? preflightPonytailSkill;
|
|
751
541
|
const checked = new Map<string, PonytailPreflight>();
|
|
752
|
-
const runnable:
|
|
753
|
-
const skillFailures:
|
|
754
|
-
for (const
|
|
755
|
-
if (
|
|
756
|
-
runnable.push(
|
|
542
|
+
const runnable: ReportAssignment[] = [];
|
|
543
|
+
const skillFailures: ReportWaveFailure[] = [];
|
|
544
|
+
for (const assignment of request.assignments) {
|
|
545
|
+
if (assignment.requiredSkill === undefined) {
|
|
546
|
+
runnable.push(assignment);
|
|
757
547
|
continue;
|
|
758
548
|
}
|
|
759
|
-
let result = checked.get(
|
|
549
|
+
let result = checked.get(assignment.requiredSkill.skillFile);
|
|
760
550
|
if (result === undefined) {
|
|
761
|
-
result = await preflight(
|
|
762
|
-
checked.set(
|
|
551
|
+
result = await preflight(assignment.requiredSkill);
|
|
552
|
+
checked.set(assignment.requiredSkill.skillFile, result);
|
|
763
553
|
}
|
|
764
554
|
if (result.ok) {
|
|
765
|
-
runnable.push(
|
|
555
|
+
runnable.push(assignment);
|
|
766
556
|
} else {
|
|
767
|
-
skillFailures.push({
|
|
557
|
+
skillFailures.push({
|
|
558
|
+
key: assignment.key,
|
|
559
|
+
reason: "skill-unavailable",
|
|
560
|
+
detail: result.detail,
|
|
561
|
+
});
|
|
768
562
|
}
|
|
769
563
|
}
|
|
770
564
|
|
|
771
|
-
const launch:
|
|
772
|
-
requested:
|
|
773
|
-
runnable: runnable.map((
|
|
565
|
+
const launch: ReportWaveLaunchManifest = {
|
|
566
|
+
requested: request.assignments.map((assignment) => assignment.key),
|
|
567
|
+
runnable: runnable.map((assignment) => assignment.key),
|
|
774
568
|
preflightFailures: [...skillFailures],
|
|
775
569
|
};
|
|
776
570
|
|
|
777
|
-
const settleWithSkillFailures = (result:
|
|
571
|
+
const settleWithSkillFailures = (result: ReportWaveResult): ReportWaveResult => {
|
|
778
572
|
if (skillFailures.length === 0) return result;
|
|
779
573
|
const failures = [...result.failures, ...skillFailures];
|
|
780
574
|
const complete =
|
|
781
|
-
|
|
575
|
+
request.completeness === "strict"
|
|
782
576
|
? failures.length === 0
|
|
783
577
|
: failures.every((failure) => failure.key !== null);
|
|
784
578
|
return { ...result, complete, failures };
|
|
@@ -793,18 +587,18 @@ export async function startReportWave(
|
|
|
793
587
|
};
|
|
794
588
|
}
|
|
795
589
|
|
|
796
|
-
// Required-skill metadata never reaches the renderer; only runnable
|
|
797
|
-
const
|
|
590
|
+
// Required-skill metadata never reaches the renderer; only runnable assignments spawn.
|
|
591
|
+
const runnableRequest: ReportWaveRequest = { ...request, assignments: runnable };
|
|
798
592
|
const workflowScript = renderWaveScript(runnable);
|
|
799
593
|
|
|
800
594
|
const start = await startWaveScript(
|
|
801
|
-
|
|
595
|
+
supplyAdapter(),
|
|
802
596
|
{
|
|
803
|
-
flow:
|
|
597
|
+
flow: request.flow,
|
|
804
598
|
workflowScript,
|
|
805
|
-
outputSchema:
|
|
806
|
-
...(
|
|
807
|
-
...(
|
|
599
|
+
outputSchema: request.outputSchema,
|
|
600
|
+
...(request.model !== undefined ? { model: request.model } : {}),
|
|
601
|
+
...(request.timeoutMs !== undefined ? { timeoutMs: request.timeoutMs } : {}),
|
|
808
602
|
},
|
|
809
603
|
signal,
|
|
810
604
|
);
|
|
@@ -814,7 +608,7 @@ export async function startReportWave(
|
|
|
814
608
|
result: settleWithSkillFailures(
|
|
815
609
|
settleReportWave(
|
|
816
610
|
{ ok: false, failure: start.failure, receipt: start.receipt },
|
|
817
|
-
|
|
611
|
+
runnableRequest,
|
|
818
612
|
),
|
|
819
613
|
),
|
|
820
614
|
launch,
|
|
@@ -824,22 +618,88 @@ export async function startReportWave(
|
|
|
824
618
|
ok: true,
|
|
825
619
|
handle: start.handle,
|
|
826
620
|
result: start.result.then((run) =>
|
|
827
|
-
settleWithSkillFailures(settleReportWave(run,
|
|
621
|
+
settleWithSkillFailures(settleReportWave(run, runnableRequest)),
|
|
828
622
|
),
|
|
829
623
|
launch,
|
|
830
624
|
};
|
|
831
625
|
}
|
|
832
626
|
|
|
627
|
+
const STILL_RUNNING = Symbol("wave-still-running");
|
|
628
|
+
|
|
833
629
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
630
|
+
* The one internal core both factories share: an instance-owned pending map over a per-launch
|
|
631
|
+
* adapter supplier. Pending state belongs to the wave INSTANCE — `waveB.collect(refFromA)` is
|
|
632
|
+
* `"none"` structurally — and the WeakMap plus the settled drain's delete both release retained
|
|
633
|
+
* results promptly.
|
|
837
634
|
*/
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
)
|
|
843
|
-
|
|
844
|
-
|
|
635
|
+
function waveOver(supplyAdapter: () => WaveAdapter): ReportWave {
|
|
636
|
+
const records = new WeakMap<ReportWaveRef, PendingRecord>();
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
async start(request, control) {
|
|
640
|
+
const start = await startWave(supplyAdapter, request, control?.signal);
|
|
641
|
+
if (!start.ok) {
|
|
642
|
+
return { ok: false, result: start.result, launch: start.launch };
|
|
643
|
+
}
|
|
644
|
+
// The ONE mint site — the isolated assertion, immediately after the runtime evidence of
|
|
645
|
+
// a successful launch. The keys snapshot is frozen and copied, never an alias of the
|
|
646
|
+
// returned manifest.
|
|
647
|
+
const ref = {} as ReportWaveRef;
|
|
648
|
+
records.set(ref, { keys: Object.freeze([...start.launch.requested]), result: start.result });
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
ref,
|
|
652
|
+
runId: start.handle.asyncId,
|
|
653
|
+
asyncDir: start.handle.asyncDir,
|
|
654
|
+
launch: start.launch,
|
|
655
|
+
};
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
async collect(ref) {
|
|
659
|
+
const record = records.get(ref);
|
|
660
|
+
if (record === undefined) return { kind: "none" };
|
|
661
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
662
|
+
let raced: ReportWaveResult | typeof STILL_RUNNING;
|
|
663
|
+
try {
|
|
664
|
+
raced = await Promise.race([
|
|
665
|
+
record.result,
|
|
666
|
+
new Promise<typeof STILL_RUNNING>((resolve) => {
|
|
667
|
+
timer = setTimeout(() => resolve(STILL_RUNNING), collectGraceMs());
|
|
668
|
+
}),
|
|
669
|
+
]);
|
|
670
|
+
} finally {
|
|
671
|
+
clearTimeout(timer);
|
|
672
|
+
}
|
|
673
|
+
if (raced === STILL_RUNNING) {
|
|
674
|
+
// The record stays in the map: its bound remains the module-owned wave timeout, and a
|
|
675
|
+
// later collect drains whatever it settles into.
|
|
676
|
+
return { kind: "running" };
|
|
677
|
+
}
|
|
678
|
+
// The delete IS the atomic drain claim: single-threaded JS makes the post-await
|
|
679
|
+
// delete-as-claim exact — overlapping collects of one ref yield exactly one settled
|
|
680
|
+
// winner; the loser (already-deleted) answers `"none"`.
|
|
681
|
+
if (!records.delete(ref)) return { kind: "none" };
|
|
682
|
+
return { kind: "settled", keys: record.keys, result: raced };
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
async run(request, control) {
|
|
686
|
+
const start = await startWave(supplyAdapter, request, control?.signal);
|
|
687
|
+
return start.ok ? await start.result : start.result;
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* The PRODUCTION factory — the wave owns adapter selection: constructs a FRESH rpc adapter per
|
|
694
|
+
* launch over the supplied bus (per-execute adapter freshness; no shared mutable ping state).
|
|
695
|
+
* One per-activation instance is constructed at the composition root (`extension/index.ts`) and
|
|
696
|
+
* threaded to the installers.
|
|
697
|
+
*/
|
|
698
|
+
export function createReportWave(bus: WaveBus): ReportWave {
|
|
699
|
+
return waveOver(() => createRpcWaveAdapter(bus));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** The injection seam (tests; the same internal core over one supplied adapter). */
|
|
703
|
+
export function reportWaveOver(adapter: WaveAdapter): ReportWave {
|
|
704
|
+
return waveOver(() => adapter);
|
|
845
705
|
}
|