@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,468 @@
|
|
|
1
|
+
// The objective-refinement grounding CONTEXT: the fixed transfer artifact Python prepares
|
|
2
|
+
// (`perk objective refine` / `perk objective refine-context`) and the interior imports, reads
|
|
3
|
+
// and binds drafts to (contracts.md §8.67).
|
|
4
|
+
//
|
|
5
|
+
// Byte ownership: Python is the SOLE serializer of this artifact. The interior validates the
|
|
6
|
+
// raw string's digest and decodes it STRICTLY for use, but every write is the UNCHANGED raw
|
|
7
|
+
// string — never `JSON.stringify(parsed)`, never a trim or newline normalization. Readers hand
|
|
8
|
+
// back the validated fields together with the raw bytes and their digest so no consumer ever
|
|
9
|
+
// serializes the context a second time; the draft's `context_digest` is exactly that digest.
|
|
10
|
+
//
|
|
11
|
+
// Provenance is a capture-time observation, not a code-freshness gate: the interior renders it
|
|
12
|
+
// as such and never refreshes it.
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ReadArtifactResult,
|
|
16
|
+
SessionArtifactReceipt,
|
|
17
|
+
WorkflowSession,
|
|
18
|
+
} from "../../session/workflowSession.ts";
|
|
19
|
+
import { digestSessionData, REFINEMENT_CONTEXT_ARTIFACT } from "../../session/workflowSession.ts";
|
|
20
|
+
|
|
21
|
+
/** The registry stage id of the refinement session (the isolated, read-only stage). */
|
|
22
|
+
export const REFINE_STAGE = "objective-refine";
|
|
23
|
+
|
|
24
|
+
/** The fixed context artifact name — the SAME name in run scratch (cold) and session data. */
|
|
25
|
+
export { REFINEMENT_CONTEXT_ARTIFACT };
|
|
26
|
+
|
|
27
|
+
/** The handoff namespace the cold door carries (never a top-level objective_id/node_id). */
|
|
28
|
+
export const REFINEMENT_HANDOFF_KEY = "objective_refinement";
|
|
29
|
+
|
|
30
|
+
export interface RefinementCodeBasis {
|
|
31
|
+
head_sha: string;
|
|
32
|
+
dirty: boolean;
|
|
33
|
+
captured_at: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RefinementProvenance {
|
|
37
|
+
authoring_run_id: string;
|
|
38
|
+
authored_at: string;
|
|
39
|
+
code_basis: RefinementCodeBasis;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RefinementIdentity {
|
|
43
|
+
backend: string;
|
|
44
|
+
objective_id: string;
|
|
45
|
+
objective_run_id: string;
|
|
46
|
+
node_id: string;
|
|
47
|
+
carrier_id: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RefinementSource {
|
|
51
|
+
description: string;
|
|
52
|
+
slug: string | null;
|
|
53
|
+
comment: string | null;
|
|
54
|
+
depends_on: string[] | null;
|
|
55
|
+
effective_depends_on: string[];
|
|
56
|
+
issue_description: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RefinementTarget {
|
|
60
|
+
identity: RefinementIdentity;
|
|
61
|
+
source: RefinementSource;
|
|
62
|
+
source_digest: string;
|
|
63
|
+
carrier_identifier: string;
|
|
64
|
+
carrier_url: string;
|
|
65
|
+
status: string;
|
|
66
|
+
plan_ref: string | null;
|
|
67
|
+
has_plan_metadata: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RefinementPrior {
|
|
71
|
+
markdown: string;
|
|
72
|
+
source_digest: string;
|
|
73
|
+
provenance: RefinementProvenance;
|
|
74
|
+
saved_at: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The validated context fields (the TS mirror of Python's `RefinementContext`). */
|
|
78
|
+
export interface RefinementContext {
|
|
79
|
+
schema_version: 1;
|
|
80
|
+
run_id: string;
|
|
81
|
+
target: RefinementTarget;
|
|
82
|
+
expected: { comment_id: string | null; body_digest: string | null };
|
|
83
|
+
provenance: RefinementProvenance;
|
|
84
|
+
objective: { id: string; title: string; url: string };
|
|
85
|
+
prior: RefinementPrior | null;
|
|
86
|
+
engagement: string;
|
|
87
|
+
warnings: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The validated fields TOGETHER with the exact bytes they came from. */
|
|
91
|
+
export interface RefinementContextRead {
|
|
92
|
+
context: RefinementContext;
|
|
93
|
+
raw: string;
|
|
94
|
+
digest: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type DecodeRefinementContextResult =
|
|
98
|
+
| { ok: true; context: RefinementContext }
|
|
99
|
+
| { ok: false; problem: string };
|
|
100
|
+
|
|
101
|
+
// ------------------------------------------------------------------------- strict decoding
|
|
102
|
+
|
|
103
|
+
class Refuse extends Error {}
|
|
104
|
+
|
|
105
|
+
function refuse(path: string, why: string): never {
|
|
106
|
+
throw new Refuse(`${path} ${why}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function obj(value: unknown, path: string, keys: readonly string[]): Record<string, unknown> {
|
|
110
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
111
|
+
refuse(path, "is not an object");
|
|
112
|
+
const record = value as Record<string, unknown>;
|
|
113
|
+
const present = Object.keys(record);
|
|
114
|
+
for (const key of keys) if (!(key in record)) refuse(`${path}.${key}`, "is missing");
|
|
115
|
+
for (const key of present) if (!keys.includes(key)) refuse(`${path}.${key}`, "is unknown");
|
|
116
|
+
return record;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function str(value: unknown, path: string): string {
|
|
120
|
+
if (typeof value !== "string") refuse(path, "is not a string");
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function nonblank(value: unknown, path: string): string {
|
|
125
|
+
const s = str(value, path);
|
|
126
|
+
if (!s.trim()) refuse(path, "is blank");
|
|
127
|
+
return s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function nullableStr(value: unknown, path: string): string | null {
|
|
131
|
+
return value === null ? null : str(value, path);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function bool(value: unknown, path: string): boolean {
|
|
135
|
+
if (typeof value !== "boolean") refuse(path, "is not a boolean");
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function strList(value: unknown, path: string): string[] {
|
|
140
|
+
if (!Array.isArray(value)) refuse(path, "is not a list");
|
|
141
|
+
return value.map((item, i) => str(item, `${path}[${i}]`));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const HEX40 = /^[0-9a-f]{40}$/;
|
|
145
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
146
|
+
const SAFE_ARTIFACT_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
147
|
+
|
|
148
|
+
function bareDigest(value: unknown, path: string): string {
|
|
149
|
+
const s = str(value, path);
|
|
150
|
+
if (!HEX64.test(s)) refuse(path, "is not a 64-char lowercase hex digest");
|
|
151
|
+
return s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function provenanceOf(value: unknown, path: string): RefinementProvenance {
|
|
155
|
+
const p = obj(value, path, ["authoring_run_id", "authored_at", "code_basis"]);
|
|
156
|
+
const cb = obj(p.code_basis, `${path}.code_basis`, ["head_sha", "dirty", "captured_at"]);
|
|
157
|
+
const head = str(cb.head_sha, `${path}.code_basis.head_sha`);
|
|
158
|
+
if (!HEX40.test(head)) refuse(`${path}.code_basis.head_sha`, "is not a 40-char lowercase sha");
|
|
159
|
+
return {
|
|
160
|
+
authoring_run_id: nonblank(p.authoring_run_id, `${path}.authoring_run_id`),
|
|
161
|
+
authored_at: nonblank(p.authored_at, `${path}.authored_at`),
|
|
162
|
+
code_basis: {
|
|
163
|
+
head_sha: head,
|
|
164
|
+
dirty: bool(cb.dirty, `${path}.code_basis.dirty`),
|
|
165
|
+
captured_at: nonblank(cb.captured_at, `${path}.code_basis.captured_at`),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Strictly decode transferred context bytes for USE (validation only — the artifact bytes are
|
|
172
|
+
* never rebuilt from this value). Every defect — malformed JSON, a non-object, unknown or
|
|
173
|
+
* missing keys at any level, mistyped scalars, an unknown `schema_version`, blank identity
|
|
174
|
+
* fields, a non-canonical digest, an inconsistent expectation — is a classified refusal
|
|
175
|
+
* carrying the exact problem. Never throws.
|
|
176
|
+
*/
|
|
177
|
+
export function decodeRefinementContext(raw: string): DecodeRefinementContextResult {
|
|
178
|
+
let parsed: unknown;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(raw);
|
|
181
|
+
} catch {
|
|
182
|
+
return { ok: false, problem: `${REFINEMENT_CONTEXT_ARTIFACT} is not valid JSON` };
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const c = obj(parsed, "context", [
|
|
186
|
+
"schema_version",
|
|
187
|
+
"run_id",
|
|
188
|
+
"target",
|
|
189
|
+
"expected",
|
|
190
|
+
"provenance",
|
|
191
|
+
"objective",
|
|
192
|
+
"prior",
|
|
193
|
+
"engagement",
|
|
194
|
+
"warnings",
|
|
195
|
+
]);
|
|
196
|
+
if (c.schema_version !== 1) refuse("context.schema_version", "is not 1");
|
|
197
|
+
const t = obj(c.target, "context.target", [
|
|
198
|
+
"identity",
|
|
199
|
+
"source",
|
|
200
|
+
"source_digest",
|
|
201
|
+
"carrier_identifier",
|
|
202
|
+
"carrier_url",
|
|
203
|
+
"status",
|
|
204
|
+
"plan_ref",
|
|
205
|
+
"has_plan_metadata",
|
|
206
|
+
]);
|
|
207
|
+
const id = obj(t.identity, "context.target.identity", [
|
|
208
|
+
"backend",
|
|
209
|
+
"objective_id",
|
|
210
|
+
"objective_run_id",
|
|
211
|
+
"node_id",
|
|
212
|
+
"carrier_id",
|
|
213
|
+
]);
|
|
214
|
+
const s = obj(t.source, "context.target.source", [
|
|
215
|
+
"description",
|
|
216
|
+
"slug",
|
|
217
|
+
"comment",
|
|
218
|
+
"depends_on",
|
|
219
|
+
"effective_depends_on",
|
|
220
|
+
"issue_description",
|
|
221
|
+
]);
|
|
222
|
+
const expected = obj(c.expected, "context.expected", ["comment_id", "body_digest"]);
|
|
223
|
+
const commentId = nullableStr(expected.comment_id, "context.expected.comment_id");
|
|
224
|
+
const bodyDigest = nullableStr(expected.body_digest, "context.expected.body_digest");
|
|
225
|
+
if ((commentId === null) !== (bodyDigest === null))
|
|
226
|
+
refuse("context.expected", "must carry both comment_id and body_digest, or neither");
|
|
227
|
+
if (commentId !== null && !commentId.trim()) refuse("context.expected.comment_id", "is blank");
|
|
228
|
+
if (bodyDigest !== null && !HEX64.test(bodyDigest))
|
|
229
|
+
refuse("context.expected.body_digest", "is not a 64-char lowercase hex digest");
|
|
230
|
+
const o = obj(c.objective, "context.objective", ["id", "title", "url"]);
|
|
231
|
+
let prior: RefinementPrior | null = null;
|
|
232
|
+
if (c.prior !== null) {
|
|
233
|
+
const p = obj(c.prior, "context.prior", [
|
|
234
|
+
"markdown",
|
|
235
|
+
"source_digest",
|
|
236
|
+
"provenance",
|
|
237
|
+
"saved_at",
|
|
238
|
+
]);
|
|
239
|
+
prior = {
|
|
240
|
+
markdown: nonblank(p.markdown, "context.prior.markdown"),
|
|
241
|
+
source_digest: bareDigest(p.source_digest, "context.prior.source_digest"),
|
|
242
|
+
provenance: provenanceOf(p.provenance, "context.prior.provenance"),
|
|
243
|
+
saved_at: str(p.saved_at, "context.prior.saved_at"),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const context: RefinementContext = {
|
|
247
|
+
schema_version: 1,
|
|
248
|
+
run_id: nonblank(c.run_id, "context.run_id"),
|
|
249
|
+
target: {
|
|
250
|
+
identity: {
|
|
251
|
+
backend: nonblank(id.backend, "context.target.identity.backend"),
|
|
252
|
+
objective_id: nonblank(id.objective_id, "context.target.identity.objective_id"),
|
|
253
|
+
objective_run_id: nonblank(
|
|
254
|
+
id.objective_run_id,
|
|
255
|
+
"context.target.identity.objective_run_id",
|
|
256
|
+
),
|
|
257
|
+
node_id: nonblank(id.node_id, "context.target.identity.node_id"),
|
|
258
|
+
carrier_id: nonblank(id.carrier_id, "context.target.identity.carrier_id"),
|
|
259
|
+
},
|
|
260
|
+
source: {
|
|
261
|
+
description: str(s.description, "context.target.source.description"),
|
|
262
|
+
slug: nullableStr(s.slug, "context.target.source.slug"),
|
|
263
|
+
comment: nullableStr(s.comment, "context.target.source.comment"),
|
|
264
|
+
depends_on:
|
|
265
|
+
s.depends_on === null
|
|
266
|
+
? null
|
|
267
|
+
: strList(s.depends_on, "context.target.source.depends_on"),
|
|
268
|
+
effective_depends_on: strList(
|
|
269
|
+
s.effective_depends_on,
|
|
270
|
+
"context.target.source.effective_depends_on",
|
|
271
|
+
),
|
|
272
|
+
issue_description: str(s.issue_description, "context.target.source.issue_description"),
|
|
273
|
+
},
|
|
274
|
+
source_digest: bareDigest(t.source_digest, "context.target.source_digest"),
|
|
275
|
+
carrier_identifier: nonblank(t.carrier_identifier, "context.target.carrier_identifier"),
|
|
276
|
+
carrier_url: str(t.carrier_url, "context.target.carrier_url"),
|
|
277
|
+
status: nonblank(t.status, "context.target.status"),
|
|
278
|
+
plan_ref: nullableStr(t.plan_ref, "context.target.plan_ref"),
|
|
279
|
+
has_plan_metadata: bool(t.has_plan_metadata, "context.target.has_plan_metadata"),
|
|
280
|
+
},
|
|
281
|
+
expected: { comment_id: commentId, body_digest: bodyDigest },
|
|
282
|
+
provenance: provenanceOf(c.provenance, "context.provenance"),
|
|
283
|
+
objective: {
|
|
284
|
+
id: nonblank(o.id, "context.objective.id"),
|
|
285
|
+
title: str(o.title, "context.objective.title"),
|
|
286
|
+
url: str(o.url, "context.objective.url"),
|
|
287
|
+
},
|
|
288
|
+
prior,
|
|
289
|
+
engagement: str(c.engagement, "context.engagement"),
|
|
290
|
+
warnings: strList(c.warnings, "context.warnings"),
|
|
291
|
+
};
|
|
292
|
+
return { ok: true, context };
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error instanceof Refuse)
|
|
295
|
+
return { ok: false, problem: `${REFINEMENT_CONTEXT_ARTIFACT} ${error.message}` };
|
|
296
|
+
return { ok: false, problem: `${REFINEMENT_CONTEXT_ARTIFACT} could not be decoded` };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Whether a string is the `sha256:<64 lowercase hex>` session-data digest shape. */
|
|
301
|
+
export function isArtifactDigest(value: string): boolean {
|
|
302
|
+
return SAFE_ARTIFACT_DIGEST.test(value);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// --------------------------------------------------------------------------- the transfer
|
|
306
|
+
|
|
307
|
+
export type ValidateContextTransferResult =
|
|
308
|
+
| { ok: true; read: RefinementContextRead }
|
|
309
|
+
| { ok: false; problem: string };
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Validate a raw transferred context string against its declared digest and the session's run:
|
|
313
|
+
* the digest is computed over the EXACT string (final LF included), the shape is decoded
|
|
314
|
+
* strictly, and `run_id` must be the current run. Parsing is validation only — the returned
|
|
315
|
+
* `raw` is the caller's unchanged string. Never throws.
|
|
316
|
+
*/
|
|
317
|
+
export function validateContextTransfer(
|
|
318
|
+
raw: string,
|
|
319
|
+
opts: { runId: string; expectedDigest?: string },
|
|
320
|
+
): ValidateContextTransferResult {
|
|
321
|
+
const digest = digestSessionData(raw);
|
|
322
|
+
if (opts.expectedDigest !== undefined) {
|
|
323
|
+
if (!isArtifactDigest(opts.expectedDigest))
|
|
324
|
+
return { ok: false, problem: "the declared context digest is not a sha256: digest" };
|
|
325
|
+
if (opts.expectedDigest !== digest)
|
|
326
|
+
return { ok: false, problem: "the transferred context bytes do not match their digest" };
|
|
327
|
+
}
|
|
328
|
+
const decoded = decodeRefinementContext(raw);
|
|
329
|
+
if (!decoded.ok) return decoded;
|
|
330
|
+
if (decoded.context.run_id !== opts.runId)
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
problem: `the transferred context belongs to run ${JSON.stringify(decoded.context.run_id)}, not this run`,
|
|
334
|
+
};
|
|
335
|
+
return { ok: true, read: { context: decoded.context, raw, digest } };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export type ImportRefinementContextResult =
|
|
339
|
+
| { status: "imported"; receipt: SessionArtifactReceipt; read: RefinementContextRead }
|
|
340
|
+
| { status: "unchanged"; receipt: SessionArtifactReceipt; read: RefinementContextRead }
|
|
341
|
+
| { status: "rejected"; problem: string }
|
|
342
|
+
| { status: "unverified"; problem: string };
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Persist a validated transfer as the session's context artifact — the UNCHANGED raw string
|
|
346
|
+
* through the strict session write. `unchanged` is the byte-identical short-circuit (a
|
|
347
|
+
* re-prepared context whose bytes happen to be identical is no new context). Never throws.
|
|
348
|
+
*/
|
|
349
|
+
export function importRefinementContext(
|
|
350
|
+
session: WorkflowSession,
|
|
351
|
+
read: RefinementContextRead,
|
|
352
|
+
): ImportRefinementContextResult {
|
|
353
|
+
const written = session.writeArtifact(REFINEMENT_CONTEXT_ARTIFACT, read.raw, {
|
|
354
|
+
provenance: "strict",
|
|
355
|
+
});
|
|
356
|
+
switch (written.status) {
|
|
357
|
+
case "applied":
|
|
358
|
+
return { status: "imported", receipt: written.receipt, read };
|
|
359
|
+
case "unchanged":
|
|
360
|
+
return { status: "unchanged", receipt: written.receipt, read };
|
|
361
|
+
case "rejected":
|
|
362
|
+
return { status: "rejected", problem: written.problem };
|
|
363
|
+
case "unverified":
|
|
364
|
+
return { status: "unverified", problem: written.problem };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export type ColdImportResult =
|
|
369
|
+
| { status: "imported" | "unchanged"; read: RefinementContextRead }
|
|
370
|
+
| { status: "not-applicable" }
|
|
371
|
+
| { status: "refused"; problem: string };
|
|
372
|
+
|
|
373
|
+
/** The transfer read port the edge binds to the run's fixed scratch path (`null` = missing). */
|
|
374
|
+
export interface ColdImportPorts {
|
|
375
|
+
readTransfer(): string | null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The cold claim's ONE-TIME context import (contracts.md §8.67): on the actual `objective-refine`
|
|
380
|
+
* cold claim, read the fixed run-scratch transfer the door materialized (through the edge-bound
|
|
381
|
+
* port), validate its declared digest (from the namespaced handoff block), run, stage and strict
|
|
382
|
+
* shape, and write the exact raw string as the session artifact. `not-applicable` for every
|
|
383
|
+
* non-refinement claim; every defect is a fail-closed `refused` (the session stays read-only and
|
|
384
|
+
* unusable for drafting — never an orphan repair, missing-pointer reimport or target refresh).
|
|
385
|
+
* Never throws.
|
|
386
|
+
*/
|
|
387
|
+
export function importColdRefinementContext(
|
|
388
|
+
session: WorkflowSession,
|
|
389
|
+
claim: { runId: string; stage: string | undefined; handoff: Record<string, unknown> },
|
|
390
|
+
ports: ColdImportPorts,
|
|
391
|
+
): ColdImportResult {
|
|
392
|
+
if (claim.stage !== REFINE_STAGE) return { status: "not-applicable" };
|
|
393
|
+
const refuse = (problem: string): ColdImportResult => ({ status: "refused", problem });
|
|
394
|
+
const identity = session.currentRunIdentity();
|
|
395
|
+
if (!identity.ok) return refuse(`session identity ${identity.reason}`);
|
|
396
|
+
if (identity.runId !== claim.runId) return refuse("the claimed run is not the session's run");
|
|
397
|
+
const block = claim.handoff[REFINEMENT_HANDOFF_KEY];
|
|
398
|
+
if (typeof block !== "object" || block === null || Array.isArray(block))
|
|
399
|
+
return refuse(`the handoff carries no ${REFINEMENT_HANDOFF_KEY} block`);
|
|
400
|
+
const declared = (block as Record<string, unknown>).context_digest;
|
|
401
|
+
if (typeof declared !== "string" || !isArtifactDigest(declared))
|
|
402
|
+
return refuse("the handoff's context_digest is not a sha256: digest");
|
|
403
|
+
let raw: string | null;
|
|
404
|
+
try {
|
|
405
|
+
raw = ports.readTransfer();
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return refuse(`could not read the context transfer (${String(error)})`);
|
|
408
|
+
}
|
|
409
|
+
if (raw === null) return refuse("the context transfer is missing from the run's scratch");
|
|
410
|
+
const validated = validateContextTransfer(raw, { runId: claim.runId, expectedDigest: declared });
|
|
411
|
+
if (!validated.ok) return refuse(validated.problem);
|
|
412
|
+
const imported = importRefinementContext(session, validated.read);
|
|
413
|
+
switch (imported.status) {
|
|
414
|
+
case "imported":
|
|
415
|
+
case "unchanged":
|
|
416
|
+
return { status: imported.status, read: imported.read };
|
|
417
|
+
case "rejected":
|
|
418
|
+
case "unverified":
|
|
419
|
+
return refuse(`could not persist the context artifact (${imported.problem})`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The classified context resume: `absent` is the genuine no-context arm; `refused` is a
|
|
424
|
+
* fail-closed STOP (a corrupt/orphan/foreign artifact never falls back to anything). */
|
|
425
|
+
export type ResumeRefinementContextResult =
|
|
426
|
+
| { kind: "valid"; read: RefinementContextRead }
|
|
427
|
+
| { kind: "absent" }
|
|
428
|
+
| { kind: "refused"; problem: string };
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Strictly resume the session's context artifact: the strict seam read (an orphan pointer or a
|
|
432
|
+
* foreign/wrong-run artifact is `invalid` there), the strict decode, and the run check against
|
|
433
|
+
* the session identity. Returns the raw bytes + digest beside the fields. Never throws.
|
|
434
|
+
*/
|
|
435
|
+
export function resumeRefinementContext(session: WorkflowSession): ResumeRefinementContextResult {
|
|
436
|
+
const identity = session.currentRunIdentity();
|
|
437
|
+
if (!identity.ok) return { kind: "refused", problem: `session identity ${identity.reason}` };
|
|
438
|
+
const read: ReadArtifactResult = session.readArtifact(REFINEMENT_CONTEXT_ARTIFACT, {
|
|
439
|
+
provenance: "strict",
|
|
440
|
+
});
|
|
441
|
+
if (read.status === "absent") return { kind: "absent" };
|
|
442
|
+
if (read.status === "invalid") return { kind: "refused", problem: read.problem };
|
|
443
|
+
const validated = validateContextTransfer(read.content, { runId: identity.runId });
|
|
444
|
+
if (!validated.ok) return { kind: "refused", problem: validated.problem };
|
|
445
|
+
return { kind: "valid", read: validated.read };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** The one-line target summary every refinement surface relays (never the full context). */
|
|
449
|
+
export function describeRefinementTarget(context: RefinementContext): string {
|
|
450
|
+
const t = context.target;
|
|
451
|
+
return (
|
|
452
|
+
`objective ${context.objective.id} node ${t.identity.node_id} (${t.status}; ` +
|
|
453
|
+
`${t.carrier_identifier}${context.prior !== null ? "; re-refining a prior refinement" : ""})`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* The capture-time observation line — the ONLY way the interior labels code provenance
|
|
459
|
+
* (contracts.md §8.67): a dated observation, never "verified", "frozen" or "current" code.
|
|
460
|
+
*/
|
|
461
|
+
export function checkoutObservationLine(provenance: RefinementProvenance): string {
|
|
462
|
+
const cb = provenance.code_basis;
|
|
463
|
+
return (
|
|
464
|
+
`Checkout observation captured at ${cb.captured_at}: HEAD ${cb.head_sha}, dirty ${cb.dirty}; ` +
|
|
465
|
+
"uncommitted files were not snapshotted and later checkout changes are not detected. " +
|
|
466
|
+
"This is not a freshness guarantee."
|
|
467
|
+
);
|
|
468
|
+
}
|