@mgiles/perk 3.2.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 +365 -380
- 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 -311
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
- package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
- 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 +154 -267
- 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 +123 -61
- 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/pi/v1/lifecycleGates.ts +127 -0
- 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/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
- 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 +73 -27
- 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 -21
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +5 -4
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +263 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +178 -163
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +87 -46
- 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 +43 -18
- 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/objective-refinement.md +17 -0
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -6
- package/prompts/stages/conflict-resolution.md +4 -4
- 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 +1 -1
- 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-browser/stack.md +5 -6
- package/shared/README.md +8 -0
- package/shared/bindings.yaml +3 -3
- package/shared/contracts.md +2601 -506
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +13 -0
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
- 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 -489
- package/extension/doors/learn.ts +0 -668
- package/extension/doors/lifecycleGates.ts +0 -207
- package/extension/doors/objectiveStack.ts +0 -1543
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -279
- package/extension/doors/submit.ts +0 -373
- package/extension/doors/submitPrReview.ts +0 -505
- 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/objectiveSave.ts +0 -366
- 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
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// The `perk:workflow-state` session tier (contracts.md §8.3) — rebuild
|
|
2
|
-
//
|
|
1
|
+
// The `perk:workflow-state` session tier (contracts.md §8.3) — rebuild and the strict-append
|
|
2
|
+
// seam (appendWorkflowState). The identity-lifecycle decisions (claim/fork-derive/stage
|
|
3
|
+
// resolution) live in `session/lifecycle.ts` — the named session operation.
|
|
3
4
|
//
|
|
4
5
|
// Mostly pure, fs-light logic kept separate from the `pi`/`ctx` effects (in index.ts); the
|
|
5
6
|
// strict-append seam touches effects only through structural slices (`EntrySink`, `BranchSource`,
|
|
6
7
|
// `ReportTarget`), so the whole module stays unit-testable under `node --test` with fakes. The
|
|
7
8
|
// reconstruction discipline (scan getBranch on session_start AND session_tree, per-field LWW)
|
|
8
|
-
//
|
|
9
|
+
// lives here.
|
|
9
10
|
|
|
10
11
|
import { type ReportTarget, report } from "../surfaces/report.ts";
|
|
11
|
-
import {
|
|
12
|
+
import { type PlanRef, readPlanRef } from "./cache.ts";
|
|
12
13
|
|
|
13
14
|
export const WORKFLOW_STATE_TYPE = "perk:workflow-state";
|
|
14
15
|
|
|
@@ -16,6 +17,9 @@ export const WORKFLOW_STATE_TYPE = "perk:workflow-state";
|
|
|
16
17
|
* A session-artifact provenance pointer (contracts §8.3): the session tier's proof
|
|
17
18
|
* that a `scratch/runs/<run_id>/data/` file is current for THIS run. Reads validate the
|
|
18
19
|
* on-disk file against the rebuilt pointer (run_id match + digest match) and refuse otherwise.
|
|
20
|
+
* This is the PERSISTENCE format only — rebuilt branch data is unvalidated, so consumption
|
|
21
|
+
* flows through the session engine's exported decode (`session/workflowSession.ts`), which
|
|
22
|
+
* narrows exactly the fields it validates.
|
|
19
23
|
*/
|
|
20
24
|
export interface SessionArtifactPointer {
|
|
21
25
|
run_id: string;
|
|
@@ -65,8 +69,13 @@ export interface WorkflowState {
|
|
|
65
69
|
* Best-effort tier (per-field LWW in `rebuildWorkflowState`, no rebuild change).
|
|
66
70
|
*/
|
|
67
71
|
review_posts?: unknown;
|
|
68
|
-
/**
|
|
69
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Session-artifact provenance pointers, keyed by artifact name (§8.3). The persisted wire
|
|
74
|
+
* shape is `SessionArtifactPointer`, but rebuilt branch data is UNVALIDATED (`rebuildWorkflowState`
|
|
75
|
+
* trusts entry data) — so the values are `unknown` and consumption goes through the session
|
|
76
|
+
* engine's exported persisted-pointer decode (`session/workflowSession.ts`), never a cast.
|
|
77
|
+
*/
|
|
78
|
+
session_artifacts?: Record<string, unknown> | null;
|
|
70
79
|
/**
|
|
71
80
|
* The objective node this session has claimed `planning` (§8.3) — the warm
|
|
72
81
|
* node-link carrier an approval-triggered save recovers from. Written by the `objective_node`
|
|
@@ -120,45 +129,19 @@ export function branchOf(source: BranchSource): BranchEntry[] {
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
/**
|
|
123
|
-
* Whether any entry on the branch already carries `needle` — the
|
|
124
|
-
* (the
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
132
|
+
* Whether any entry on the FULL selected branch already carries `needle` — the historical
|
|
133
|
+
* once-per-selected-branch guard (the toolGating read-only marker form): history across
|
|
134
|
+
* compaction, not live model context. Serializing each entry is the robust, shape-agnostic scan;
|
|
135
|
+
* safe while the needle is a distinctive literal that other entries' data can't casually contain
|
|
136
|
+
* (known accepted false positive: a tool result quoting perk's own source; the typed customType
|
|
137
|
+
* scan is the escalation if that bites — docs/learned/pi/context-injection.md). Live-delivery
|
|
138
|
+
* evidence ("is the copy still in model context?") is a different authority — Pi's own projection
|
|
139
|
+
* via `pi/v1/contextEvidence.ts` — never reconstructed from branch entries here.
|
|
128
140
|
*/
|
|
129
141
|
export function branchCarries(branch: readonly BranchEntry[], needle: string): boolean {
|
|
130
142
|
return branch.some((entry) => JSON.stringify(entry).includes(needle));
|
|
131
143
|
}
|
|
132
144
|
|
|
133
|
-
/**
|
|
134
|
-
* The branch entries still represented directly in model context. Before compaction that is the
|
|
135
|
-
* full branch. After compaction, Pi keeps entries from `firstKeptEntryId` onward plus anything
|
|
136
|
-
* appended later; historical entries before that cutoff survive only through the summary.
|
|
137
|
-
* Compaction entries are excluded because text quoted by a summary is not a live custom block.
|
|
138
|
-
*/
|
|
139
|
-
export function activeContextWindow(branch: readonly BranchEntry[]): BranchEntry[] {
|
|
140
|
-
let latestCompaction = -1;
|
|
141
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
142
|
-
if (branch[i]?.type === "compaction") {
|
|
143
|
-
latestCompaction = i;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (latestCompaction === -1) return [...branch];
|
|
148
|
-
|
|
149
|
-
const firstKeptEntryId = (branch[latestCompaction] as { firstKeptEntryId?: unknown })
|
|
150
|
-
.firstKeptEntryId;
|
|
151
|
-
const firstKept =
|
|
152
|
-
typeof firstKeptEntryId === "string"
|
|
153
|
-
? branch.findIndex(
|
|
154
|
-
(entry, index) =>
|
|
155
|
-
index < latestCompaction && (entry as { id?: unknown }).id === firstKeptEntryId,
|
|
156
|
-
)
|
|
157
|
-
: -1;
|
|
158
|
-
const start = firstKept === -1 ? latestCompaction + 1 : firstKept;
|
|
159
|
-
return branch.slice(start).filter((entry) => entry.type !== "compaction");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
145
|
/**
|
|
163
146
|
* Per-field last-write-wins over the `perk:workflow-state` custom entries on a branch.
|
|
164
147
|
* Non-perk entries are ignored; `undefined` fields never clobber (but explicit `null` does).
|
|
@@ -188,156 +171,188 @@ export interface EntrySink {
|
|
|
188
171
|
export function appendWorkflowState<K extends keyof WorkflowState>(
|
|
189
172
|
sink: EntrySink,
|
|
190
173
|
source: BranchSource & ReportTarget,
|
|
191
|
-
opts:
|
|
192
|
-
/** The entry payload — may carry extra fields beyond the verified one (the claim record). */
|
|
193
|
-
data: WorkflowState;
|
|
194
|
-
/** The field verified on read-back. */
|
|
195
|
-
field: K;
|
|
196
|
-
/** The value the rebuilt field must equal. */
|
|
197
|
-
expected: WorkflowState[K];
|
|
198
|
-
/** report() scope, e.g. "plan-save", "workflow-state linkage error". */
|
|
199
|
-
scope: string;
|
|
200
|
-
/** The mismatch message (byte-preserved per site). */
|
|
201
|
-
failure: string;
|
|
202
|
-
/** Comparator; default: (a, b) => Object.is(a ?? null, b ?? null). */
|
|
203
|
-
equals?: (rebuilt: WorkflowState[K] | undefined, expected: WorkflowState[K]) => boolean;
|
|
204
|
-
},
|
|
174
|
+
opts: AppendWorkflowStateOpts<K>,
|
|
205
175
|
): boolean {
|
|
176
|
+
return appendWorkflowStateClassified(sink, source, opts).status === "applied";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The strict-append options (shared by the boolean and classified entry points). */
|
|
180
|
+
export interface AppendWorkflowStateOpts<K extends keyof WorkflowState> {
|
|
181
|
+
/** The entry payload — may carry extra fields beyond the verified one (the claim record). */
|
|
182
|
+
data: WorkflowState;
|
|
183
|
+
/** The field verified on read-back. */
|
|
184
|
+
field: K;
|
|
185
|
+
/** The value the rebuilt field must equal. */
|
|
186
|
+
expected: WorkflowState[K];
|
|
187
|
+
/** report() scope, e.g. "plan-save", "workflow-state linkage error". */
|
|
188
|
+
scope: string;
|
|
189
|
+
/** The mismatch message (byte-preserved per site). */
|
|
190
|
+
failure: string;
|
|
191
|
+
/** Comparator; default: (a, b) => Object.is(a ?? null, b ?? null). */
|
|
192
|
+
equals?: (rebuilt: WorkflowState[K] | undefined, expected: WorkflowState[K]) => boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The classified strict-append outcome. `rejected` is PROVEN refusal-before-effect: the append
|
|
197
|
+
* threw AND the rebuilt field is still not the expected value, so no entry landed. `unverified`
|
|
198
|
+
* means an effect may have landed unproven — the append returned but the read-back missed, or
|
|
199
|
+
* the post-throw rebuild itself failed.
|
|
200
|
+
*/
|
|
201
|
+
export type ClassifiedAppend =
|
|
202
|
+
| { status: "applied" }
|
|
203
|
+
| { status: "rejected"; problem: string }
|
|
204
|
+
| { status: "unverified"; problem: string };
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The classified sibling of `appendWorkflowState` (same report discipline — every failure arm
|
|
208
|
+
* reports loudly before returning). The extra classification work happens only on failure
|
|
209
|
+
* paths: a throwing `appendEntry` is re-checked against the rebuilt branch — a field still not
|
|
210
|
+
* equal to `expected` proves the entry never landed (`rejected`, the refusal-before-effect arm
|
|
211
|
+
* the session seam surfaces); a field that DOES equal `expected` landed despite the throw (the
|
|
212
|
+
* read-back is the proof authority — `applied`); a rebuild failure stays honest (`unverified`).
|
|
213
|
+
*/
|
|
214
|
+
export function appendWorkflowStateClassified<K extends keyof WorkflowState>(
|
|
215
|
+
sink: EntrySink,
|
|
216
|
+
source: BranchSource & ReportTarget,
|
|
217
|
+
opts: AppendWorkflowStateOpts<K>,
|
|
218
|
+
): ClassifiedAppend {
|
|
206
219
|
const equals =
|
|
207
220
|
opts.equals ??
|
|
208
221
|
((a: WorkflowState[K] | undefined, b: WorkflowState[K]) => Object.is(a ?? null, b ?? null));
|
|
209
222
|
try {
|
|
210
223
|
sink.appendEntry(WORKFLOW_STATE_TYPE, opts.data);
|
|
211
224
|
const rebuilt = rebuildWorkflowState(branchOf(source))[opts.field];
|
|
212
|
-
if (equals(rebuilt, opts.expected)) return
|
|
225
|
+
if (equals(rebuilt, opts.expected)) return { status: "applied" };
|
|
213
226
|
report(source, opts.scope, "error", opts.failure, { alsoLog: true });
|
|
214
|
-
return
|
|
227
|
+
return { status: "unverified", problem: opts.failure };
|
|
215
228
|
} catch (error) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
const problem = `${String(opts.field)} append threw — ${String(error)}`;
|
|
230
|
+
report(source, opts.scope, "error", problem, { alsoLog: true });
|
|
231
|
+
try {
|
|
232
|
+
const rebuilt = rebuildWorkflowState(branchOf(source))[opts.field];
|
|
233
|
+
if (equals(rebuilt, opts.expected)) return { status: "applied" };
|
|
234
|
+
return { status: "rejected", problem };
|
|
235
|
+
} catch {
|
|
236
|
+
return { status: "unverified", problem };
|
|
237
|
+
}
|
|
220
238
|
}
|
|
221
239
|
}
|
|
222
240
|
|
|
223
241
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
242
|
+
* The rebuilt `conflict_resolution_attempts` counter, read WITHOUT a catch: a throwing
|
|
243
|
+
* `getBranch()` propagates — unreadable bounding state is never treated as proven zero (the
|
|
244
|
+
* load-bearing failure path both conflict drives rely on). A READABLE but malformed persisted
|
|
245
|
+
* value (non-integer, negative, string, object, null, absent) narrows to 0.
|
|
226
246
|
*/
|
|
227
|
-
export function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (a === null || a === undefined || b === null || b === undefined) {
|
|
232
|
-
return (a === null || a === undefined) && (b === null || b === undefined);
|
|
233
|
-
}
|
|
234
|
-
return a.provider === b.provider && a.pr_id === b.pr_id;
|
|
247
|
+
export function conflictResolutionAttempts(source: BranchSource): number {
|
|
248
|
+
const value = rebuildWorkflowState(branchOf(source)).conflict_resolution_attempts;
|
|
249
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
250
|
+
return 0;
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
/**
|
|
238
|
-
*
|
|
239
|
-
* (
|
|
254
|
+
* The checked `conflict_resolution_attempts` write: a non-integer/negative value is refused
|
|
255
|
+
* loudly (`false`, no append — the reader narrows such values to 0, so persisting one would
|
|
256
|
+
* silently reopen the budget); equal-value short-circuit (no append) → `true`; otherwise the
|
|
257
|
+
* strict-read-back boolean from `appendWorkflowState` (its loud report path is the loudness
|
|
258
|
+
* channel). The counter is shared across the two warm conflict drives, so
|
|
259
|
+
* `scope` names the TRUE writing surface for failure reports — `/submit` passes "submit", the
|
|
260
|
+
* stack door "objective-sync".
|
|
240
261
|
*/
|
|
241
|
-
export function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
262
|
+
export function setConflictAttempts(
|
|
263
|
+
sink: EntrySink,
|
|
264
|
+
source: BranchSource & ReportTarget,
|
|
265
|
+
opts: { attempts: number; scope: string },
|
|
266
|
+
): boolean {
|
|
267
|
+
// The write seam enforces the same invariant the reader narrows by: persisting a value the
|
|
268
|
+
// reader would coerce to 0 would silently reopen the conflict budget on the next read, so an
|
|
269
|
+
// invalid write is refused loudly instead of persisted (invalid counter states are
|
|
270
|
+
// unrepresentable through this seam).
|
|
271
|
+
if (!Number.isInteger(opts.attempts) || opts.attempts < 0) {
|
|
272
|
+
report(
|
|
273
|
+
source,
|
|
274
|
+
opts.scope,
|
|
275
|
+
"warning",
|
|
276
|
+
`refused an invalid conflict_resolution_attempts write (${opts.attempts}) — the counter is a non-negative integer`,
|
|
277
|
+
{ alsoLog: true },
|
|
278
|
+
);
|
|
279
|
+
return false;
|
|
249
280
|
}
|
|
250
|
-
|
|
281
|
+
if (conflictResolutionAttempts(source) === opts.attempts) return true;
|
|
282
|
+
return appendWorkflowState(sink, source, {
|
|
283
|
+
data: { conflict_resolution_attempts: opts.attempts },
|
|
284
|
+
field: "conflict_resolution_attempts",
|
|
285
|
+
expected: opts.attempts,
|
|
286
|
+
scope: opts.scope,
|
|
287
|
+
failure:
|
|
288
|
+
opts.attempts === 0
|
|
289
|
+
? "conflict_resolution_attempts reset read-back failed (expected 0)"
|
|
290
|
+
: `conflict_resolution_attempts read-back failed (expected ${opts.attempts})`,
|
|
291
|
+
});
|
|
251
292
|
}
|
|
252
293
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
| {
|
|
264
|
-
action: "adopt";
|
|
265
|
-
source: "env-child";
|
|
266
|
-
childRunId: string;
|
|
267
|
-
parentRunId: string;
|
|
268
|
-
/** Inherited from the parent's handoff so read-only gating survives into the child. */
|
|
269
|
-
mode?: string;
|
|
270
|
-
}
|
|
271
|
-
| { action: "none"; source: "none"; state: WorkflowState };
|
|
294
|
+
/** Structural claim equality (objective + node match); absent compares equal only to absent. */
|
|
295
|
+
export function nodeClaimsEqual(
|
|
296
|
+
a: WorkflowState["objective_node_claim"] | undefined,
|
|
297
|
+
b: WorkflowState["objective_node_claim"] | undefined,
|
|
298
|
+
): boolean {
|
|
299
|
+
const an = a ?? null;
|
|
300
|
+
const bn = b ?? null;
|
|
301
|
+
if (an === null || bn === null) return an === bn;
|
|
302
|
+
return an.objective === bn.objective && an.node === bn.node;
|
|
303
|
+
}
|
|
272
304
|
|
|
273
305
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* was inherited across a fork → derive a child; if it matches (or is absent), it's a reload.
|
|
279
|
-
* An env-inherited run id whose handoff was already CONSUMED by a different session is a
|
|
280
|
-
* spawned child, not the launched session → `adopt` (derive a sibling id, inherit `mode`).
|
|
306
|
+
* Resolve the active plan-ref: the worktree's `cache.plan-ref` file first, then the rebuilt
|
|
307
|
+
* workflow-state's `active_plan_ref` (the branch-carried linkage). The worktree read is itself
|
|
308
|
+
* null-lenient and runs OUTSIDE the catch (a throw from it propagates); only the branch
|
|
309
|
+
* rebuild is fail-open → null. Structural + Pi-free: `ExtensionContext` satisfies the slice.
|
|
281
310
|
*/
|
|
311
|
+
export function activePlanRef(source: { cwd: string } & BranchSource): PlanRef | null {
|
|
312
|
+
const fromWorktree = readPlanRef(source.cwd);
|
|
313
|
+
if (fromWorktree) return fromWorktree;
|
|
314
|
+
try {
|
|
315
|
+
const branch = branchOf(source);
|
|
316
|
+
return (rebuildWorkflowState(branch).active_plan_ref as PlanRef | null) ?? null;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
282
322
|
/**
|
|
283
|
-
* The
|
|
284
|
-
*
|
|
285
|
-
* `
|
|
286
|
-
*
|
|
287
|
-
* `session_start` reconciles `cache.plan-ref` into `active_plan_ref`.
|
|
323
|
+
* The three-tier stack-objective resolution shared by every stack tool + command: an explicit
|
|
324
|
+
* id wins; else the branch-rebuilt `active_objective`; else the worktree plan-ref's
|
|
325
|
+
* `objective_id`. Each fallible tier fails open to the next; null when nothing resolves.
|
|
326
|
+
* Structural + Pi-free: `ExtensionContext` satisfies the slice.
|
|
288
327
|
*/
|
|
289
|
-
export function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
328
|
+
export function resolveStackObjective(
|
|
329
|
+
explicit: string | undefined,
|
|
330
|
+
source: { cwd: string } & BranchSource,
|
|
331
|
+
): string | null {
|
|
332
|
+
if (explicit !== undefined && explicit.length > 0) return explicit;
|
|
333
|
+
try {
|
|
334
|
+
const active = rebuildWorkflowState(branchOf(source)).active_objective;
|
|
335
|
+
if (active !== undefined && active !== null) return active;
|
|
336
|
+
} catch {
|
|
337
|
+
// fall through to the plan-ref tier
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
return readPlanRef(source.cwd)?.objective_id ?? null;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
299
344
|
}
|
|
300
345
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
const childRunId = deriveForkRunId(state.run_id, cwd);
|
|
313
|
-
return { action: "fork", source: "fork", childRunId, parentRunId: state.run_id, state };
|
|
314
|
-
}
|
|
315
|
-
if (envRunId !== null && envRunId !== "") {
|
|
316
|
-
// Env-child detection (contracts §8.2): subagent children are spawned as separate `pi`
|
|
317
|
-
// processes with the parent's env, so they arrive here carrying the parent's PERK_RUN_ID.
|
|
318
|
-
// A handoff already consumed by a DIFFERENT (or unrecorded) session belongs to someone else:
|
|
319
|
-
// adopt a derived `<run_id>.<n>` child identity instead of re-claiming — never re-consume the
|
|
320
|
-
// handoff, never capture pointers, never impersonate the launched stage. The parent's `mode`
|
|
321
|
-
// is inherited so read-only gating survives into exploration children. Everything else —
|
|
322
|
-
// absent/corrupt/mismatched handoff (the loud unclaimed error), unconsumed (the normal cold
|
|
323
|
-
// claim), or consumed by THIS session (idempotent re-claim after lost branch state) — stays
|
|
324
|
-
// the claim arm.
|
|
325
|
-
const handoff = readHandoff(cwd, envRunId);
|
|
326
|
-
if (
|
|
327
|
-
handoff !== null &&
|
|
328
|
-
handoff.run_id === envRunId &&
|
|
329
|
-
handoff.consumed === true &&
|
|
330
|
-
handoff.pi_session_id !== currentSessionId
|
|
331
|
-
) {
|
|
332
|
-
return {
|
|
333
|
-
action: "adopt",
|
|
334
|
-
source: "env-child",
|
|
335
|
-
childRunId: deriveForkRunId(envRunId, cwd),
|
|
336
|
-
parentRunId: envRunId,
|
|
337
|
-
mode: handoff.mode,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
return { action: "claim", source: "env", runId: envRunId };
|
|
346
|
+
/**
|
|
347
|
+
* Equality by identity (provider + pr_id) — the plan-ref dedup key. Two refs to
|
|
348
|
+
* the same plan are equal even if other fields drift; absent compares equal only to absent.
|
|
349
|
+
*/
|
|
350
|
+
export function planRefsEqual(
|
|
351
|
+
a: PlanRef | null | undefined,
|
|
352
|
+
b: PlanRef | null | undefined,
|
|
353
|
+
): boolean {
|
|
354
|
+
if (a === null || a === undefined || b === null || b === undefined) {
|
|
355
|
+
return (a === null || a === undefined) && (b === null || b === undefined);
|
|
341
356
|
}
|
|
342
|
-
return
|
|
357
|
+
return a.provider === b.provider && a.pr_id === b.pr_id;
|
|
343
358
|
}
|