@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
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
// The warm `objective_save` door, the objective mirror of planSave.ts. The in-session twin
|
|
2
|
-
// of the Python cold door (`perk objective create`): a deterministic, terminating tool + command
|
|
3
|
-
// that WRAP the existing storage — they do NOT reimplement the GitHub write. `saveObjective()`
|
|
4
|
-
// passes the STRUCTURED roadmap as `--roadmap <json>` (the agent never hand-writes roadmap YAML)
|
|
5
|
-
// and delegates to `perk objective create --json` via the shared cold-door client (`runColdDoor`,
|
|
6
|
-
// the prose rides the run-scratch stdin channel), then links the live session: `active_objective` + a fresh `perk:objective-budget` activation marker
|
|
7
|
-
// (mirrors the `/objective <id>` activation in objective.ts). Failures are loud-but-non-fatal.
|
|
8
|
-
//
|
|
9
|
-
// APPROVAL→SAVE ORCHESTRATION (mirroring planSave.ts's `approvalSave`). The
|
|
10
|
-
// exported `objectiveApprovalSave` seam is the shared APPROVED-review → save flow: re-read the
|
|
11
|
-
// STRUCTURED artifact (`readObjectiveDraft` — never the rendered markdown, never the transcript)
|
|
12
|
-
// → `saveObjective` → D1a gate exit on a successful save (snapshot `gating.isActive()` BEFORE the
|
|
13
|
-
// save; a failed save leaves the gate ON). `plan_review`'s objective arm (planReview.ts) wires
|
|
14
|
-
// its APPROVED outcome into it; the `/objective-save` command is the artifact-first MANUAL
|
|
15
|
-
// FAILSAFE invocation of the same seam, keeping the legacy drive-the-session behavior as the
|
|
16
|
-
// no-draft fallback (objectives have no transcript scrape by design).
|
|
17
|
-
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
21
|
-
import { atomicWriteFileSync, ensureRunScratch } from "../substrate/cache.ts";
|
|
22
|
-
import {
|
|
23
|
-
booleanField,
|
|
24
|
-
type ColdJson,
|
|
25
|
-
objectField,
|
|
26
|
-
runColdDoor,
|
|
27
|
-
stringField,
|
|
28
|
-
} from "../substrate/coldDoor.ts";
|
|
29
|
-
import { registerPerkCommand } from "../substrate/command.ts";
|
|
30
|
-
import { render } from "../substrate/prompts.ts";
|
|
31
|
-
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
32
|
-
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
33
|
-
import { appendWorkflowState, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
34
|
-
import { report, type Severity } from "../surfaces/report.ts";
|
|
35
|
-
import { OBJECTIVE_BUDGET_TYPE } from "./objective.ts";
|
|
36
|
-
import {
|
|
37
|
-
DELIVERY_PARAM_SCHEMA,
|
|
38
|
-
type DeliveryChoice,
|
|
39
|
-
DREAM_REPORT_PARAM_SCHEMA,
|
|
40
|
-
decodeObjectiveSaveParams,
|
|
41
|
-
ROADMAP_PARAM_SCHEMA,
|
|
42
|
-
readObjectiveDraft,
|
|
43
|
-
} from "./objectiveDraft.ts";
|
|
44
|
-
import { resolveDreamReportGate } from "./objectiveDreamReport.ts";
|
|
45
|
-
|
|
46
|
-
/** The `objective-save` registry stage id (the objectiveAuthor.ts constant's sibling). */
|
|
47
|
-
export const OBJECTIVE_SAVE_STAGE = "objective-save";
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The run-scoped dream-report transfer filename (contracts §8.64) — the extension→door handoff
|
|
51
|
-
* carrying the reviewed CANONICAL parts. The Python mirror is
|
|
52
|
-
* `perk.learn.dream_companion.DREAM_REPORT_TRANSFER_FILENAME` (parity-pinned by test), beside
|
|
53
|
-
* the existing `DREAM_MANIFEST_FILENAME` mirror pair.
|
|
54
|
-
*/
|
|
55
|
-
export const DREAM_REPORT_TRANSFER_FILENAME = "dream-report-transfer.json";
|
|
56
|
-
|
|
57
|
-
/** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
|
|
58
|
-
export interface ObjectiveSaveOk {
|
|
59
|
-
/** `id` is the opaque string objective id (GitHub "7", Linear "ENG-7") — §8.21. */
|
|
60
|
-
objective: { id: string; url: string };
|
|
61
|
-
existed: boolean | null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type ObjectiveSaveResult = Result<ObjectiveSaveOk>;
|
|
65
|
-
|
|
66
|
-
/** The decoded `perk objective create --json` payload slice the warm door consumes. */
|
|
67
|
-
interface ObjectiveCreatePayload {
|
|
68
|
-
objective: { id: string; url: string; existed: boolean | undefined };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Narrow the `perk objective create --json` success payload; strict on `objective`. */
|
|
72
|
-
function decodeObjectiveCreate(payload: ColdJson): ObjectiveCreatePayload | null {
|
|
73
|
-
const objective = objectField(payload, "objective");
|
|
74
|
-
if (objective === undefined) return null;
|
|
75
|
-
const id = stringField(objective, "id");
|
|
76
|
-
const url = stringField(objective, "url");
|
|
77
|
-
if (id === undefined || url === undefined) return null;
|
|
78
|
-
return { objective: { id, url, existed: booleanField(objective, "existed") } };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The single save implementation both surfaces call. Delegates the GitHub write to the Python cold
|
|
83
|
-
* door, then links the live session (`active_objective` + budget marker). Returns a soft result
|
|
84
|
-
* (never throws); failures set `details.ok = false` and append no linkage.
|
|
85
|
-
*
|
|
86
|
-
* `dream_report` is ONE carrier with two sources (§8.63): the direct tool path supplies
|
|
87
|
-
* `{input}` and the save stamps `generated_at`; the approval path passes the artifact block
|
|
88
|
-
* through with its stored stamp AND stored parts — the stored parts are byte-compared against
|
|
89
|
-
* the freshly re-rendered ones, so run-scratch drift or artifact tamper between draft-write
|
|
90
|
-
* and save refuses `bad_state` (nothing saved, the gate stays on). The parts do NOT cross to
|
|
91
|
-
* the Python plane yet — companion persistence is deferred (explicit in §8.63, not silent).
|
|
92
|
-
*/
|
|
93
|
-
export async function saveObjective(
|
|
94
|
-
pi: ExtensionAPI,
|
|
95
|
-
ctx: ExtensionContext,
|
|
96
|
-
opts: {
|
|
97
|
-
prose: string;
|
|
98
|
-
title?: string;
|
|
99
|
-
roadmap?: unknown[];
|
|
100
|
-
base?: string;
|
|
101
|
-
delivery?: DeliveryChoice;
|
|
102
|
-
dream_report?: { input: unknown; generated_at?: string; parts?: string[] };
|
|
103
|
-
},
|
|
104
|
-
): Promise<ObjectiveSaveResult> {
|
|
105
|
-
const fail = failFor(ctx, "objective-save");
|
|
106
|
-
|
|
107
|
-
const prose = opts.prose.trim();
|
|
108
|
-
if (!prose)
|
|
109
|
-
return fail("no objective prose to save (draft the objective first)", "invalid_input");
|
|
110
|
-
if (opts.roadmap !== undefined && !Array.isArray(opts.roadmap)) {
|
|
111
|
-
return fail("roadmap must be a JSON array of nodes", "invalid_input");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// The §8.63 fail-closed re-validation, before anything reaches the cold door. Presence is
|
|
115
|
-
// the `opts.dream_report === undefined` boundary — an `{input: undefined}` carrier is never
|
|
116
|
-
// constructed (the execute wraps only a present decoded value; the approval path passes the
|
|
117
|
-
// validated artifact block).
|
|
118
|
-
const generatedAt = opts.dream_report?.generated_at ?? new Date().toISOString();
|
|
119
|
-
const gate =
|
|
120
|
-
opts.dream_report === undefined
|
|
121
|
-
? resolveDreamReportGate(ctx, undefined, generatedAt)
|
|
122
|
-
: resolveDreamReportGate(ctx, opts.dream_report.input, generatedAt);
|
|
123
|
-
if (gate.kind === "refuse") {
|
|
124
|
-
return fail(gate.detail, gate.errorType);
|
|
125
|
-
}
|
|
126
|
-
if (gate.kind === "block" && opts.dream_report?.parts !== undefined) {
|
|
127
|
-
// The approval path: the reviewed (stored) parts must byte-match the re-render against
|
|
128
|
-
// freshly recovered context — the same stored `generated_at` stamp keeps the comparison
|
|
129
|
-
// deterministic.
|
|
130
|
-
if (JSON.stringify(gate.block.parts) !== JSON.stringify(opts.dream_report.parts)) {
|
|
131
|
-
return fail(
|
|
132
|
-
"the reviewed report no longer matches the wave state — re-draft and re-review",
|
|
133
|
-
"bad_state",
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const branch = () => branchOf(ctx);
|
|
139
|
-
const runId = rebuildWorkflowState(branch()).run_id ?? "";
|
|
140
|
-
|
|
141
|
-
if (gate.kind === "block") {
|
|
142
|
-
// The §8.64 transfer write (the dream arm only): the reviewed CANONICAL parts cross to the
|
|
143
|
-
// Python save door through the run-scoped scratch handoff — written atomically BEFORE the
|
|
144
|
-
// cold door is invoked. A write failure is the soft `scratch_failed` failure (the
|
|
145
|
-
// runColdDoor stdin-staging precedent): the cold door is NOT invoked, nothing activates,
|
|
146
|
-
// and the read-only gate stays on. Non-dream saves write nothing (byte-identical).
|
|
147
|
-
try {
|
|
148
|
-
const dir = ensureRunScratch(ctx.cwd, runId);
|
|
149
|
-
const content = `${JSON.stringify(
|
|
150
|
-
{ schema_version: "1", run_id: runId, parts: gate.block.parts },
|
|
151
|
-
null,
|
|
152
|
-
2,
|
|
153
|
-
)}\n`;
|
|
154
|
-
atomicWriteFileSync(join(dir, DREAM_REPORT_TRANSFER_FILENAME), content);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
return fail(`could not stage the dream-report transfer: ${String(err)}`, "scratch_failed");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const args = ["objective", "create", "--json"];
|
|
161
|
-
if (opts.title) args.push("--title", opts.title);
|
|
162
|
-
if (opts.base) args.push("--base", opts.base);
|
|
163
|
-
// The reviewed delivery choice rides verbatim; the cold door owns validation + preflight.
|
|
164
|
-
if (opts.delivery) args.push("--delivery", opts.delivery);
|
|
165
|
-
if (runId) args.push("--run-id", runId);
|
|
166
|
-
if (opts.roadmap && opts.roadmap.length > 0) {
|
|
167
|
-
args.push("--roadmap", JSON.stringify(opts.roadmap));
|
|
168
|
-
}
|
|
169
|
-
const r = await runColdDoor<ObjectiveCreatePayload>(pi, ctx, args, {
|
|
170
|
-
label: "perk objective create",
|
|
171
|
-
decode: decodeObjectiveCreate,
|
|
172
|
-
stdin: { flag: "--body", content: prose, filename: "objective.md" },
|
|
173
|
-
});
|
|
174
|
-
if (!r.ok) return fail(r.message, r.errorType);
|
|
175
|
-
|
|
176
|
-
// Link the live session: set active_objective (LWW) + seed a fresh budget activation marker
|
|
177
|
-
// (mirrors objective.ts's `/objective <id>` activation), so budget tracking starts immediately.
|
|
178
|
-
// The envelope id is already the opaque string id (§8.21) — no coercion needed.
|
|
179
|
-
const objective = r.data.objective;
|
|
180
|
-
const objectiveId = objective.id;
|
|
181
|
-
const linked = rebuildWorkflowState(branch()).active_objective ?? null;
|
|
182
|
-
if (linked !== objectiveId) {
|
|
183
|
-
appendWorkflowState(pi, ctx, {
|
|
184
|
-
data: { active_objective: objectiveId },
|
|
185
|
-
field: "active_objective",
|
|
186
|
-
expected: objectiveId,
|
|
187
|
-
scope: "objective-save",
|
|
188
|
-
failure: `active_objective read-back failed for #${objectiveId}`,
|
|
189
|
-
});
|
|
190
|
-
pi.appendEntry(OBJECTIVE_BUDGET_TYPE, {
|
|
191
|
-
objective_id: objectiveId,
|
|
192
|
-
activated_at: new Date().toISOString(),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const verb = objective.existed ? "Found existing" : "Saved";
|
|
197
|
-
return ok(
|
|
198
|
-
`${verb} objective #${objective.id} → ${objective.url}`,
|
|
199
|
-
{
|
|
200
|
-
objective: { id: objective.id, url: objective.url },
|
|
201
|
-
existed: objective.existed ?? null,
|
|
202
|
-
},
|
|
203
|
-
{ terminate: true },
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** The approval→save orchestration outcome (the objective `ApprovalSaveOutcome`). */
|
|
208
|
-
export type ObjectiveApprovalSaveOutcome =
|
|
209
|
-
| { status: "no-draft" }
|
|
210
|
-
| { status: "saved" | "save-failed"; result: ObjectiveSaveResult; gateExited: boolean };
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* The shared approval→save orchestration seam (the objective sibling of
|
|
214
|
-
* planSave.ts's `approvalSave`): an APPROVED objective review (`plan_review`'s objective arm)
|
|
215
|
-
* and the manual `/objective-save` failsafe both run THIS. Flow: re-read the STRUCTURED draft
|
|
216
|
-
* artifact at save time (`readObjectiveDraft` — never the rendered markdown, never in-hand
|
|
217
|
-
* bytes) → `saveObjective` → gate exit on a successful save while read-only (the D1a pattern:
|
|
218
|
-
* snapshot `gating.isActive()` before the save; a failed save leaves the gate ON). No draft →
|
|
219
|
-
* `no-draft` (nothing saved, the gate untouched); callers render their own fallback. Title
|
|
220
|
-
* precedence: an explicit `opts.title` wins; else the draft's `title`; else the cold door
|
|
221
|
-
* derives from the prose heading. The returned result keeps `saveObjective`'s `terminate: true`
|
|
222
|
-
* for tool-path callers.
|
|
223
|
-
*/
|
|
224
|
-
export async function objectiveApprovalSave(
|
|
225
|
-
pi: ExtensionAPI,
|
|
226
|
-
ctx: ExtensionContext,
|
|
227
|
-
gating: ToolGating,
|
|
228
|
-
opts: { title?: string } = {},
|
|
229
|
-
): Promise<ObjectiveApprovalSaveOutcome> {
|
|
230
|
-
const draft = readObjectiveDraft(ctx);
|
|
231
|
-
if (draft === null) return { status: "no-draft" };
|
|
232
|
-
// D1a: snapshot the gate BEFORE the save; on success, exit it so save marks the read-only →
|
|
233
|
-
// read-write boundary in one gesture. A failed save leaves the gate on.
|
|
234
|
-
const wasReadOnly = gating.isActive();
|
|
235
|
-
const result = await saveObjective(pi, ctx, {
|
|
236
|
-
prose: draft.prose,
|
|
237
|
-
title: opts.title ?? draft.title,
|
|
238
|
-
roadmap: draft.roadmap,
|
|
239
|
-
base: draft.base,
|
|
240
|
-
delivery: draft.delivery,
|
|
241
|
-
// The approval path passes the artifact block through whole — stored stamp + stored parts
|
|
242
|
-
// (the save re-validates and byte-compares, §8.63).
|
|
243
|
-
...(draft.dream_report !== undefined ? { dream_report: draft.dream_report } : {}),
|
|
244
|
-
});
|
|
245
|
-
let gateExited = false;
|
|
246
|
-
if (result.details.ok && wasReadOnly) {
|
|
247
|
-
gating.exit(ctx);
|
|
248
|
-
gateExited = true;
|
|
249
|
-
}
|
|
250
|
-
return { status: result.details.ok ? "saved" : "save-failed", result, gateExited };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const TOOL_GUIDELINES = [
|
|
254
|
-
"Use objective_save only after the objective + roadmap are decision-complete; it creates the canonical perk:objective issue, activates it, and ends the turn.",
|
|
255
|
-
"Pass objective_save the objective PROSE in `prose` and the STRUCTURED roadmap in `roadmap` (a JSON array of nodes) — never hand-write roadmap YAML.",
|
|
256
|
-
'Each objective_save roadmap node needs a stable `id` (e.g. "1.1") and a `description`; `status` defaults to pending. Use `depends_on` for explicit ordering.',
|
|
257
|
-
];
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* The seed guidance the warm `/objective-save` injects to drive the structured save (the
|
|
261
|
-
* perk-objective-author skill pointer rides the skill-binding suffix — not hardcoded
|
|
262
|
-
* here). Pure + exported for offline tests.
|
|
263
|
-
*/
|
|
264
|
-
export function objectiveSaveGuidance(title?: string): string {
|
|
265
|
-
const named = title?.trim() || "";
|
|
266
|
-
return render("stages/objective-save.md", { title: named });
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Register the warm door: the `objective_save` tool (canonical) + the `/objective-save` twin. */
|
|
270
|
-
export function registerObjectiveSave(pi: ExtensionAPI, gating: ToolGating): void {
|
|
271
|
-
pi.registerTool({
|
|
272
|
-
name: "objective_save",
|
|
273
|
-
label: "Save objective",
|
|
274
|
-
description:
|
|
275
|
-
"Persist a drafted objective + structured roadmap to GitHub as a perk:objective issue, " +
|
|
276
|
-
"activate it, and start budget tracking. Terminating: ends the turn on save. Call only when " +
|
|
277
|
-
"the objective and roadmap are decision-complete.",
|
|
278
|
-
promptSnippet: "Save the decision-complete objective + roadmap to GitHub (terminates the turn)",
|
|
279
|
-
promptGuidelines: TOOL_GUIDELINES,
|
|
280
|
-
executionMode: "sequential",
|
|
281
|
-
parameters: {
|
|
282
|
-
type: "object",
|
|
283
|
-
additionalProperties: false,
|
|
284
|
-
required: ["prose"],
|
|
285
|
-
properties: {
|
|
286
|
-
prose: {
|
|
287
|
-
type: "string",
|
|
288
|
-
description: "The objective prose (the why, the design, the boundaries/non-goals).",
|
|
289
|
-
},
|
|
290
|
-
title: {
|
|
291
|
-
type: "string",
|
|
292
|
-
description: "Optional objective title (defaults to the prose's first heading).",
|
|
293
|
-
},
|
|
294
|
-
base: {
|
|
295
|
-
type: "string",
|
|
296
|
-
description:
|
|
297
|
-
"Optional target branch for this objective's plans (omit to use the repo default).",
|
|
298
|
-
},
|
|
299
|
-
delivery: DELIVERY_PARAM_SCHEMA,
|
|
300
|
-
dream_report: DREAM_REPORT_PARAM_SCHEMA,
|
|
301
|
-
roadmap: {
|
|
302
|
-
type: "array",
|
|
303
|
-
description:
|
|
304
|
-
"The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
|
|
305
|
-
items: ROADMAP_PARAM_SCHEMA,
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
310
|
-
const decoded = decodeObjectiveSaveParams(params);
|
|
311
|
-
if (decoded === null) {
|
|
312
|
-
return failFor(
|
|
313
|
-
ctx,
|
|
314
|
-
"objective-save",
|
|
315
|
-
"objective_save",
|
|
316
|
-
)(
|
|
317
|
-
"objective_save needs { prose: string, roadmap?: array } per the tool schema",
|
|
318
|
-
"bad_input",
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
// The direct tool path wraps ONLY a present decoded value as the `{input}` carrier (the
|
|
322
|
-
// save stamps generated_at); no stored parts, so no byte-compare on this path.
|
|
323
|
-
const { dream_report, ...rest } = decoded;
|
|
324
|
-
return saveObjective(pi, ctx, {
|
|
325
|
-
...rest,
|
|
326
|
-
...(dream_report !== undefined ? { dream_report: { input: dream_report } } : {}),
|
|
327
|
-
});
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
registerPerkCommand(pi, "objective-save", {
|
|
332
|
-
description:
|
|
333
|
-
"Save the working objective draft to GitHub — the manual failsafe for the approval→save " +
|
|
334
|
-
"flow (artifact-first; drives the structured save only when no draft exists).",
|
|
335
|
-
handler: async (args, ctx) => {
|
|
336
|
-
const title = args.trim() || undefined;
|
|
337
|
-
// The artifact-first manual-failsafe invocation of the shared approval→save
|
|
338
|
-
// seam (the D1a gate exit lives in the seam). The legacy drive-the-session behavior is kept
|
|
339
|
-
// as the NO-DRAFT fallback — objectives have no transcript scrape by design, so a draftless
|
|
340
|
-
// session still needs a working save path.
|
|
341
|
-
const outcome = await objectiveApprovalSave(pi, ctx, gating, { title });
|
|
342
|
-
if (outcome.status === "no-draft") {
|
|
343
|
-
// Exit the read-only gate so the objective_save tool (excluded from READ_ONLY_TOOLS)
|
|
344
|
-
// becomes reachable on the driven turn, then drive the turn (mirrors /address and
|
|
345
|
-
// /objective-plan).
|
|
346
|
-
if (gating.isActive()) gating.exit(ctx);
|
|
347
|
-
report(ctx, "objective-save", "info", "handing the structured save to the session");
|
|
348
|
-
// The perk-objective-author pointer rides the skill-binding suffix (D5) since a
|
|
349
|
-
// warm /objective-save outside a stage:objective-author session gets none from Mechanism A.
|
|
350
|
-
pi.sendUserMessage(
|
|
351
|
-
objectiveSaveGuidance(title) + bindingSuffix(ctx.cwd, "stage:objective-author"),
|
|
352
|
-
);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
// Saved or save-failed: relay the save message. No node-link sub-step on the objective path,
|
|
356
|
-
// so the severity ladder is simpler than /plan-save's (no warning tier).
|
|
357
|
-
const result = outcome.result;
|
|
358
|
-
const message = result.content[0]?.text ?? "objective-save done";
|
|
359
|
-
const severity: Severity = result.details.ok ? "info" : "error";
|
|
360
|
-
report(ctx, "objective-save", severity, message);
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
// The `plan_draft` file tool: the first session-data PRODUCER and the
|
|
2
|
-
// narrow structural read-only-gate carve-out (session data dir only).
|
|
3
|
-
//
|
|
4
|
-
// Carve-out doctrine: the tool takes NO path/name parameter — the artifact name is the fixed
|
|
5
|
-
// constant `PLAN_DRAFT_ARTIFACT` and the path is derived exclusively through the session-data
|
|
6
|
-
// accessor seam (`writeSessionArtifact`, sessionData.ts), so the only bytes it can ever write are
|
|
7
|
-
// the one working-plan artifact in the current run's data dir (gitignored scratch). Allowlisting
|
|
8
|
-
// its name in `READ_ONLY_TOOLS` (toolGating.ts) is therefore safe: the read-only invariant (the
|
|
9
|
-
// worktree stays untouched) holds, and the gate's `tool_call` edit/write/bash blocking logic is
|
|
10
|
-
// UNCHANGED. Full rewrite per call, non-terminating; NOT a save — `plan_save`/`/plan-save` still
|
|
11
|
-
// persist to GitHub. Consumers read the draft only via `readSessionArtifact` (digest-validated,
|
|
12
|
-
// fail-open); the artifact is consumed by `resolvePlanSource` (planSave.ts) — both save
|
|
13
|
-
// surfaces prefer it over an explicit param and over the transcript scrape.
|
|
14
|
-
//
|
|
15
|
-
// Imports stay node builtins + sibling seams (sessionData.ts, toolParams.ts, result.ts) so the
|
|
16
|
-
// module loads under `node --test`; no manual `scratch`/`runs` path segments (cacheGuard.test.ts).
|
|
17
|
-
|
|
18
|
-
import { relative } from "node:path";
|
|
19
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
21
|
-
import {
|
|
22
|
-
activeSessionRunId,
|
|
23
|
-
digestSessionData,
|
|
24
|
-
type SessionDataCtx,
|
|
25
|
-
writeSessionArtifact,
|
|
26
|
-
} from "../substrate/sessionData.ts";
|
|
27
|
-
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
28
|
-
import type { EntrySink } from "../substrate/workflowState.ts";
|
|
29
|
-
import type { ReportTarget } from "../surfaces/report.ts";
|
|
30
|
-
|
|
31
|
-
/** The fixed working-plan artifact name (NOT `plan.md` — `cache.plan` is a different file). */
|
|
32
|
-
export const PLAN_DRAFT_ARTIFACT = "plan-draft.md";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Decode unknown `plan_draft` tool-call params (the tool-boundary seam). `plan` absent decodes to
|
|
36
|
-
* `""` (so the core's `invalid_input` arm owns the empty-plan message); present-but-mistyped →
|
|
37
|
-
* null (strict-fail `bad_input`). Decode-before-side-effect.
|
|
38
|
-
*/
|
|
39
|
-
export function decodePlanDraftParams(params: unknown): { plan: string } | null {
|
|
40
|
-
const p = paramsOf(params);
|
|
41
|
-
if (p === null) return null;
|
|
42
|
-
const plan = stringParam(p, "plan");
|
|
43
|
-
if (plan === null) return null;
|
|
44
|
-
return { plan: plan ?? "" };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The ok-arm details — provenance-consistent with the recorded `session_artifacts` pointer. */
|
|
48
|
-
export interface PlanDraftOk {
|
|
49
|
-
name: string;
|
|
50
|
-
path: string;
|
|
51
|
-
digest: string;
|
|
52
|
-
bytes: number;
|
|
53
|
-
run_id: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type PlanDraftResult = Result<PlanDraftOk>;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The core both the tool handler and tests call: write the working-plan artifact through the
|
|
60
|
-
* accessor seam (file + `session_artifacts` provenance pointer). Soft result, never throws —
|
|
61
|
-
* failure taxonomy: empty plan → `invalid_input`; no session run_id → `no_run_id`; file-or-pointer
|
|
62
|
-
* write failure → `write_failed` (the seam already warned on stderr).
|
|
63
|
-
*/
|
|
64
|
-
export function writePlanDraft(
|
|
65
|
-
sink: EntrySink,
|
|
66
|
-
ctx: SessionDataCtx & ReportTarget,
|
|
67
|
-
plan: string,
|
|
68
|
-
): PlanDraftResult {
|
|
69
|
-
const fail = failFor(ctx, "plan-draft");
|
|
70
|
-
|
|
71
|
-
if (!plan.trim()) {
|
|
72
|
-
return fail("no plan markdown to write (pass the full working draft)", "invalid_input");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const runId = activeSessionRunId(ctx);
|
|
76
|
-
if (runId === null) {
|
|
77
|
-
return fail("session has no run_id — cannot write the plan-draft artifact", "no_run_id");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const written = writeSessionArtifact(sink, ctx, PLAN_DRAFT_ARTIFACT, plan);
|
|
81
|
-
if (written === null) {
|
|
82
|
-
return fail(
|
|
83
|
-
`could not write the ${PLAN_DRAFT_ARTIFACT} artifact (see warnings)`,
|
|
84
|
-
"write_failed",
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Derive digest/relative path consistently with the pointer the seam recorded.
|
|
89
|
-
const digest = digestSessionData(plan);
|
|
90
|
-
const relPath = relative(ctx.cwd, written);
|
|
91
|
-
return ok(`Plan draft written → ${relPath} (${digest})`, {
|
|
92
|
-
name: PLAN_DRAFT_ARTIFACT,
|
|
93
|
-
path: relPath,
|
|
94
|
-
digest,
|
|
95
|
-
bytes: Buffer.byteLength(plan, "utf8"),
|
|
96
|
-
run_id: runId,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const TOOL_GUIDELINES = [
|
|
101
|
-
"Call plan_draft to persist the current working draft as you author or revise the plan; pass the FULL plan markdown each time (it rewrites the whole draft).",
|
|
102
|
-
"plan_draft never saves to GitHub and never ends the turn — plan_save//plan-save remain the canonical save surface.",
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
/** Register the `plan_draft` tool (the carve-out producer; interior-only). */
|
|
106
|
-
export function registerPlanDraft(pi: ExtensionAPI): void {
|
|
107
|
-
pi.registerTool({
|
|
108
|
-
name: "plan_draft",
|
|
109
|
-
label: "Plan draft",
|
|
110
|
-
description:
|
|
111
|
-
"Write (or overwrite) the working plan draft to the session data dir and record its " +
|
|
112
|
-
"provenance pointer. The only sanctioned write surface while read-only. NOT a save — " +
|
|
113
|
-
"plan_save//plan-save still persist the plan to GitHub.",
|
|
114
|
-
promptSnippet: "Persist the working plan draft to the session data dir (full rewrite)",
|
|
115
|
-
promptGuidelines: TOOL_GUIDELINES,
|
|
116
|
-
executionMode: "sequential",
|
|
117
|
-
parameters: {
|
|
118
|
-
type: "object",
|
|
119
|
-
additionalProperties: false,
|
|
120
|
-
required: ["plan"],
|
|
121
|
-
properties: {
|
|
122
|
-
plan: {
|
|
123
|
-
type: "string",
|
|
124
|
-
description: "The full working-plan markdown (rewrites the whole draft).",
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
129
|
-
const decoded = decodePlanDraftParams(params);
|
|
130
|
-
if (decoded === null) {
|
|
131
|
-
return failFor(
|
|
132
|
-
ctx,
|
|
133
|
-
"plan-draft",
|
|
134
|
-
"plan_draft",
|
|
135
|
-
)("plan_draft needs { plan: string } per the tool schema", "bad_input");
|
|
136
|
-
}
|
|
137
|
-
return writePlanDraft(pi, ctx, decoded.plan);
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
}
|