@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
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
// The session identity lifecycle (contracts.md §8.2/§8.3) as named, Pi-free session
|
|
2
|
+
// operations: decide what `session_start` should do (claim / fork / adopt / mint / keep) and
|
|
3
|
+
// perform the workflow-state establishment for the decided arm — the ONE combined claim entry
|
|
4
|
+
// with establish-before-consume, the derived fork/adopt identities, the warm mint, and the
|
|
5
|
+
// deliberate keep-arm non-write (reload-generation reconstruction IS the LWW rebuild; no
|
|
6
|
+
// version backfill) — then the two-phase startup facts around the gate: `sessionStartToolScope`
|
|
7
|
+
// (pure; the mode/stage slice the gate syncs from BEFORE any fallible read) and
|
|
8
|
+
// `resolveSessionStartFacts` (post-gate; the lazy launched-stage linkage reconciliation plus the
|
|
9
|
+
// implementation-capture and feedback-receiver inputs), with `sessionTreeFacts` as the
|
|
10
|
+
// navigation twin over the already-rebuilt selected-branch state.
|
|
11
|
+
//
|
|
12
|
+
// Pi-free by construction (importDirectionGuard Rule D): effects arrive through narrow
|
|
13
|
+
// injection points — `SessionStateStore` (the workflow-state slice: rebuild + plain append +
|
|
14
|
+
// the strict verified append), `SessionIdentityPorts` (handoff read/consume, run-scratch
|
|
15
|
+
// isolation, the run-id mint, the §8.3 version stamp), and `SessionStartFactReads` (the handoff
|
|
16
|
+
// + checkout plan-ref reads the post-gate facts may touch). `index.ts` binds the production
|
|
17
|
+
// values, orders the Pi effects, and renders the outcome's per-arm problems/warnings with today's
|
|
18
|
+
// exact report scopes; the strict appends keep reporting through
|
|
19
|
+
// `appendWorkflowStateClassified`'s own loudness channel (the report slice rides
|
|
20
|
+
// `SessionArtifactCtx`, re-exported via `substrate/sessionData.ts` — this module never imports
|
|
21
|
+
// `surfaces/`, `pi/`, or the feedback receiver).
|
|
22
|
+
//
|
|
23
|
+
// ONE handoff authority: every handoff/run-id read — the claim arm's, `decideClaim`'s
|
|
24
|
+
// env-child probe, `resolveRunStage`'s stage lookup, and `deriveForkRunId`'s sibling scan —
|
|
25
|
+
// flows through the injected reads (`SessionIdentityReads`, the read slice of
|
|
26
|
+
// `SessionIdentityPorts`), so the lifecycle is genuinely independent of the cache backing and
|
|
27
|
+
// the fakes never touch disk. The decision logic is byte-identical to its
|
|
28
|
+
// `substrate/workflowState.ts` ancestry.
|
|
29
|
+
|
|
30
|
+
import type { Handoff, PlanRef } from "../substrate/cache.ts";
|
|
31
|
+
import { type Registry, stageConsumesPlanRef } from "../substrate/registry.ts";
|
|
32
|
+
import type { SessionArtifactCtx } from "../substrate/sessionData.ts";
|
|
33
|
+
import {
|
|
34
|
+
type AppendWorkflowStateOpts,
|
|
35
|
+
appendWorkflowStateClassified,
|
|
36
|
+
branchOf,
|
|
37
|
+
type ClassifiedAppend,
|
|
38
|
+
type EntrySink,
|
|
39
|
+
planRefsEqual,
|
|
40
|
+
rebuildWorkflowState,
|
|
41
|
+
WORKFLOW_STATE_TYPE,
|
|
42
|
+
type WorkflowState,
|
|
43
|
+
} from "../substrate/workflowState.ts";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The exterior reads the lifecycle's decision tier needs — the read slice of
|
|
47
|
+
* `SessionIdentityPorts` (production bound to `substrate/cache.ts` by `index.ts`; the test
|
|
48
|
+
* suites bind fakes).
|
|
49
|
+
*/
|
|
50
|
+
export interface SessionIdentityReads {
|
|
51
|
+
/** The cold-launch handoff blob for `runId`, or null (missing/unreadable). */
|
|
52
|
+
readHandoff(runId: string): Handoff | null;
|
|
53
|
+
/** The existing run ids under `scratch/runs/` (the fork/adopt sibling-derivation scan). */
|
|
54
|
+
listRunIds(): string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive a fork-child run_id: `<parent>.<n>` where `n` is the max existing sibling + 1
|
|
59
|
+
* (over the `scratch/runs/` scan), else 1.
|
|
60
|
+
*/
|
|
61
|
+
export function deriveForkRunId(parentRunId: string, runIds: Iterable<string>): string {
|
|
62
|
+
const prefix = `${parentRunId}.`;
|
|
63
|
+
let max = 0;
|
|
64
|
+
for (const id of runIds) {
|
|
65
|
+
if (!id.startsWith(prefix)) continue;
|
|
66
|
+
const segment = id.slice(prefix.length).split(".")[0] ?? "";
|
|
67
|
+
const n = Number.parseInt(segment, 10);
|
|
68
|
+
if (Number.isInteger(n) && n > max) max = n;
|
|
69
|
+
}
|
|
70
|
+
return `${parentRunId}.${max + 1}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ClaimDecision =
|
|
74
|
+
| { action: "keep"; source: "session"; state: WorkflowState }
|
|
75
|
+
| {
|
|
76
|
+
action: "fork";
|
|
77
|
+
source: "fork";
|
|
78
|
+
childRunId: string;
|
|
79
|
+
parentRunId: string;
|
|
80
|
+
state: WorkflowState;
|
|
81
|
+
}
|
|
82
|
+
| { action: "claim"; source: "env"; runId: string }
|
|
83
|
+
| {
|
|
84
|
+
action: "adopt";
|
|
85
|
+
source: "env-child";
|
|
86
|
+
childRunId: string;
|
|
87
|
+
parentRunId: string;
|
|
88
|
+
/** Inherited from the parent's handoff so read-only gating survives into the child. */
|
|
89
|
+
mode?: string;
|
|
90
|
+
}
|
|
91
|
+
| { action: "none"; source: "none"; state: WorkflowState };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The registry stage id the launched run is acting on, read from its handoff blob, or null.
|
|
95
|
+
* Only `claim` (cold) and `keep` (reload) sessions have a settled run whose handoff records a
|
|
96
|
+
* `stage` — a kept session re-reads its run's handoff on every reload, so a consuming stage's
|
|
97
|
+
* checkout binding is re-read there too; `fork`, `adopt`, and `none` carry no launched stage (an
|
|
98
|
+
* adopted env-child must never impersonate the launched stage; LWW restores fork/none state
|
|
99
|
+
* instead). The stage gates whether `session_start` reconciles `cache.plan-ref` into
|
|
100
|
+
* `active_plan_ref`.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveRunStage(
|
|
103
|
+
decision: ClaimDecision,
|
|
104
|
+
reads: Pick<SessionIdentityReads, "readHandoff">,
|
|
105
|
+
): string | null {
|
|
106
|
+
const runId =
|
|
107
|
+
decision.action === "claim"
|
|
108
|
+
? decision.runId
|
|
109
|
+
: decision.action === "keep"
|
|
110
|
+
? decision.state.run_id
|
|
111
|
+
: null;
|
|
112
|
+
if (runId === undefined || runId === null) return null;
|
|
113
|
+
const stage = reads.readHandoff(runId)?.stage;
|
|
114
|
+
return typeof stage === "string" && stage !== "" ? stage : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decide what `session_start` should do, from the rebuilt state + the current session handle
|
|
119
|
+
* + the launch env. Reload vs fork is distinguished by the `run_id ↔ pi_session_id` mapping
|
|
120
|
+
* (NOT `event.reason`, which is "startup" for a headless `pi --fork`): if the branch already
|
|
121
|
+
* carries a `run_id` whose recorded `pi_session_id` differs from the current session, the id
|
|
122
|
+
* was inherited across a fork → derive a child; if it matches (or is absent), it's a reload.
|
|
123
|
+
* An env-inherited run id whose handoff was already CONSUMED by a different session is a
|
|
124
|
+
* spawned child, not the launched session → `adopt` (derive a sibling id, inherit `mode`).
|
|
125
|
+
*/
|
|
126
|
+
export function decideClaim(args: {
|
|
127
|
+
state: WorkflowState;
|
|
128
|
+
currentSessionId: string | null;
|
|
129
|
+
envRunId: string | null;
|
|
130
|
+
reads: SessionIdentityReads;
|
|
131
|
+
}): ClaimDecision {
|
|
132
|
+
const { state, currentSessionId, envRunId, reads } = args;
|
|
133
|
+
if (state.run_id !== undefined) {
|
|
134
|
+
if (state.pi_session_id === undefined || state.pi_session_id === currentSessionId) {
|
|
135
|
+
return { action: "keep", source: "session", state };
|
|
136
|
+
}
|
|
137
|
+
const childRunId = deriveForkRunId(state.run_id, reads.listRunIds());
|
|
138
|
+
return { action: "fork", source: "fork", childRunId, parentRunId: state.run_id, state };
|
|
139
|
+
}
|
|
140
|
+
if (envRunId !== null && envRunId !== "") {
|
|
141
|
+
// Env-child detection (contracts §8.2): subagent children are spawned as separate `pi`
|
|
142
|
+
// processes with the parent's env, so they arrive here carrying the parent's PERK_RUN_ID.
|
|
143
|
+
// A handoff already consumed by a DIFFERENT (or unrecorded) session belongs to someone else:
|
|
144
|
+
// adopt a derived `<run_id>.<n>` child identity instead of re-claiming — never re-consume the
|
|
145
|
+
// handoff, never capture pointers, never impersonate the launched stage. The parent's `mode`
|
|
146
|
+
// is inherited so read-only gating survives into exploration children. Everything else —
|
|
147
|
+
// absent/corrupt/mismatched handoff (the loud unclaimed error), unconsumed (the normal cold
|
|
148
|
+
// claim), or consumed by THIS session (idempotent re-claim after lost branch state) — stays
|
|
149
|
+
// the claim arm.
|
|
150
|
+
const handoff = reads.readHandoff(envRunId);
|
|
151
|
+
if (
|
|
152
|
+
handoff !== null &&
|
|
153
|
+
handoff.run_id === envRunId &&
|
|
154
|
+
handoff.consumed === true &&
|
|
155
|
+
handoff.pi_session_id !== currentSessionId
|
|
156
|
+
) {
|
|
157
|
+
return {
|
|
158
|
+
action: "adopt",
|
|
159
|
+
source: "env-child",
|
|
160
|
+
childRunId: deriveForkRunId(envRunId, reads.listRunIds()),
|
|
161
|
+
parentRunId: envRunId,
|
|
162
|
+
mode: handoff.mode,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { action: "claim", source: "env", runId: envRunId };
|
|
166
|
+
}
|
|
167
|
+
return { action: "none", source: "none", state };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --------------------------------------------------------------- the identity establishment
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The narrow workflow-state store port: the LWW rebuild, the plain (honest-tier, no read-back)
|
|
174
|
+
* append the fork/adopt arms use, and the strict verified append. ONE port serves both the
|
|
175
|
+
* identity lifecycle (which verifies `run_id`) and the session engine
|
|
176
|
+
* (`session/workflowSession.ts`, which verifies each change's own field) — hence the generic
|
|
177
|
+
* verified append.
|
|
178
|
+
*/
|
|
179
|
+
export interface SessionStateStore {
|
|
180
|
+
rebuild(): WorkflowState;
|
|
181
|
+
append(data: WorkflowState): void;
|
|
182
|
+
appendVerified<K extends keyof WorkflowState>(opts: AppendWorkflowStateOpts<K>): ClassifiedAppend;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The production `SessionStateStore` over the live branch: `EntrySink` appends,
|
|
187
|
+
* `rebuildWorkflowState` over the branch, and `appendWorkflowStateClassified` for the strict
|
|
188
|
+
* tier (its report() path stays the loudness channel for read-back failures).
|
|
189
|
+
*/
|
|
190
|
+
export function branchSessionStateStore(
|
|
191
|
+
sink: EntrySink,
|
|
192
|
+
source: SessionArtifactCtx,
|
|
193
|
+
): SessionStateStore {
|
|
194
|
+
return {
|
|
195
|
+
rebuild: () => rebuildWorkflowState(branchOf(source)),
|
|
196
|
+
append: (data) => sink.appendEntry(WORKFLOW_STATE_TYPE, data),
|
|
197
|
+
appendVerified: (opts) => appendWorkflowStateClassified(sink, source, opts),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The exterior effects the lifecycle needs — production bound in `index.ts`, fakes in tests. */
|
|
202
|
+
export interface SessionIdentityPorts extends SessionIdentityReads {
|
|
203
|
+
/** Mark the handoff consumed (establish-before-consume: called only after a verified claim). */
|
|
204
|
+
markHandoffConsumed(runId: string, opts: { piSessionId?: string }): void;
|
|
205
|
+
/** Isolate the derived child's run scratch root (a throw is tolerated — warned, not fatal). */
|
|
206
|
+
ensureRunScratch(runId: string): void;
|
|
207
|
+
/** Mint a fresh run_id for the warm identity-less arm. */
|
|
208
|
+
mintRunId(): string;
|
|
209
|
+
/**
|
|
210
|
+
* The §8.3 exact-vintage stamp recorded by every identity-establishing arm
|
|
211
|
+
* (claim/fork/adopt/mint); undefined (the perkVersion() failure sentinel) drops the key on
|
|
212
|
+
* serialize and leaves the session on the timestamp-estimate arm.
|
|
213
|
+
*/
|
|
214
|
+
versionStamp: string | undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Which lifecycle arm settled (the failed claim/mint arms both read `unclaimed`). */
|
|
218
|
+
export type SessionIdentityArm = "claimed" | "kept" | "forked" | "adopted" | "minted" | "unclaimed";
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* What `session_start` consumes downstream: the settled arm, the resolved state the byte-
|
|
222
|
+
* identical derivations run over (`scopeStage`, `implStage`, the sentinel source —
|
|
223
|
+
* `arm === "minted" ? "mint" : decision.source`), the decision itself, and the per-arm
|
|
224
|
+
* problems/warnings the caller renders with today's exact report scopes ("workflow-state
|
|
225
|
+
* linkage error" `{alsoLog: true}`; "run scratch" warnings).
|
|
226
|
+
*
|
|
227
|
+
* A discriminated union on `arm`, correlating each settled arm with exactly the decision that
|
|
228
|
+
* can produce it: an impossible pair (e.g. `forked` carrying a claim decision) does not
|
|
229
|
+
* compile, and narrowing on `arm` proves the decision's fields. `unclaimed` is the shared
|
|
230
|
+
* failure arm of the two strict-append paths — a failed cold claim (`action: "claim"`) or a
|
|
231
|
+
* failed mint (`action: "none"`); the correlated decision says which.
|
|
232
|
+
*/
|
|
233
|
+
export type EstablishIdentityOutcome = {
|
|
234
|
+
resolved: WorkflowState;
|
|
235
|
+
/** Caller-rendered with scope "workflow-state linkage error" (`{alsoLog: true}`). */
|
|
236
|
+
problems: string[];
|
|
237
|
+
/** Caller-rendered with scope "run scratch" (`{alsoLog: true}`). */
|
|
238
|
+
warnings: string[];
|
|
239
|
+
} & (
|
|
240
|
+
| { arm: "claimed"; decision: Extract<ClaimDecision, { action: "claim" }> }
|
|
241
|
+
| { arm: "kept"; decision: Extract<ClaimDecision, { action: "keep" }> }
|
|
242
|
+
| { arm: "forked"; decision: Extract<ClaimDecision, { action: "fork" }> }
|
|
243
|
+
| { arm: "adopted"; decision: Extract<ClaimDecision, { action: "adopt" }> }
|
|
244
|
+
| { arm: "minted"; decision: Extract<ClaimDecision, { action: "none" }> }
|
|
245
|
+
| { arm: "unclaimed"; decision: Extract<ClaimDecision, { action: "claim" | "none" }> }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/** Reflect a captured restriction without participating in identity or handoff authority. */
|
|
249
|
+
export function reflectSessionReadOnlyFloor(
|
|
250
|
+
store: SessionStateStore,
|
|
251
|
+
outcome: EstablishIdentityOutcome,
|
|
252
|
+
): { outcome: EstablishIdentityOutcome; unexpectedFailure: boolean } {
|
|
253
|
+
if (outcome.arm === "unclaimed" || outcome.resolved.mode === "read-only") {
|
|
254
|
+
return { outcome, unexpectedFailure: false };
|
|
255
|
+
}
|
|
256
|
+
let appended: ClassifiedAppend;
|
|
257
|
+
try {
|
|
258
|
+
appended = store.appendVerified({
|
|
259
|
+
data: { mode: "read-only" },
|
|
260
|
+
field: "mode",
|
|
261
|
+
expected: "read-only",
|
|
262
|
+
scope: "child restriction",
|
|
263
|
+
failure:
|
|
264
|
+
"could not persist child read-only restriction; in-memory restriction remains active",
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
// The Pi edge reports this escaped-contract failure once and continues startup with the floor.
|
|
268
|
+
return { outcome, unexpectedFailure: true };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
outcome:
|
|
272
|
+
appended.status === "applied"
|
|
273
|
+
? { ...outcome, resolved: { ...outcome.resolved, mode: "read-only" } }
|
|
274
|
+
: outcome,
|
|
275
|
+
unexpectedFailure: false,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The refinement stage id (registry vocabulary; the admission check keys on it). */
|
|
280
|
+
const REFINE_STAGE_ID = "objective-refine";
|
|
281
|
+
|
|
282
|
+
/** The top-level handoff keys the plan-graph doors use as planning-link / plan-ref inputs. */
|
|
283
|
+
const PLANNING_LINK_KEYS = [
|
|
284
|
+
"objective_id",
|
|
285
|
+
"node_id",
|
|
286
|
+
"adopt_from",
|
|
287
|
+
"supersedes",
|
|
288
|
+
"gist_scope",
|
|
289
|
+
"consumed_learn",
|
|
290
|
+
] as const;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Which planning-link / plan-ref input (if any) an `objective-refine` handoff carries at the
|
|
294
|
+
* top level — `null` for every non-refinement handoff and for a clean refinement one. A key is
|
|
295
|
+
* "carried" when present and not null/undefined (an empty `consumed_learn` list is clean).
|
|
296
|
+
*/
|
|
297
|
+
export function refinementHandoffContamination(handoff: Handoff): string | null {
|
|
298
|
+
if (handoff.stage !== REFINE_STAGE_ID) return null;
|
|
299
|
+
const carried = PLANNING_LINK_KEYS.filter((key) => {
|
|
300
|
+
const value = handoff[key];
|
|
301
|
+
if (value === undefined || value === null) return false;
|
|
302
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
303
|
+
return true;
|
|
304
|
+
});
|
|
305
|
+
return carried.length === 0 ? null : carried.join(", ");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Establish the session's run identity — the four `session_start` arms as one named operation:
|
|
310
|
+
*
|
|
311
|
+
* - **claim** (cold): read the handoff via the port (missing/mismatched ⇒ `unclaimed` with the
|
|
312
|
+
* loud problem; never falls through to mint) → build the ONE combined entry (`run_id`,
|
|
313
|
+
* `pi_session_id`, `mode`, the `perk_version` stamp, `stage`, and the `objective_node_claim`
|
|
314
|
+
* carrier when the handoff's `objective_id`/`node_id` are both non-blank strings) → ONE
|
|
315
|
+
* strict append verified on `run_id` → only on verified success, consume the handoff
|
|
316
|
+
* (establish-before-consume: a failed read-back ⇒ `unclaimed`, NOT consumed).
|
|
317
|
+
* - **fork** / **adopt**: isolate the derived child's scratch (a throw is a warning; identity
|
|
318
|
+
* still settles) → the derived-identity append — plain appends (honest tier, no read-back).
|
|
319
|
+
* Fork inherits the parent's `mode` and NO `stage` (LWW carries the parent's); adopt takes
|
|
320
|
+
* `mode` from the handoff and never impersonates stage or claim (and never re-consumes).
|
|
321
|
+
* - **mint** (`none`): mint a run_id → strict append verified on `run_id`; a failed read-back
|
|
322
|
+
* leaves the session unidentified (`unclaimed` — re-mints next `session_start`).
|
|
323
|
+
* - **keep** (reload): NO append — reload-generation reconstruction IS the LWW rebuild, and the
|
|
324
|
+
* deliberate no-version-backfill non-write is preserved.
|
|
325
|
+
*/
|
|
326
|
+
export function establishSessionIdentity(
|
|
327
|
+
store: SessionStateStore,
|
|
328
|
+
ports: SessionIdentityPorts,
|
|
329
|
+
input: { currentSessionId: string | null; envRunId: string | null },
|
|
330
|
+
): EstablishIdentityOutcome {
|
|
331
|
+
const problems: string[] = [];
|
|
332
|
+
const warnings: string[] = [];
|
|
333
|
+
const currentSessionId = input.currentSessionId;
|
|
334
|
+
const decision = decideClaim({
|
|
335
|
+
state: store.rebuild(),
|
|
336
|
+
currentSessionId,
|
|
337
|
+
envRunId: input.envRunId,
|
|
338
|
+
reads: ports,
|
|
339
|
+
});
|
|
340
|
+
const stamp = ports.versionStamp;
|
|
341
|
+
|
|
342
|
+
if (decision.action === "claim") {
|
|
343
|
+
// Cold claim — establish before consume (strict).
|
|
344
|
+
const handoff = ports.readHandoff(decision.runId);
|
|
345
|
+
if (handoff === null || handoff.run_id !== decision.runId) {
|
|
346
|
+
problems.push(`handoff missing or mismatched for run ${decision.runId}`);
|
|
347
|
+
return { arm: "unclaimed", resolved: {}, decision, problems, warnings };
|
|
348
|
+
}
|
|
349
|
+
// Refinement admission (contracts.md §8.67): the isolated `objective-refine` stage must
|
|
350
|
+
// never arrive carrying a planning link or plan-ref input — those top-level keys are what
|
|
351
|
+
// the claim arm below (and the plan-save recovery paths) read as a planning claim /
|
|
352
|
+
// adoption. A contaminated refinement handoff is REFUSED before any claim is recorded and
|
|
353
|
+
// is NOT consumed (the door that wrote it is the defect; nothing here rebinds or clears).
|
|
354
|
+
// Ordinary objective-plan handoffs (which legitimately carry objective_id/node_id) are
|
|
355
|
+
// untouched.
|
|
356
|
+
const contamination = refinementHandoffContamination(handoff);
|
|
357
|
+
if (contamination !== null) {
|
|
358
|
+
problems.push(
|
|
359
|
+
`refusing the objective-refine handoff for run ${decision.runId}: it carries ${contamination} (a planning link/plan-ref input a refinement session never accepts)`,
|
|
360
|
+
);
|
|
361
|
+
return { arm: "unclaimed", resolved: {}, decision, problems, warnings };
|
|
362
|
+
}
|
|
363
|
+
// The objective-plan cold door's handoff_extra carries the node link
|
|
364
|
+
// (objective_id/node_id): persist it as the objective_node_claim so the implement-here
|
|
365
|
+
// exits are structurally suppressed in COLD objective-plan sessions too (the warm
|
|
366
|
+
// `objective_node` tool records the claim; a cold factory session never calls it — the
|
|
367
|
+
// door marked the node before launch). Blank/absent ids persist nothing; the claim
|
|
368
|
+
// clears on a successful node-linked save exactly as the warm-recorded one does.
|
|
369
|
+
const handoffObjective = handoff.objective_id;
|
|
370
|
+
const handoffNode = handoff.node_id;
|
|
371
|
+
const nodeClaim =
|
|
372
|
+
typeof handoffObjective === "string" &&
|
|
373
|
+
handoffObjective.trim() !== "" &&
|
|
374
|
+
typeof handoffNode === "string" &&
|
|
375
|
+
handoffNode.trim() !== ""
|
|
376
|
+
? { objective: handoffObjective, node: handoffNode }
|
|
377
|
+
: undefined;
|
|
378
|
+
const data: WorkflowState = {
|
|
379
|
+
run_id: decision.runId,
|
|
380
|
+
pi_session_id: currentSessionId ?? undefined,
|
|
381
|
+
mode: handoff.mode,
|
|
382
|
+
perk_version: stamp,
|
|
383
|
+
// Record the launched stage so the interior can tell e.g. objective-author from plan
|
|
384
|
+
// (both are read-only) and inject the right authoring context.
|
|
385
|
+
stage: handoff.stage,
|
|
386
|
+
...(nodeClaim !== undefined ? { objective_node_claim: nodeClaim } : {}),
|
|
387
|
+
};
|
|
388
|
+
const appended = store.appendVerified({
|
|
389
|
+
data,
|
|
390
|
+
field: "run_id",
|
|
391
|
+
expected: decision.runId,
|
|
392
|
+
scope: "workflow-state linkage error",
|
|
393
|
+
failure: `read-back failed for run ${decision.runId}`,
|
|
394
|
+
});
|
|
395
|
+
if (appended.status !== "applied") {
|
|
396
|
+
// do NOT consume — the strict-append seam already reported the failure loudly.
|
|
397
|
+
return { arm: "unclaimed", resolved: {}, decision, problems, warnings };
|
|
398
|
+
}
|
|
399
|
+
ports.markHandoffConsumed(decision.runId, { piSessionId: currentSessionId ?? undefined });
|
|
400
|
+
return { arm: "claimed", resolved: data, decision, problems, warnings };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (decision.action === "fork" || decision.action === "adopt") {
|
|
404
|
+
// Inherited/adopted run identity → isolate the child's scratch. A static redirect or
|
|
405
|
+
// filesystem failure is loud but does not prevent the derived workflow identity from
|
|
406
|
+
// settling; later eligible turns retry through the agent-scratch resolver. The adopt arm
|
|
407
|
+
// mirrors fork minus everything that belongs to the launched session (contracts §8.2):
|
|
408
|
+
// never re-consume the handoff, no `stage` (no stage impersonation), no claim.
|
|
409
|
+
try {
|
|
410
|
+
ports.ensureRunScratch(decision.childRunId);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const kind = decision.action === "fork" ? "fork" : "adopted";
|
|
413
|
+
warnings.push(
|
|
414
|
+
`could not create ${kind} run root for ${decision.childRunId}: ${String(error)}`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const data: WorkflowState = {
|
|
418
|
+
run_id: decision.childRunId,
|
|
419
|
+
pi_session_id: currentSessionId ?? undefined,
|
|
420
|
+
predecessor: decision.parentRunId,
|
|
421
|
+
mode: decision.action === "fork" ? decision.state.mode : decision.mode,
|
|
422
|
+
perk_version: stamp,
|
|
423
|
+
};
|
|
424
|
+
store.append(data);
|
|
425
|
+
if (decision.action === "fork") {
|
|
426
|
+
return { arm: "forked", resolved: data, decision, problems, warnings };
|
|
427
|
+
}
|
|
428
|
+
return { arm: "adopted", resolved: data, decision, problems, warnings };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (decision.action === "none") {
|
|
432
|
+
// A warm session with no identity mints its own run_id so per-run state (the session data
|
|
433
|
+
// dir) can key off it. No disk artifacts — dirs are the accessor's job; provenance is
|
|
434
|
+
// recorded separately. A failed cold claim above never falls here (claim stays a loud
|
|
435
|
+
// unclaimed error).
|
|
436
|
+
const runId = ports.mintRunId();
|
|
437
|
+
const data: WorkflowState = {
|
|
438
|
+
run_id: runId,
|
|
439
|
+
pi_session_id: currentSessionId ?? undefined,
|
|
440
|
+
perk_version: stamp,
|
|
441
|
+
};
|
|
442
|
+
const appended = store.appendVerified({
|
|
443
|
+
data,
|
|
444
|
+
field: "run_id",
|
|
445
|
+
expected: runId,
|
|
446
|
+
scope: "workflow-state linkage error",
|
|
447
|
+
failure: `read-back failed for minted run ${runId}`,
|
|
448
|
+
});
|
|
449
|
+
if (appended.status === "applied") {
|
|
450
|
+
return {
|
|
451
|
+
arm: "minted",
|
|
452
|
+
resolved: { ...decision.state, ...data },
|
|
453
|
+
decision,
|
|
454
|
+
problems,
|
|
455
|
+
warnings,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// Loud-but-non-fatal (the seam reported): the session stays unidentified and re-mints on
|
|
459
|
+
// the next session_start.
|
|
460
|
+
return { arm: "unclaimed", resolved: decision.state, decision, problems, warnings };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// keep (reload): NO append — reload-generation reconstruction IS the LWW rebuild; the
|
|
464
|
+
// deliberate no-version-backfill non-write is preserved (§8.3: an LWW backfill would
|
|
465
|
+
// mis-stamp an old session with today's version).
|
|
466
|
+
return { arm: "kept", resolved: decision.state, decision, problems, warnings };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ------------------------------------------------------------- the two-phase startup facts
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* PHASE 1 (before the gate) — the tool-scope slice `gating.syncFromState` consumes, derived
|
|
473
|
+
* PURELY from the established identity: no store, handoff, registry, or checkout read can sit
|
|
474
|
+
* between identity establishment and gate synchronization (a read failure must never leave the
|
|
475
|
+
* gate unsynced). Mode is exactly the resolved mode (after optional floor reflection). The scope
|
|
476
|
+
* stage is the workflow-state `stage` key (§8.40): claim → the handoff-recorded stage just
|
|
477
|
+
* appended; keep/none → the branch-LWW stage; fork INHERITS the parent's stage (a forked
|
|
478
|
+
* implement session is an implement session); adopt NEVER impersonates (a subagent child's
|
|
479
|
+
* fresh branch carries no stage, so `session_tree` agrees). A failed claim leaves `resolved`
|
|
480
|
+
* empty → no stage → unscoped (stage scoping is fail-open). No stage validation or
|
|
481
|
+
* normalization — an unknown stage id passes through as-is.
|
|
482
|
+
*/
|
|
483
|
+
export function sessionStartToolScope(
|
|
484
|
+
identity: EstablishIdentityOutcome,
|
|
485
|
+
): Pick<WorkflowState, "mode" | "stage"> {
|
|
486
|
+
const { resolved, decision } = identity;
|
|
487
|
+
const stage =
|
|
488
|
+
decision.action === "adopt"
|
|
489
|
+
? undefined
|
|
490
|
+
: (resolved.stage ?? (decision.action === "fork" ? decision.state.stage : undefined));
|
|
491
|
+
return { mode: resolved.mode, stage };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The exterior reads the post-gate facts may touch — the handoff (the launched-stage authority
|
|
496
|
+
* for claim/keep) and the checkout `cache.plan-ref` (read ONLY on the consuming arm). Production
|
|
497
|
+
* binds both cwd-bound in `index.ts`; the suites bind recording fakes.
|
|
498
|
+
*/
|
|
499
|
+
export interface SessionStartFactReads extends Pick<SessionIdentityReads, "readHandoff"> {
|
|
500
|
+
/** The worktree's `cache.plan-ref` selector, or null (missing/unreadable). */
|
|
501
|
+
readPlanRef(): PlanRef | null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The receiver-shaped feedback inputs both startup and navigation derive. Deliberately a local,
|
|
506
|
+
* inferred structural shape (no shared vocabulary with `hunkFeedback/receiver.ts`): the
|
|
507
|
+
* `index.ts` call site adds Pi's run `mode` and the receiver's own `ReceiverSyncArgs` accepts
|
|
508
|
+
* the result structurally. Eligibility stays receiver-owned (its fresh checkout read included).
|
|
509
|
+
*/
|
|
510
|
+
interface SessionFeedbackFacts {
|
|
511
|
+
stage: string | null;
|
|
512
|
+
adopted: boolean;
|
|
513
|
+
runId: string | null;
|
|
514
|
+
piSessionId: string | null;
|
|
515
|
+
activePlanRef: PlanRef | null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* PHASE 2 (after the gate and the claimed-only refinement import) — the lazy linkage
|
|
520
|
+
* reconciliation plus the derived capture/feedback inputs. `resolved` is the same
|
|
521
|
+
* `WorkflowState`-shaped diagnostic startup always produced (the T3 sentinel's material) —
|
|
522
|
+
* lifecycle-local, not a feature-facing snapshot.
|
|
523
|
+
*/
|
|
524
|
+
export interface SessionStartFacts {
|
|
525
|
+
resolved: WorkflowState;
|
|
526
|
+
/** Present only for an identified implement session (launched or fork-inherited stage). */
|
|
527
|
+
implementationCapture: { runId: string; parentSessionId: string | null } | null;
|
|
528
|
+
feedback: SessionFeedbackFacts;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The post-gate startup facts. Operation order (each step's port is the only one it touches):
|
|
533
|
+
*
|
|
534
|
+
* 1. Rebuild the live linked `active_plan_ref` ONCE (the store), before any handoff read.
|
|
535
|
+
* 2. Resolve the launched stage through the handoff reader — claim/keep only (`resolveRunStage`);
|
|
536
|
+
* fork/adopt/none never read a stage handoff here.
|
|
537
|
+
* 3. Registry admission: only a non-null launched stage can consume the checkout ref
|
|
538
|
+
* (`stageConsumesPlanRef`); a null registry stays PERMISSIVE when a stage is present (to
|
|
539
|
+
* preserve implement linkage); an unknown stage in an available registry does not consume.
|
|
540
|
+
* 4. On the consuming arm ONLY, read the checkout `cache.plan-ref`: same `(provider, pr_id)`
|
|
541
|
+
* keeps the existing linked object without appending; a different ref performs ONE strict
|
|
542
|
+
* verified append (`active_plan_ref`, `planRefsEqual`, the "workflow-state linkage error"
|
|
543
|
+
* scope) and folds the new ref in ONLY on `applied` — a rejected/unverified append leaves
|
|
544
|
+
* `resolved` exactly as it arrived (a kept session keeps its LWW ref; a fresh claim keeps
|
|
545
|
+
* none — never flattened, never rebuilt again to manufacture a fallback); no cached ref
|
|
546
|
+
* preserves a non-null linked ref. A non-consuming (or absent) launched stage preserves a
|
|
547
|
+
* non-null linked ref WITHOUT reading the checkout — the root selector must not leak in.
|
|
548
|
+
* 5. The implementation stage is the launched stage, with only a fork falling back to its
|
|
549
|
+
* parent's LWW stage; `implementationCapture` exists only for a truthy run id on
|
|
550
|
+
* `stage === "implement"` (parent provenance comes only from the fork decision).
|
|
551
|
+
* 6. `feedback` carries the implementation stage, the decision's adoption, the resolved
|
|
552
|
+
* run/ref, and startup's CURRENT Pi session handle (never an inherited branch handle).
|
|
553
|
+
*
|
|
554
|
+
* A throwing store/read propagates to the caller (Pi's hook error boundary) — unreadability is
|
|
555
|
+
* never turned into confirmed absence, and no later effect runs from guessed facts.
|
|
556
|
+
*/
|
|
557
|
+
export function resolveSessionStartFacts(
|
|
558
|
+
store: SessionStateStore,
|
|
559
|
+
reads: SessionStartFactReads,
|
|
560
|
+
input: {
|
|
561
|
+
identity: EstablishIdentityOutcome;
|
|
562
|
+
/** The already-loaded registry (null when it failed to load) — never a loader. */
|
|
563
|
+
registry: Registry | null;
|
|
564
|
+
currentSessionId: string | null;
|
|
565
|
+
},
|
|
566
|
+
): SessionStartFacts {
|
|
567
|
+
const { identity, registry, currentSessionId } = input;
|
|
568
|
+
const decision = identity.decision;
|
|
569
|
+
let resolved: WorkflowState = identity.resolved;
|
|
570
|
+
|
|
571
|
+
const linked = store.rebuild().active_plan_ref ?? null;
|
|
572
|
+
const launchedStage = resolveRunStage(decision, reads);
|
|
573
|
+
const consumesPlanRef =
|
|
574
|
+
launchedStage !== null && (registry === null || stageConsumesPlanRef(registry, launchedStage));
|
|
575
|
+
if (consumesPlanRef) {
|
|
576
|
+
const cachedRef = reads.readPlanRef();
|
|
577
|
+
if (cachedRef !== null) {
|
|
578
|
+
if (planRefsEqual(linked, cachedRef)) {
|
|
579
|
+
resolved = { ...resolved, active_plan_ref: linked };
|
|
580
|
+
} else {
|
|
581
|
+
const appended = store.appendVerified({
|
|
582
|
+
data: { active_plan_ref: cachedRef },
|
|
583
|
+
field: "active_plan_ref",
|
|
584
|
+
expected: cachedRef,
|
|
585
|
+
scope: "workflow-state linkage error",
|
|
586
|
+
failure: `plan-ref read-back failed for ${cachedRef.provider}:${cachedRef.pr_id}`,
|
|
587
|
+
equals: planRefsEqual,
|
|
588
|
+
});
|
|
589
|
+
if (appended.status === "applied") resolved = { ...resolved, active_plan_ref: cachedRef };
|
|
590
|
+
}
|
|
591
|
+
} else if (linked !== null) {
|
|
592
|
+
resolved = { ...resolved, active_plan_ref: linked };
|
|
593
|
+
}
|
|
594
|
+
} else if (linked !== null) {
|
|
595
|
+
resolved = { ...resolved, active_plan_ref: linked };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const implementationStage =
|
|
599
|
+
launchedStage ?? (decision.action === "fork" ? (decision.state.stage ?? null) : null);
|
|
600
|
+
const implementationCapture =
|
|
601
|
+
resolved.run_id && implementationStage === "implement"
|
|
602
|
+
? {
|
|
603
|
+
runId: resolved.run_id,
|
|
604
|
+
parentSessionId:
|
|
605
|
+
decision.action === "fork" ? (decision.state.pi_session_id ?? null) : null,
|
|
606
|
+
}
|
|
607
|
+
: null;
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
resolved,
|
|
611
|
+
implementationCapture,
|
|
612
|
+
feedback: {
|
|
613
|
+
stage: implementationStage,
|
|
614
|
+
adopted: decision.action === "adopt",
|
|
615
|
+
runId: resolved.run_id ?? null,
|
|
616
|
+
piSessionId: currentSessionId,
|
|
617
|
+
activePlanRef: resolved.active_plan_ref ?? null,
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* The navigation twin (`session_tree`), PURE over the ONE already-rebuilt selected-branch state:
|
|
624
|
+
* tool scope is the branch's per-field-LWW mode/stage (the §8.40 key); feedback uses the
|
|
625
|
+
* branch's stage/run/ref AND ITS OWN recorded session id (not startup's current-handle
|
|
626
|
+
* override) with `adopted: false` — an env-adopted child's fresh branch carries no stage, so the
|
|
627
|
+
* stage gate alone keeps it receiver-ineligible. No handoff/checkout read, claim, linkage, or
|
|
628
|
+
* implementation capture happens on navigation (a deliberate asymmetry with startup).
|
|
629
|
+
*/
|
|
630
|
+
export function sessionTreeFacts(state: WorkflowState): {
|
|
631
|
+
toolScope: Pick<WorkflowState, "mode" | "stage">;
|
|
632
|
+
feedback: SessionFeedbackFacts;
|
|
633
|
+
} {
|
|
634
|
+
return {
|
|
635
|
+
toolScope: { mode: state.mode, stage: state.stage },
|
|
636
|
+
feedback: {
|
|
637
|
+
stage: state.stage ?? null,
|
|
638
|
+
adopted: false,
|
|
639
|
+
runId: state.run_id ?? null,
|
|
640
|
+
piSessionId: state.pi_session_id ?? null,
|
|
641
|
+
activePlanRef: state.active_plan_ref ?? null,
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
}
|