@mgiles/perk 3.1.0 → 3.2.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/extension/doors/address.ts +11 -0
- package/extension/doors/dreamWaveTools.ts +29 -15
- package/extension/doors/land.ts +6 -0
- package/extension/doors/learn.ts +16 -3
- package/extension/doors/lifecycleGates.ts +36 -1
- package/extension/doors/objectiveStack.ts +423 -23
- package/extension/doors/plannotatorHandoff.ts +80 -8
- package/extension/doors/prReview.ts +2 -1
- package/extension/doors/prReviewBrowser.ts +75 -27
- package/extension/doors/ready.ts +209 -17
- package/extension/doors/reviewWaveTools.ts +24 -3
- package/extension/doors/stackReviewBrowser.ts +573 -0
- package/extension/doors/submit.ts +36 -10
- package/extension/doors/submitPrReview.ts +116 -19
- package/extension/factories/objectivePlan.ts +12 -6
- package/extension/factories/objectiveSave.ts +5 -2
- package/extension/index.ts +26 -1
- package/extension/substrate/config.ts +4 -2
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/resolverLease.ts +363 -0
- package/extension/substrate/toolGating.ts +16 -0
- package/extension/substrate/workflowState.ts +13 -3
- package/extension/waves/adversarialReviewWave.ts +16 -2
- package/package.json +1 -1
- package/prompts/_fixtures/live.yaml +63 -0
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/stages/conflict-resolution-continuation.md +6 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- 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-reconcile-ready.md +7 -0
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +23 -0
- package/shared/README.md +0 -3
- package/shared/bindings.yaml +3 -0
- package/shared/contracts.md +2010 -1753
- package/shared/registry.yaml +16 -1
- package/shared/schemas/outputs/objective-stack-status.schema.json +172 -1
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/contracts-history.md +0 -605
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
runReviewClassifierWave,
|
|
47
47
|
} from "../waves/reviewClassifierWave.ts";
|
|
48
48
|
import { createRpcWaveAdapter } from "../waves/rpcAdapter.ts";
|
|
49
|
+
import { planningStageRefusal } from "./lifecycleGates.ts";
|
|
49
50
|
import { driveConflictResolution, type SubmitOk, submitPr } from "./submit.ts";
|
|
50
51
|
|
|
51
52
|
export interface ThreadInput {
|
|
@@ -284,6 +285,10 @@ export async function finalizeAddress(
|
|
|
284
285
|
params: ResolveParams,
|
|
285
286
|
): Promise<FinalizeAddressResult> {
|
|
286
287
|
const fail = failFor<FinalizeAddressFailExtras>(ctx, "address", "finalize_address");
|
|
288
|
+
// Planning sessions never legitimately publish review fixes — the first check (a positioned
|
|
289
|
+
// stacked planning session's cwd binding is the PREDECESSOR).
|
|
290
|
+
const planningRefusal = planningStageRefusal(ctx, "address");
|
|
291
|
+
if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
|
|
287
292
|
if (params.threads.length === 0) {
|
|
288
293
|
return fail("no threads to finalize (pass { threads: [{thread_id, comment?}] })", "bad_input");
|
|
289
294
|
}
|
|
@@ -529,6 +534,12 @@ export function registerAddress(pi: ExtensionAPI): void {
|
|
|
529
534
|
"Classify PR review feedback (isolated child) and resolve threads (submit → address). " +
|
|
530
535
|
"Pass --preview to classify only (take no action).",
|
|
531
536
|
handler: async (args, ctx) => {
|
|
537
|
+
// Planning sessions never legitimately run the review loop — the first check.
|
|
538
|
+
const planningRefusal = planningStageRefusal(ctx, "address");
|
|
539
|
+
if (planningRefusal !== null) {
|
|
540
|
+
report(ctx, "address", "warning", planningRefusal);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
532
543
|
const preview = /(^|\s)--preview(\s|$)/.test(args ?? "");
|
|
533
544
|
// `/address` needs an active plan-ref (the converged body carries the PR identity, and the
|
|
534
545
|
// classifier child's `perk pr feedback` hard-errors `no_plan_ref` without one). Mirror the
|
|
@@ -131,12 +131,13 @@ function resultText(details: DreamWaveOk): string {
|
|
|
131
131
|
* record) and returns whether the cleared state was VERIFIED (append + read-back) — a false
|
|
132
132
|
* return stops the wave before any filesystem work or spawn, because proceeding over an
|
|
133
133
|
* unverified invalidation could leave a prior bundle + prior digest pair recoverable; `set`
|
|
134
|
-
* publishes the finalized bytes' digest and
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* publishes the finalized bytes' digest and returns the append+read-back verification — a
|
|
135
|
+
* failed set makes the wave outcome honestly incomplete (the marker stays cleared by the
|
|
136
|
+
* entry clear, so recovery refuses — fail-closed, never silent). The registered execute wires
|
|
137
|
+
* the production `appendWorkflowState` pair; tests inject fakes. */
|
|
137
138
|
export interface DreamBundleMarkers {
|
|
138
139
|
clear(): boolean;
|
|
139
|
-
set(digest: string):
|
|
140
|
+
set(digest: string): boolean;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
/**
|
|
@@ -175,7 +176,10 @@ export interface DreamBundleMarkers {
|
|
|
175
176
|
* undraftable), returning ok with `complete: false` and the bracket recorded;
|
|
176
177
|
* 8. bracket ok ⇒ the finalize-in-place rewrite of the same fixed name; a throw ⇒ the second
|
|
177
178
|
* post-launch `io_error` fail arm (mirroring arm 5's extras); on success
|
|
178
|
-
* `markers.set(digest)` with the sha256 of the finalized bytes
|
|
179
|
+
* `markers.set(digest)` with the sha256 of the finalized bytes — a failed set (an
|
|
180
|
+
* unverified append) makes the OK aggregate `complete: false` with a named
|
|
181
|
+
* `digest-marker` failure entry (the wave ran; the outcome is honestly incomplete —
|
|
182
|
+
* never the `io_error` fail arm), and the marker stays cleared so recovery refuses.
|
|
179
183
|
*/
|
|
180
184
|
export async function executeDreamWave(
|
|
181
185
|
adapter: WaveAdapter,
|
|
@@ -324,6 +328,7 @@ export async function executeDreamWave(
|
|
|
324
328
|
// honest coverage reporting.
|
|
325
329
|
bracket = opts.bracket();
|
|
326
330
|
}
|
|
331
|
+
let markerSet = false;
|
|
327
332
|
if (bracket?.ok === true) {
|
|
328
333
|
// Finalize in place — the SAME fixed name gains the reducers section (never a second
|
|
329
334
|
// file), then the digest marker publishes the finalized bytes for the recovery consumer.
|
|
@@ -344,13 +349,24 @@ export async function executeDreamWave(
|
|
|
344
349
|
attempts,
|
|
345
350
|
});
|
|
346
351
|
}
|
|
347
|
-
// A failed marker append
|
|
348
|
-
//
|
|
349
|
-
|
|
352
|
+
// A failed marker append (an unverified read-back) leaves the marker cleared by the
|
|
353
|
+
// entry clear — recovery refuses (fail-closed) — and the aggregate reports the outcome
|
|
354
|
+
// as honestly incomplete; re-running the wave repairs it.
|
|
355
|
+
markerSet = opts.markers.set(digestSessionData(finalized));
|
|
350
356
|
}
|
|
351
357
|
|
|
358
|
+
const failures = [...reducers.failures];
|
|
359
|
+
if (bracket?.ok === true && !markerSet) {
|
|
360
|
+
failures.push({
|
|
361
|
+
angle: "digest-marker",
|
|
362
|
+
reason: "run-failed",
|
|
363
|
+
detail:
|
|
364
|
+
"dream_bundle_digest marker append failed its read-back — the marker stays cleared, " +
|
|
365
|
+
"so recovery refuses this bundle; re-run perk learn dream",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
352
368
|
const details: DreamWaveOk = {
|
|
353
|
-
complete: analysis.complete && reducers.complete && bracket?.ok === true,
|
|
369
|
+
complete: analysis.complete && reducers.complete && bracket?.ok === true && markerSet,
|
|
354
370
|
analysis: analysisDetails,
|
|
355
371
|
bracket,
|
|
356
372
|
bundle: {
|
|
@@ -365,7 +381,7 @@ export async function executeDreamWave(
|
|
|
365
381
|
skip_reason: null,
|
|
366
382
|
complete: reducers.complete,
|
|
367
383
|
reports: reducers.reports,
|
|
368
|
-
failures
|
|
384
|
+
failures,
|
|
369
385
|
},
|
|
370
386
|
attempts,
|
|
371
387
|
};
|
|
@@ -444,8 +460,8 @@ export function registerDreamWave(pi: ExtensionAPI): void {
|
|
|
444
460
|
const reducerModel = subagentModel(ctx.cwd, "dream-reducer");
|
|
445
461
|
// The production digest-marker pair: the ordinary strict-append session-entry channel.
|
|
446
462
|
// The boolean is the seam's verified append+read-back result — the execute core refuses
|
|
447
|
-
// the wave on an unverified CLEAR (fail-closed); a failed SET
|
|
448
|
-
// (the entry clear already invalidated, so recovery refuses).
|
|
463
|
+
// the wave on an unverified CLEAR (fail-closed); a failed SET makes the aggregate
|
|
464
|
+
// honestly incomplete (the entry clear already invalidated, so recovery refuses).
|
|
449
465
|
const marker = (digest: string): boolean =>
|
|
450
466
|
appendWorkflowState(pi, ctx, {
|
|
451
467
|
data: { dream_bundle_digest: digest },
|
|
@@ -462,9 +478,7 @@ export function registerDreamWave(pi: ExtensionAPI): void {
|
|
|
462
478
|
bracket: () => revalidationBracket(ctx.cwd, decoded.manifest.commit_sha),
|
|
463
479
|
markers: {
|
|
464
480
|
clear: () => marker(""),
|
|
465
|
-
set: (digest) =>
|
|
466
|
-
marker(digest);
|
|
467
|
-
},
|
|
481
|
+
set: (digest) => marker(digest),
|
|
468
482
|
},
|
|
469
483
|
...(analystModel !== undefined ? { analystModel } : {}),
|
|
470
484
|
...(reducerModel !== undefined ? { reducerModel } : {}),
|
package/extension/doors/land.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { registerPerkCommand } from "../substrate/command.ts";
|
|
22
22
|
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
23
23
|
import { report } from "../surfaces/report.ts";
|
|
24
|
+
import { planningStageRefusal } from "./lifecycleGates.ts";
|
|
24
25
|
|
|
25
26
|
// Learn-consume skip reasons that are ordinary, not failures: non-factory plans carry no
|
|
26
27
|
// `consumed_learn` (`no_consumed_learn`), and a dry run reports `dry_run`. Anything else surfaces.
|
|
@@ -132,6 +133,11 @@ function decodeLand(payload: ColdJson): LandPayload | null {
|
|
|
132
133
|
export async function landPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise<LandResult> {
|
|
133
134
|
const fail = failFor(ctx, "land");
|
|
134
135
|
|
|
136
|
+
// Planning sessions never legitimately land — the first check, before any cold-door
|
|
137
|
+
// delegation (a positioned stacked planning session's cwd binding is the PREDECESSOR).
|
|
138
|
+
const planningRefusal = planningStageRefusal(ctx, "land");
|
|
139
|
+
if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
|
|
140
|
+
|
|
135
141
|
const r = await runColdDoor<LandPayload>(pi, ctx, ["pr", "land", "--json"], {
|
|
136
142
|
label: "perk pr land",
|
|
137
143
|
decode: decodeLand,
|
package/extension/doors/learn.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
// it validates the angle selection in code (2–4 angles, `session-deviations` mandatory — the
|
|
14
14
|
// §8.35 policy as tested implementation), derives the manifest path from the relayed
|
|
15
15
|
// `bundle_dir`, resolves the analyst model from `[models.subagents] learn-analyst` (because
|
|
16
|
-
// `
|
|
17
|
-
// workflow-level `model` default), and runs 2–4 fresh-context `perk.learn-analyst` lanes through
|
|
16
|
+
// an `agentOverrides` model can never displace the def's frontmatter-pinned `model:`, the model
|
|
17
|
+
// rides the wave as the workflow-level `model` default), and runs 2–4 fresh-context `perk.learn-analyst` lanes through
|
|
18
18
|
// `runLearnWave` (best-effort completeness: a failed analyst is an explicitly-reported skipped
|
|
19
19
|
// angle). A wave-level failure soft-fails LOUDLY — never a silent fallback to model-authored
|
|
20
20
|
// scripts; the guidance routes the parent to a single-context analysis of the bundle instead.
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
type WaveAttemptReceipt,
|
|
72
72
|
} from "../waves/reportWave.ts";
|
|
73
73
|
import { createRpcWaveAdapter } from "../waves/rpcAdapter.ts";
|
|
74
|
-
import { planReadInstruction } from "./lifecycleGates.ts";
|
|
74
|
+
import { planningStageRefusal, planReadInstruction } from "./lifecycleGates.ts";
|
|
75
75
|
|
|
76
76
|
/** The ok-arm fields. */
|
|
77
77
|
export interface LearnOk {
|
|
@@ -190,6 +190,12 @@ export async function learnDone(
|
|
|
190
190
|
const trimmed = (summary ?? "").trim();
|
|
191
191
|
const fail = failFor(ctx, "learn");
|
|
192
192
|
|
|
193
|
+
// Planning sessions never legitimately run the learn cycle — the first check, before any
|
|
194
|
+
// cold-door delegation (a positioned stacked planning session's cwd binding is the
|
|
195
|
+
// PREDECESSOR).
|
|
196
|
+
const planningRefusal = planningStageRefusal(ctx, "learn");
|
|
197
|
+
if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
|
|
198
|
+
|
|
193
199
|
// No summary: record the deliberate skip canonically (the cold door stamps the plan-header and
|
|
194
200
|
// clears the marker; the skip carries no classification, so `decision`/`target` are
|
|
195
201
|
// intentionally ignored on this arm). On failure the marker is NOT cleared — never silently
|
|
@@ -567,6 +573,13 @@ export function registerLearn(pi: ExtensionAPI): void {
|
|
|
567
573
|
"/learn skip records the skip on the plan and clears pending-learn; " +
|
|
568
574
|
"/learn <text> captures the text verbatim.",
|
|
569
575
|
handler: async (args, ctx) => {
|
|
576
|
+
// Planning sessions never legitimately run the learn cycle — the first check (the
|
|
577
|
+
// orchestrating bare-/learn arm below never reaches learnDone, so it needs its own gate).
|
|
578
|
+
const planningRefusal = planningStageRefusal(ctx, "learn");
|
|
579
|
+
if (planningRefusal !== null) {
|
|
580
|
+
report(ctx, "learn", "warning", planningRefusal);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
570
583
|
const trimmed = (args ?? "").trim();
|
|
571
584
|
|
|
572
585
|
// Explicit text (or `skip`): the existing learnDone path — capture verbatim / record skip.
|
|
@@ -16,9 +16,44 @@ import type {
|
|
|
16
16
|
import type { PlanRef } from "../substrate/cache.ts";
|
|
17
17
|
import { registerPerkCommand } from "../substrate/command.ts";
|
|
18
18
|
import { render } from "../substrate/prompts.ts";
|
|
19
|
-
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
19
|
+
import { type BranchSource, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
20
20
|
import { report } from "../surfaces/report.ts";
|
|
21
21
|
|
|
22
|
+
// The planning stages whose sessions never legitimately run lifecycle doors. After an approved
|
|
23
|
+
// save, a still-live planning session holds TWO plan identities — the cwd binding (a positioned
|
|
24
|
+
// stacked session's predecessor checkout, read via readPlanRef(ctx.cwd)) and the just-saved plan
|
|
25
|
+
// on active_plan_ref — so a door invocation there could act on the predecessor. At the repo root
|
|
26
|
+
// the same invocation fails confusingly today; the refusal is honest in both shapes.
|
|
27
|
+
const PLANNING_STAGES = new Set(["plan", "objective-plan"]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The planning-stage lifecycle-door refusal (the shared first check of the warm /submit,
|
|
31
|
+
* /address, /land, and /learn doors): when this session's workflow-state `stage` is a planning
|
|
32
|
+
* stage, return the refusal message directing the human at the fresh-session implement door;
|
|
33
|
+
* `null` otherwise (non-planning stages — and stage-less sessions — are unaffected). Fail-CLOSED
|
|
34
|
+
* on an unreadable branch: without the state this guard cannot prove the session is not a
|
|
35
|
+
* positioned planning session (whose cwd binding is the PREDECESSOR — the exact target it
|
|
36
|
+
* protects), so an unreadable read refuses rather than letting the door act.
|
|
37
|
+
*/
|
|
38
|
+
export function planningStageRefusal(ctx: BranchSource, door: string): string | null {
|
|
39
|
+
let stage: string | undefined;
|
|
40
|
+
try {
|
|
41
|
+
stage = rebuildWorkflowState(branchOf(ctx)).stage;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return (
|
|
44
|
+
`${door} is unavailable: the session's workflow state could not be read ` +
|
|
45
|
+
`(${String(error)}), so this cannot be proven not to be a planning session — ` +
|
|
46
|
+
"retry, or implement the saved plan with `perk impl <N>` in a fresh session."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (stage === undefined || !PLANNING_STAGES.has(stage)) return null;
|
|
50
|
+
return (
|
|
51
|
+
`${door} is unavailable in a planning session (stage ${stage}): a planning session can ` +
|
|
52
|
+
"hold two plan identities (its checkout's own binding and the just-saved plan) — " +
|
|
53
|
+
"implement the saved plan with `perk impl <N>` in a fresh session instead."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
22
57
|
const DIRTY_MESSAGE = "uncommitted changes — commit or stash before switching/forking this stage.";
|
|
23
58
|
const HANDOFF_DIRTY_MESSAGE =
|
|
24
59
|
"uncommitted changes — commit before a fresh-context /implement handoff (the plan is the " +
|