@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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// The objective-refinement working DRAFT: the one model-facing artifact (`objective_refinement_draft`
|
|
2
|
+
// writes it), bound to the session's context by run id + exact context digest, and the review
|
|
3
|
+
// rendering over the validated (draft, context) pair (contracts.md §8.67).
|
|
4
|
+
//
|
|
5
|
+
// Byte ownership: TypeScript alone serializes drafts — the explicit property order
|
|
6
|
+
// `schema_version`, `run_id`, `context_digest`, `markdown`, compact `JSON.stringify`, one LF.
|
|
7
|
+
// The Markdown is the model's ONLY contribution: it cannot supply a title, path, target, digest,
|
|
8
|
+
// run id or expectation. Python parses the transferred draft without reserializing or trimming.
|
|
9
|
+
//
|
|
10
|
+
// A draft is USABLE only when both strict session pointers verify, the run ids match, the
|
|
11
|
+
// draft's `context_digest` equals the current context artifact's digest, the context fields
|
|
12
|
+
// validate, and the Markdown is nonblank. A context re-prepared after the draft makes the draft
|
|
13
|
+
// `mismatch` (evidence that needs rewriting), never a silent rebind.
|
|
14
|
+
|
|
15
|
+
import type { SessionArtifactReceipt, WorkflowSession } from "../../session/workflowSession.ts";
|
|
16
|
+
import {
|
|
17
|
+
checkoutObservationLine,
|
|
18
|
+
isArtifactDigest,
|
|
19
|
+
type RefinementContextRead,
|
|
20
|
+
resumeRefinementContext,
|
|
21
|
+
} from "./context.ts";
|
|
22
|
+
|
|
23
|
+
/** The fixed working-draft artifact name (one small JSON envelope around the Markdown). */
|
|
24
|
+
export const REFINEMENT_DRAFT_ARTIFACT = "objective-refinement-draft.json";
|
|
25
|
+
|
|
26
|
+
export interface RefinementDraft {
|
|
27
|
+
run_id: string;
|
|
28
|
+
context_digest: string;
|
|
29
|
+
markdown: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The validated (draft, context) pair plus the draft's exact bytes — the review + save unit. */
|
|
33
|
+
export interface RefinementDraftPair {
|
|
34
|
+
draft: RefinementDraft;
|
|
35
|
+
draftRaw: string;
|
|
36
|
+
context: RefinementContextRead;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a draft: the fixed property order, compact `JSON.stringify`, exactly one trailing LF.
|
|
41
|
+
* Pure; never throws.
|
|
42
|
+
*/
|
|
43
|
+
export function encodeRefinementDraft(draft: {
|
|
44
|
+
runId: string;
|
|
45
|
+
contextDigest: string;
|
|
46
|
+
markdown: string;
|
|
47
|
+
}): string {
|
|
48
|
+
return `${JSON.stringify({
|
|
49
|
+
schema_version: 1,
|
|
50
|
+
run_id: draft.runId,
|
|
51
|
+
context_digest: draft.contextDigest,
|
|
52
|
+
markdown: draft.markdown,
|
|
53
|
+
})}\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type DecodeRefinementDraftResult =
|
|
57
|
+
| { ok: true; draft: RefinementDraft }
|
|
58
|
+
| { ok: false; problem: string };
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Strictly decode draft bytes: a JSON object with exactly the four keys, `schema_version` 1, a
|
|
62
|
+
* nonblank run id, a `sha256:` context digest and nonblank Markdown (tested with trim, NEVER
|
|
63
|
+
* trimmed). Never throws.
|
|
64
|
+
*/
|
|
65
|
+
export function decodeRefinementDraft(raw: string): DecodeRefinementDraftResult {
|
|
66
|
+
const refuse = (why: string): { ok: false; problem: string } => ({
|
|
67
|
+
ok: false,
|
|
68
|
+
problem: `${REFINEMENT_DRAFT_ARTIFACT} ${why}`,
|
|
69
|
+
});
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
return refuse("is not valid JSON");
|
|
75
|
+
}
|
|
76
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
77
|
+
return refuse("is not a JSON object");
|
|
78
|
+
const p = parsed as Record<string, unknown>;
|
|
79
|
+
const keys = Object.keys(p).sort();
|
|
80
|
+
const expected = ["context_digest", "markdown", "run_id", "schema_version"];
|
|
81
|
+
if (keys.length !== expected.length || keys.some((k, i) => k !== expected[i]))
|
|
82
|
+
return refuse("does not carry exactly schema_version, run_id, context_digest, markdown");
|
|
83
|
+
if (p.schema_version !== 1) return refuse("has an unsupported schema_version");
|
|
84
|
+
if (typeof p.run_id !== "string" || !p.run_id.trim()) return refuse("has a blank run_id");
|
|
85
|
+
if (typeof p.context_digest !== "string" || !isArtifactDigest(p.context_digest))
|
|
86
|
+
return refuse("has a malformed context_digest");
|
|
87
|
+
if (typeof p.markdown !== "string" || !p.markdown.trim()) return refuse("has blank markdown");
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
draft: { run_id: p.run_id, context_digest: p.context_digest, markdown: p.markdown },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ------------------------------------------------------------------------------- revise
|
|
95
|
+
|
|
96
|
+
export type ReviseRefinementDraftResult =
|
|
97
|
+
| {
|
|
98
|
+
status: "revised";
|
|
99
|
+
receipt: SessionArtifactReceipt;
|
|
100
|
+
bytes: number;
|
|
101
|
+
context: RefinementContextRead;
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
status: "unchanged";
|
|
105
|
+
receipt: SessionArtifactReceipt;
|
|
106
|
+
bytes: number;
|
|
107
|
+
context: RefinementContextRead;
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
status: "rejected";
|
|
111
|
+
reason: "blank_markdown" | "no_identity" | "no_context" | "context_refused" | "write_refused";
|
|
112
|
+
problem: string;
|
|
113
|
+
}
|
|
114
|
+
| { status: "unverified"; problem: string };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Rewrite the working draft (a whole-value replacement) bound to the CURRENT context: blank
|
|
118
|
+
* Markdown is refused first, then missing identity, then the strict context resume (absent →
|
|
119
|
+
* the session needs a grounding pass; refused → a fail-closed stop), then the verified write of
|
|
120
|
+
* the small fixed envelope. Identical bytes are `unchanged`. Never throws.
|
|
121
|
+
*/
|
|
122
|
+
export function reviseRefinementDraft(
|
|
123
|
+
input: { markdown: string },
|
|
124
|
+
session: WorkflowSession,
|
|
125
|
+
): ReviseRefinementDraftResult {
|
|
126
|
+
if (!input.markdown.trim()) {
|
|
127
|
+
return {
|
|
128
|
+
status: "rejected",
|
|
129
|
+
reason: "blank_markdown",
|
|
130
|
+
problem: "no refinement markdown to write (pass the full working draft)",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const identity = session.currentRunIdentity();
|
|
134
|
+
if (!identity.ok) {
|
|
135
|
+
return {
|
|
136
|
+
status: "rejected",
|
|
137
|
+
reason: "no_identity",
|
|
138
|
+
problem: `session identity ${identity.reason} — cannot write the refinement draft`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const context = resumeRefinementContext(session);
|
|
142
|
+
if (context.kind === "absent") {
|
|
143
|
+
return {
|
|
144
|
+
status: "rejected",
|
|
145
|
+
reason: "no_context",
|
|
146
|
+
problem:
|
|
147
|
+
"no refinement context in this session — enter a grounding pass first (perk objective " +
|
|
148
|
+
"refine <objective> or /objective-refine)",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (context.kind === "refused") {
|
|
152
|
+
return {
|
|
153
|
+
status: "rejected",
|
|
154
|
+
reason: "context_refused",
|
|
155
|
+
problem: `the refinement context is invalid: ${context.problem}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const content = encodeRefinementDraft({
|
|
159
|
+
runId: identity.runId,
|
|
160
|
+
contextDigest: context.read.digest,
|
|
161
|
+
markdown: input.markdown,
|
|
162
|
+
});
|
|
163
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
164
|
+
const written = session.writeArtifact(REFINEMENT_DRAFT_ARTIFACT, content, {
|
|
165
|
+
provenance: "strict",
|
|
166
|
+
});
|
|
167
|
+
switch (written.status) {
|
|
168
|
+
case "applied":
|
|
169
|
+
return { status: "revised", receipt: written.receipt, bytes, context: context.read };
|
|
170
|
+
case "unchanged":
|
|
171
|
+
return { status: "unchanged", receipt: written.receipt, bytes, context: context.read };
|
|
172
|
+
case "rejected":
|
|
173
|
+
return {
|
|
174
|
+
status: "rejected",
|
|
175
|
+
reason: "write_refused",
|
|
176
|
+
problem: `could not write the ${REFINEMENT_DRAFT_ARTIFACT} artifact (${written.problem})`,
|
|
177
|
+
};
|
|
178
|
+
case "unverified":
|
|
179
|
+
return {
|
|
180
|
+
status: "unverified",
|
|
181
|
+
problem: `could not write the ${REFINEMENT_DRAFT_ARTIFACT} artifact (${written.problem})`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ------------------------------------------------------------------------------- resume
|
|
187
|
+
|
|
188
|
+
/** The classified draft resume. `mismatch` is a usable-looking draft bound to a DIFFERENT
|
|
189
|
+
* context (re-prepared after it was written) — evidence that must be rewritten, never routed. */
|
|
190
|
+
export type ResumeRefinementDraftResult =
|
|
191
|
+
| { kind: "valid"; pair: RefinementDraftPair }
|
|
192
|
+
| { kind: "absent" }
|
|
193
|
+
| { kind: "no-context" }
|
|
194
|
+
| { kind: "mismatch"; problem: string }
|
|
195
|
+
| { kind: "refused"; problem: string };
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resume the (draft, context) pair strictly: identity → strict context → strict draft → the
|
|
199
|
+
* binding check (matching run id AND exact context digest). Corruption, orphan pointers,
|
|
200
|
+
* fork/wrong-run data and a context mismatch never fall back to plans or to any other artifact.
|
|
201
|
+
* Never throws.
|
|
202
|
+
*/
|
|
203
|
+
export function resumeRefinementDraft(session: WorkflowSession): ResumeRefinementDraftResult {
|
|
204
|
+
const identity = session.currentRunIdentity();
|
|
205
|
+
if (!identity.ok) return { kind: "refused", problem: `session identity ${identity.reason}` };
|
|
206
|
+
const context = resumeRefinementContext(session);
|
|
207
|
+
if (context.kind === "refused") return { kind: "refused", problem: context.problem };
|
|
208
|
+
const read = session.readArtifact(REFINEMENT_DRAFT_ARTIFACT, { provenance: "strict" });
|
|
209
|
+
if (read.status === "invalid") return { kind: "refused", problem: read.problem };
|
|
210
|
+
if (context.kind === "absent")
|
|
211
|
+
return read.status === "absent" ? { kind: "absent" } : { kind: "no-context" };
|
|
212
|
+
if (read.status === "absent") return { kind: "absent" };
|
|
213
|
+
const decoded = decodeRefinementDraft(read.content);
|
|
214
|
+
if (!decoded.ok) return { kind: "refused", problem: decoded.problem };
|
|
215
|
+
const draft = decoded.draft;
|
|
216
|
+
if (draft.run_id !== identity.runId)
|
|
217
|
+
return { kind: "refused", problem: `${REFINEMENT_DRAFT_ARTIFACT} belongs to another run` };
|
|
218
|
+
if (draft.context_digest !== context.read.digest)
|
|
219
|
+
return {
|
|
220
|
+
kind: "mismatch",
|
|
221
|
+
problem:
|
|
222
|
+
"the working draft was written against an earlier refinement context (the context was " +
|
|
223
|
+
"re-prepared since) — rewrite it with objective_refinement_draft against the current context",
|
|
224
|
+
};
|
|
225
|
+
return { kind: "valid", pair: { draft, draftRaw: read.content, context: context.read } };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ------------------------------------------------------------------------------- render
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render the validated pair as the review surface: identity + carrier + pass time, a prominent
|
|
232
|
+
* advisory notice, the capture-time observation label (never "verified/frozen/current"), then
|
|
233
|
+
* the FULL Markdown verbatim. Identity/provenance are immutable review metadata — the
|
|
234
|
+
* Markdown below the rule is the only reviewable content. Pure; never throws.
|
|
235
|
+
*/
|
|
236
|
+
export function renderRefinementDraft(pair: RefinementDraftPair): string {
|
|
237
|
+
const c = pair.context.context;
|
|
238
|
+
const t = c.target;
|
|
239
|
+
const head = [
|
|
240
|
+
`# Refinement — objective ${c.objective.id} · node ${t.identity.node_id}`,
|
|
241
|
+
"",
|
|
242
|
+
`Objective: ${c.objective.title}`,
|
|
243
|
+
`Node ${t.identity.node_id} (${t.status}): ${t.source.description}`,
|
|
244
|
+
`Carrier: ${t.carrier_identifier}${t.carrier_url ? ` — ${t.carrier_url}` : ""}`,
|
|
245
|
+
`Authoring pass started: ${c.provenance.authored_at} (run ${c.provenance.authoring_run_id})`,
|
|
246
|
+
c.prior !== null
|
|
247
|
+
? `Replaces the prior refinement saved at ${c.prior.saved_at} (full-content replacement).`
|
|
248
|
+
: "First refinement of this node.",
|
|
249
|
+
"",
|
|
250
|
+
"> ADVISORY: this is a dated refinement of a FUTURE node, not an executable plan. Saving it " +
|
|
251
|
+
"writes only the node's marked refinement comment — no plan is created, no node is claimed, " +
|
|
252
|
+
"no status changes. Unresolved future assumptions are legitimate here.",
|
|
253
|
+
">",
|
|
254
|
+
`> ${checkoutObservationLine(c.provenance)}`,
|
|
255
|
+
"",
|
|
256
|
+
"---",
|
|
257
|
+
"",
|
|
258
|
+
"",
|
|
259
|
+
];
|
|
260
|
+
return head.join("\n") + pair.draft.markdown;
|
|
261
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The refinement feature's model-facing prose units: the draft tool's guidelines, the
|
|
2
|
+
// refinement session context content builder, and the injection marker/customType constants.
|
|
3
|
+
// Prose-unit MEANING and ORDER live here (the feature); the v1 adapter only PLACES these units
|
|
4
|
+
// in Pi registration fields and event payloads. Prompting, NOT enforcement — the read-only tool
|
|
5
|
+
// gate (its refinement flavor) is the enforcement.
|
|
6
|
+
|
|
7
|
+
import { render } from "../../substrate/prompts.ts";
|
|
8
|
+
import { objectiveReadInstruction } from "../objective/prose.ts";
|
|
9
|
+
import type { RefinementContext } from "./context.ts";
|
|
10
|
+
|
|
11
|
+
/** The refinement context customType (distinct from the plan/objective/gist contexts). */
|
|
12
|
+
export const REFINEMENT_CONTEXT_TYPE = "perk:objective-refinement-context";
|
|
13
|
+
|
|
14
|
+
/** The injected refinement context's identity marker (the strip + dedup key). */
|
|
15
|
+
export const REFINEMENT_MARKER = "[OBJECTIVE REFINEMENT]";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The refinement session context: live state + pointers only (contracts.md §8.57 — the flow is
|
|
19
|
+
* stated by the launch statement, the detail by the `perk-objective-refine` skill). It names the
|
|
20
|
+
* context artifact, the working-draft tool, the review tool, the human failsafe and the bound
|
|
21
|
+
* skill; it never restates the flow.
|
|
22
|
+
*/
|
|
23
|
+
export const REFINEMENT_CONTEXT = render("contexts/objective-refinement.md", {
|
|
24
|
+
marker: REFINEMENT_MARKER,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Build the full injection, appending the project-config authoring addendum when present. */
|
|
28
|
+
export function refinementContextContent(addendum: string | undefined): string {
|
|
29
|
+
return addendum ? `${REFINEMENT_CONTEXT}\n\n${addendum.trim()}` : REFINEMENT_CONTEXT;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The `objective_refinement_draft` tool guidelines (verbatim prose units; the adapter places them). */
|
|
33
|
+
export const REFINEMENT_DRAFT_TOOL_GUIDELINES = [
|
|
34
|
+
"Call objective_refinement_draft to persist the current working refinement as you author or revise it; pass the FULL Markdown each time (it rewrites the whole draft).",
|
|
35
|
+
"objective_refinement_draft binds the draft to the session's grounding context for you — it never saves to the issue backend and never ends the turn; plan_review (approval) or the human's /objective-refinement-save persist it.",
|
|
36
|
+
"Name unresolved future assumptions and the code seams as observed at capture time; never present the checkout observation as a freshness guarantee.",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** The re-refinement note (byte-identical to the cold door's `_PRIOR_NOTE`). */
|
|
40
|
+
const PRIOR_NOTE =
|
|
41
|
+
"A valid prior refinement already exists on this node — the context carries its FULL " +
|
|
42
|
+
"Markdown; you are re-refining, and a save replaces it whole (full-content replacement).";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The shared cold/warm refinement flow prompt (`stages/objective-refine/seed.md`) rendered for
|
|
46
|
+
* the warm `/objective-refine` entry from the VALIDATED context: the objective title + node
|
|
47
|
+
* description ride inside `<untrusted_objective>` as DATA; the only door-derived interpolations
|
|
48
|
+
* are identifiers, the read clause and the session-data path of the context artifact.
|
|
49
|
+
*/
|
|
50
|
+
export function refinementGuidance(context: RefinementContext, contextPath: string): string {
|
|
51
|
+
const t = context.target;
|
|
52
|
+
return render("stages/objective-refine/seed.md", {
|
|
53
|
+
number: context.objective.id,
|
|
54
|
+
title: context.objective.title,
|
|
55
|
+
node_id: t.identity.node_id,
|
|
56
|
+
node_description: t.source.description,
|
|
57
|
+
read_clause: objectiveReadInstruction(
|
|
58
|
+
t.identity.backend,
|
|
59
|
+
context.objective.id,
|
|
60
|
+
context.objective.url,
|
|
61
|
+
),
|
|
62
|
+
context_path: contextPath,
|
|
63
|
+
prior_note: context.prior !== null ? PRIOR_NOTE : "",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The refusal every plan-graph surface renders when invoked inside a refinement session
|
|
69
|
+
* (contracts.md §8.67 isolation): the surface names itself; the text names the only two
|
|
70
|
+
* legitimate exits. Pure.
|
|
71
|
+
*/
|
|
72
|
+
export function refinementStageRefusal(surface: string): string {
|
|
73
|
+
return (
|
|
74
|
+
`${surface} is not available in an objective-refine session — a refinement never creates ` +
|
|
75
|
+
"a plan, claims a node, or changes node/objective state. Author with " +
|
|
76
|
+
"objective_refinement_draft and review with plan_review; the human saves with " +
|
|
77
|
+
"/objective-refinement-save."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The objective-refinement REVIEW feature: the reviewer role over the rendered pair and the
|
|
2
|
+
// one-entry completion routing (contracts.md §8.67 / §8.23's refinement arm).
|
|
3
|
+
//
|
|
4
|
+
// Discipline: the (draft, context) pair is the SOLE review source — never a param, never the
|
|
5
|
+
// transcript. The Pi arm resumes the pair, renders it, reviews, then routes the verdict through
|
|
6
|
+
// `completeRefinementReview`. An approval carrying Direct Edits SKIPS the save and returns one
|
|
7
|
+
// revise round (the heading means no save and no gate exit — Markdown edits are folded via the
|
|
8
|
+
// draft tool and re-reviewed; target/provenance edits need a new grounding pass, never
|
|
9
|
+
// fabricated metadata). A plain approval re-reads the pair through `refinementApprovalSave`,
|
|
10
|
+
// which compares it with the REVIEWED pair when the caller captured one — approval never
|
|
11
|
+
// transfers to a replacement artifact. Denial stays untrusted DATA. Skipped / dismissed /
|
|
12
|
+
// unavailable outcomes save nothing — the caller offers the human `/objective-refinement-save`.
|
|
13
|
+
|
|
14
|
+
import type { RefinementApprovalSaveOutcome } from "./save.ts";
|
|
15
|
+
|
|
16
|
+
export type RefinementReviewOutcome =
|
|
17
|
+
| { status: "approved"; feedback?: string; reviewId?: string }
|
|
18
|
+
| { status: "approvedDirectEdits"; feedback: string; reviewId?: string }
|
|
19
|
+
| { status: "denied"; feedback?: string; reviewId?: string }
|
|
20
|
+
| { status: "dismissed" }
|
|
21
|
+
| { status: "aborted" }
|
|
22
|
+
| { status: "unavailable"; warning: string };
|
|
23
|
+
|
|
24
|
+
/** The reviewer role: judge the RENDERED surface (never raw artifact JSON). */
|
|
25
|
+
export interface RefinementDraftReviewer {
|
|
26
|
+
review(rendered: string, signal?: AbortSignal): Promise<RefinementReviewOutcome>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ReviewRefinementResult =
|
|
30
|
+
| { status: "noDraft" }
|
|
31
|
+
| { status: "noContext" }
|
|
32
|
+
| { status: "refusedDraft"; problem: string }
|
|
33
|
+
| { status: "directEditsRevise"; feedback: string; reviewId?: string }
|
|
34
|
+
| {
|
|
35
|
+
status: "approvedSaved";
|
|
36
|
+
save: Extract<RefinementApprovalSaveOutcome, { status: "saved" }>;
|
|
37
|
+
feedback?: string;
|
|
38
|
+
reviewId?: string;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
status: "approvedSaveFailed";
|
|
42
|
+
save: Extract<RefinementApprovalSaveOutcome, { status: "save-failed" }>;
|
|
43
|
+
feedback?: string;
|
|
44
|
+
reviewId?: string;
|
|
45
|
+
}
|
|
46
|
+
| { status: "approvedNoDraft"; feedback?: string; reviewId?: string }
|
|
47
|
+
| { status: "approvedRefusedDraft"; problem: string; feedback?: string; reviewId?: string }
|
|
48
|
+
| {
|
|
49
|
+
/** The approval named a pair the session no longer holds: nothing saved, review again. */
|
|
50
|
+
status: "approvedSourceChanged";
|
|
51
|
+
changed: "draft" | "context";
|
|
52
|
+
feedback?: string;
|
|
53
|
+
reviewId?: string;
|
|
54
|
+
}
|
|
55
|
+
| { status: "denied"; feedback?: string; reviewId?: string }
|
|
56
|
+
| { status: "dismissed" }
|
|
57
|
+
| { status: "aborted" }
|
|
58
|
+
| { status: "unavailable"; warning: string };
|
|
59
|
+
|
|
60
|
+
/** Subject policy only: callers authorize effects before entering this completion seam. */
|
|
61
|
+
export async function completeRefinementReview(
|
|
62
|
+
outcome: RefinementReviewOutcome,
|
|
63
|
+
approvalSave: () => Promise<RefinementApprovalSaveOutcome>,
|
|
64
|
+
): Promise<ReviewRefinementResult> {
|
|
65
|
+
if (outcome.status === "approvedDirectEdits") {
|
|
66
|
+
return {
|
|
67
|
+
status: "directEditsRevise",
|
|
68
|
+
feedback: outcome.feedback,
|
|
69
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (outcome.status === "approved") {
|
|
73
|
+
const carried = {
|
|
74
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
75
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
76
|
+
};
|
|
77
|
+
const save = await approvalSave();
|
|
78
|
+
switch (save.status) {
|
|
79
|
+
case "saved":
|
|
80
|
+
return { status: "approvedSaved", save, ...carried };
|
|
81
|
+
case "save-failed":
|
|
82
|
+
return { status: "approvedSaveFailed", save, ...carried };
|
|
83
|
+
case "no-draft":
|
|
84
|
+
return { status: "approvedNoDraft", ...carried };
|
|
85
|
+
case "no-context":
|
|
86
|
+
return {
|
|
87
|
+
status: "approvedRefusedDraft",
|
|
88
|
+
problem: "the refinement context vanished between the review and the save",
|
|
89
|
+
...carried,
|
|
90
|
+
};
|
|
91
|
+
case "refused-draft":
|
|
92
|
+
return { status: "approvedRefusedDraft", problem: save.problem, ...carried };
|
|
93
|
+
case "source-changed":
|
|
94
|
+
return { status: "approvedSourceChanged", changed: save.changed, ...carried };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
switch (outcome.status) {
|
|
98
|
+
case "denied":
|
|
99
|
+
return {
|
|
100
|
+
status: "denied",
|
|
101
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
102
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
103
|
+
};
|
|
104
|
+
case "dismissed":
|
|
105
|
+
return { status: "dismissed" };
|
|
106
|
+
case "aborted":
|
|
107
|
+
return { status: "aborted" };
|
|
108
|
+
case "unavailable":
|
|
109
|
+
return { status: "unavailable", warning: outcome.warning };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The objective-refinement SAVE feature: the narrow exterior `RefinementBackend` port (one
|
|
2
|
+
// production adapter — the `perk objective refinement-save` cold door in pi/v1 — plus
|
|
3
|
+
// deterministic fakes in the tests) and the shared save seam `refinementApprovalSave` both
|
|
4
|
+
// persistence gestures run: the APPROVED `plan_review` refinement arm and the human
|
|
5
|
+
// `/objective-refinement-save` command (contracts.md §8.67).
|
|
6
|
+
//
|
|
7
|
+
// The seam strict-resumes the (draft, context) pair and hands the backend the draft's EXACT
|
|
8
|
+
// bytes (never the rendered markdown, never in-hand bytes) with the explicit run id; the gate
|
|
9
|
+
// exits only after a verified save (D1a). A caller that showed a human a specific pair passes
|
|
10
|
+
// its `reviewed` digests: the resumed pair must equal them byte for byte (draft) and digest for
|
|
11
|
+
// digest (context), or the seam stops with `source-changed` — an approval never transfers to a
|
|
12
|
+
// replacement artifact written during the human wait. There is no linkage, budget, claim or
|
|
13
|
+
// cache effect: a successful save carries the backend's verified facts for the caller to relay,
|
|
14
|
+
// and the caller says advisory content — not an executable plan — was saved.
|
|
15
|
+
|
|
16
|
+
import type { WorkflowSession } from "../../session/workflowSession.ts";
|
|
17
|
+
import { type ApprovalGate, saveThroughApprovalGate } from "../review/approvalGate.ts";
|
|
18
|
+
import { type RefinementDraftPair, resumeRefinementDraft } from "./draft.ts";
|
|
19
|
+
|
|
20
|
+
/** The verified save facts (the worker's success envelope) or a typed failure. */
|
|
21
|
+
export type RefinementBackendSaveResult =
|
|
22
|
+
| {
|
|
23
|
+
status: "saved";
|
|
24
|
+
commentId: string;
|
|
25
|
+
carrierUrl: string;
|
|
26
|
+
carrierIdentifier: string;
|
|
27
|
+
objectiveId: string;
|
|
28
|
+
objectiveRunId: string;
|
|
29
|
+
nodeId: string;
|
|
30
|
+
bodyDigest: string;
|
|
31
|
+
savedAt: string;
|
|
32
|
+
authoredAt: string;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
status: "failed";
|
|
36
|
+
message: string;
|
|
37
|
+
errorType: string;
|
|
38
|
+
/** The worker's diagnostics when it reported them (read back, never blindly retry). */
|
|
39
|
+
writeAttempted: boolean | null;
|
|
40
|
+
commentIds: string[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The narrow exterior port: the exact draft bytes + the run whose context they bind to. */
|
|
44
|
+
export interface RefinementBackend {
|
|
45
|
+
save(req: { runId: string; draftRaw: string }): Promise<RefinementBackendSaveResult>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The pair a reviewer actually judged: the draft's exact bytes and the context artifact's digest
|
|
50
|
+
* (the two inputs the rendering is a pure function of — identical rendering over a re-prepared
|
|
51
|
+
* context still names a different pair). Callers capture it BEFORE display and hand it to the
|
|
52
|
+
* seam AFTER the verdict.
|
|
53
|
+
*/
|
|
54
|
+
export interface ReviewedRefinementPair {
|
|
55
|
+
draftRaw: string;
|
|
56
|
+
contextDigest: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The exact digests of a resumed pair, for a later `reviewed` comparison. */
|
|
60
|
+
export function reviewedPairOf(pair: RefinementDraftPair): ReviewedRefinementPair {
|
|
61
|
+
return { draftRaw: pair.draftRaw, contextDigest: pair.context.digest };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The save-seam outcome. The four non-save arms are fail-closed stops: nothing saved, the
|
|
65
|
+
* gate never touched, with the exact guidance the caller renders. */
|
|
66
|
+
export type RefinementApprovalSaveOutcome =
|
|
67
|
+
| { status: "no-draft" }
|
|
68
|
+
| { status: "no-context" }
|
|
69
|
+
| { status: "refused-draft"; problem: string }
|
|
70
|
+
| {
|
|
71
|
+
/** The resumed pair is valid but is not the pair the approval was given for. */
|
|
72
|
+
status: "source-changed";
|
|
73
|
+
changed: "draft" | "context";
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
status: "saved";
|
|
77
|
+
save: Extract<RefinementBackendSaveResult, { status: "saved" }>;
|
|
78
|
+
pair: RefinementDraftPair;
|
|
79
|
+
gateExited: boolean;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
status: "save-failed";
|
|
83
|
+
save: Extract<RefinementBackendSaveResult, { status: "failed" }>;
|
|
84
|
+
pair: RefinementDraftPair;
|
|
85
|
+
gateExited: false;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The shared save seam: strict-resume the pair at save time (a `mismatch` is a refused draft —
|
|
90
|
+
* it names the rewrite; `no-context` names the grounding pass), compare it with the `reviewed`
|
|
91
|
+
* pair when one was captured (the context first: a re-prepared context also rebinds any draft
|
|
92
|
+
* written against it), stage the EXACT draft bytes through the backend with the session's run
|
|
93
|
+
* id, and exit the gate only on a verified save (`saveThroughApprovalGate`: snapshot before,
|
|
94
|
+
* exit after success while read-only, a failed save leaves the gate ON). Never throws.
|
|
95
|
+
*/
|
|
96
|
+
export async function refinementApprovalSave(deps: {
|
|
97
|
+
session: WorkflowSession;
|
|
98
|
+
backend: RefinementBackend;
|
|
99
|
+
gate: ApprovalGate;
|
|
100
|
+
reviewed?: ReviewedRefinementPair;
|
|
101
|
+
}): Promise<RefinementApprovalSaveOutcome> {
|
|
102
|
+
const resumed = resumeRefinementDraft(deps.session);
|
|
103
|
+
if (resumed.kind === "absent") return { status: "no-draft" };
|
|
104
|
+
if (resumed.kind === "no-context") return { status: "no-context" };
|
|
105
|
+
if (resumed.kind === "refused" || resumed.kind === "mismatch")
|
|
106
|
+
return { status: "refused-draft", problem: resumed.problem };
|
|
107
|
+
const pair = resumed.pair;
|
|
108
|
+
if (deps.reviewed !== undefined) {
|
|
109
|
+
if (pair.context.digest !== deps.reviewed.contextDigest)
|
|
110
|
+
return { status: "source-changed", changed: "context" };
|
|
111
|
+
if (pair.draftRaw !== deps.reviewed.draftRaw)
|
|
112
|
+
return { status: "source-changed", changed: "draft" };
|
|
113
|
+
}
|
|
114
|
+
const { outcome: save, gateExited } = await saveThroughApprovalGate(deps.gate, () =>
|
|
115
|
+
deps.backend.save({ runId: pair.draft.run_id, draftRaw: pair.draftRaw }),
|
|
116
|
+
);
|
|
117
|
+
if (save.status !== "saved") return { status: "save-failed", save, pair, gateExited: false };
|
|
118
|
+
return { status: "saved", save, pair, gateExited };
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The shared approval-gate invariant behind the three approval→save orchestrations
|
|
2
|
+
// (`gistApprovalSave`/`planApprovalSave`/`objectiveApprovalSave` — contracts §8.23/§8.41):
|
|
3
|
+
// the gate transition and its structural type unify HERE; draft resolution, save unions, and
|
|
4
|
+
// result field names stay feature-owned. Pi-free and storage-free (the pi/v1 adapters build the
|
|
5
|
+
// gate slice over ToolGating). The no-save siblings — `reviewPlanDraft`'s implement-here arm and
|
|
6
|
+
// `pi/v1/planReview.ts`'s `implementHereExit` — keep their own gate handling on purpose (the
|
|
7
|
+
// sanctioned §8.23 no-save exits, not save flows).
|
|
8
|
+
|
|
9
|
+
/** The structural gate slice the approval→save flows release (pi/v1 adapters build it over ToolGating). */
|
|
10
|
+
export interface ApprovalGate {
|
|
11
|
+
isActive(): boolean;
|
|
12
|
+
exit(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The approval-gate invariant (contracts §8.23/§8.41 — the D1a pattern): snapshot the gate
|
|
17
|
+
* BEFORE the save; exit ONLY after a successful save while read-only; report gateExited.
|
|
18
|
+
* Success is the fixed shared discriminant (status === "saved") — not caller policy. A failed
|
|
19
|
+
* save leaves the gate untouched; a thrown save propagates with the gate untouched.
|
|
20
|
+
*/
|
|
21
|
+
export async function saveThroughApprovalGate<T extends { status: "saved" } | { status: "failed" }>(
|
|
22
|
+
gate: ApprovalGate,
|
|
23
|
+
save: () => Promise<T>,
|
|
24
|
+
): Promise<{ outcome: T; gateExited: boolean }> {
|
|
25
|
+
const wasReadOnly = gate.isActive();
|
|
26
|
+
const outcome = await save();
|
|
27
|
+
if (outcome.status !== "saved") return { outcome, gateExited: false };
|
|
28
|
+
let gateExited = false;
|
|
29
|
+
if (wasReadOnly) {
|
|
30
|
+
gate.exit();
|
|
31
|
+
gateExited = true;
|
|
32
|
+
}
|
|
33
|
+
return { outcome, gateExited };
|
|
34
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The door-primed draft-review context + per-activation state for the draft-review doors
|
|
2
|
+
// (/plan-review-browser + /objective-review-browser). THE TRUST POSTURE (the difference from
|
|
3
|
+
// the PR pair): the wave's inputs — the draft under review, its type, and the optional
|
|
4
|
+
// human-supplied custom-angle definition — are REGISTRATION-OWNED STATE primed by the door
|
|
5
|
+
// (the `primeAnnotationSurface` discipline), never tool params: one `DraftReviewWaveState`
|
|
6
|
+
// instance per activation, created in `index.ts` and threaded to the two browser doors and the
|
|
7
|
+
// `pi/v1/draftReviewWaveTools.ts` tool pair. `start_draft_review_wave` takes ONLY `{angles}`
|
|
8
|
+
// and refuses unprimed, so the model can never substitute a transcript/arbitrary draft or
|
|
9
|
+
// invent a custom lane: reviewed bytes == browsed bytes == wave bytes by construction. A prime
|
|
10
|
+
// supersedes everything (context AND pending ref); a clear drops only the primed inputs — a
|
|
11
|
+
// launched wave stays collectable.
|
|
12
|
+
|
|
13
|
+
import type { ReportWaveRef } from "../../waves/reportWave.ts";
|
|
14
|
+
|
|
15
|
+
/** The door-primed draft-review inputs (registration-owned state — never tool params). */
|
|
16
|
+
export interface DraftReviewContext {
|
|
17
|
+
/** The draft kind under review (the wave lane tasks are parameterized on it). */
|
|
18
|
+
draftType: "plan" | "objective";
|
|
19
|
+
/** The rendered draft bytes the door surfaced in the browser — the wave reviews exactly these. */
|
|
20
|
+
draft: string;
|
|
21
|
+
/** The human-supplied custom-angle definition from the door arg — adds the `custom` lane. */
|
|
22
|
+
custom?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The draft pair's per-activation state: the ONE opaque ref of the pending (launched,
|
|
27
|
+
* uncollected) draft-review wave (the `pi/v1/codeReview/reviewWave.ts` pending-slot mirror —
|
|
28
|
+
* `start_draft_review_wave` refuses while it is set, and `collect_draft_review_wave` drains it;
|
|
29
|
+
* the wave's settled keys include the `custom` lane when one was primed — the covered
|
|
30
|
+
* computation needs it) PLUS the door-primed context slot (same defect class, same lifetime —
|
|
31
|
+
* one browser session's inputs, superseded by the next prime). Which wave is *current* is flow
|
|
32
|
+
* policy (this slot); every race/grace/drain mechanic below it is wave-owned.
|
|
33
|
+
*/
|
|
34
|
+
export interface DraftReviewWaveState {
|
|
35
|
+
pending: ReportWaveRef | null;
|
|
36
|
+
context: DraftReviewContext | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Create a fresh draft-review state (plain object — no Pi calls; safe anywhere in activation). */
|
|
40
|
+
export function createDraftReviewWaveState(): DraftReviewWaveState {
|
|
41
|
+
return { pending: null, context: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Prime the draft-review context for a new browser session (door-owned; called beside
|
|
46
|
+
* `primeAnnotationSurface` the moment the browser open picks the port). Resets the pending-wave
|
|
47
|
+
* slot too — a new browser session supersedes everything (the `primeAnnotationSurface`
|
|
48
|
+
* discipline).
|
|
49
|
+
*/
|
|
50
|
+
export function primeDraftReviewContext(
|
|
51
|
+
state: DraftReviewWaveState,
|
|
52
|
+
next: DraftReviewContext,
|
|
53
|
+
): void {
|
|
54
|
+
state.context = {
|
|
55
|
+
draftType: next.draftType,
|
|
56
|
+
draft: next.draft,
|
|
57
|
+
...(next.custom !== undefined ? { custom: next.custom } : {}),
|
|
58
|
+
};
|
|
59
|
+
state.pending = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Drop the context (door-owned; called when the bridge settles AND on the degrade arm). A
|
|
64
|
+
* launched wave stays collectable — only the primed inputs die with the browser session.
|
|
65
|
+
*/
|
|
66
|
+
export function clearDraftReviewContext(state: DraftReviewWaveState): void {
|
|
67
|
+
state.context = null;
|
|
68
|
+
}
|