@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,265 +0,0 @@
|
|
|
1
|
-
// The `gist_draft` file tool: the third member of the draft carve-out family
|
|
2
|
-
// (planDraft.ts, objectiveDraft.ts) — the gist twin, minus the roadmap.
|
|
3
|
-
//
|
|
4
|
-
// Carve-out doctrine: the tool takes NO path/name parameter — the artifact name is the fixed
|
|
5
|
-
// constant `GIST_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-gist 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 — `gist_save`/`/gist-save` still
|
|
11
|
-
// persist the gist to the issue backend.
|
|
12
|
-
//
|
|
13
|
-
// Format doctrine: JSON is the storage/transport format, NEVER the human review surface. The
|
|
14
|
-
// artifact carries `{schema_version, title?, scope?, prose}` — deliberately light: a gist is a
|
|
15
|
-
// problem-space statement of intent with no structured roadmap (contracts.md §8.41). The review
|
|
16
|
-
// surface reads the draft via `readGistDraft` (over `readSessionArtifact` — digest-validated,
|
|
17
|
-
// fail-open) and renders markdown via `renderGistDraft` (title + a `Scope:` line + the prose) —
|
|
18
|
-
// never raw JSON.
|
|
19
|
-
//
|
|
20
|
-
// Vocabulary ownership: this module owns the shared draft/save param vocabulary
|
|
21
|
-
// (`GistSaveParams`, `decodeGistSaveParams`, `GIST_SCOPES`) — gistDraft is the LEAF (mirroring
|
|
22
|
-
// planDraft←planSave's direction); gistSave.ts consumes it, so it may value-import
|
|
23
|
-
// `readGistDraft` cycle-free for the approval→save orchestration.
|
|
24
|
-
//
|
|
25
|
-
// Imports stay node builtins + sibling seams (sessionData.ts, result.ts) so the module loads
|
|
26
|
-
// under `node --test`; no manual `scratch`/`runs` path segments (cacheGuard.test.ts).
|
|
27
|
-
|
|
28
|
-
import { relative } from "node:path";
|
|
29
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
30
|
-
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
31
|
-
import {
|
|
32
|
-
activeSessionRunId,
|
|
33
|
-
digestSessionData,
|
|
34
|
-
readSessionArtifact,
|
|
35
|
-
type SessionDataCtx,
|
|
36
|
-
writeSessionArtifact,
|
|
37
|
-
} from "../substrate/sessionData.ts";
|
|
38
|
-
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
39
|
-
import type { EntrySink } from "../substrate/workflowState.ts";
|
|
40
|
-
import type { ReportTarget } from "../surfaces/report.ts";
|
|
41
|
-
|
|
42
|
-
/** The gist consumption tiers (`scope` — contracts.md §8.41). */
|
|
43
|
-
export const GIST_SCOPES = ["plan", "objective"] as const;
|
|
44
|
-
|
|
45
|
-
export type GistScope = (typeof GIST_SCOPES)[number];
|
|
46
|
-
|
|
47
|
-
/** The decoded `gist_save` tool params (shared with `gist_draft`). */
|
|
48
|
-
export interface GistSaveParams {
|
|
49
|
-
prose: string;
|
|
50
|
-
title?: string;
|
|
51
|
-
scope?: GistScope;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Decode unknown `gist_save` tool-call params (the tool-boundary seam). `prose` absent decodes
|
|
56
|
-
* to `""` (so `saveGist`'s "no gist prose to save" `invalid_input` arm keeps owning that
|
|
57
|
-
* message) but present-but-mistyped → null (strict-fail); a present `scope` outside the enum is
|
|
58
|
-
* likewise a strict-fail (the schema already declares the enum — a bad value means a malformed
|
|
59
|
-
* call, never a silent default).
|
|
60
|
-
*/
|
|
61
|
-
export function decodeGistSaveParams(params: unknown): GistSaveParams | null {
|
|
62
|
-
const p = paramsOf(params);
|
|
63
|
-
if (p === null) return null;
|
|
64
|
-
const prose = stringParam(p, "prose");
|
|
65
|
-
const title = stringParam(p, "title");
|
|
66
|
-
const scope = stringParam(p, "scope");
|
|
67
|
-
if (prose === null || title === null || scope === null) return null;
|
|
68
|
-
if (scope !== undefined && !(GIST_SCOPES as readonly string[]).includes(scope)) return null;
|
|
69
|
-
return { prose: prose ?? "", title, scope: scope as GistScope | undefined };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The fixed working-gist artifact name (one JSON file: the prose + the optional scope hint). */
|
|
73
|
-
export const GIST_DRAFT_ARTIFACT = "gist-draft.json";
|
|
74
|
-
|
|
75
|
-
/** The ok-arm details — provenance-consistent with the recorded `session_artifacts` pointer. */
|
|
76
|
-
export interface GistDraftOk {
|
|
77
|
-
name: string;
|
|
78
|
-
path: string;
|
|
79
|
-
digest: string;
|
|
80
|
-
bytes: number;
|
|
81
|
-
run_id: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type GistDraftResult = Result<GistDraftOk>;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The core both the tool handler and tests call: serialize the working gist (prose + the
|
|
88
|
-
* optional title/scope) as one JSON artifact and write it through the accessor seam (file +
|
|
89
|
-
* `session_artifacts` provenance pointer). Soft result, never throws — failure taxonomy: empty
|
|
90
|
-
* prose → `invalid_input`; no session run_id → `no_run_id`; file-or-pointer write failure →
|
|
91
|
-
* `write_failed` (the seam already warned).
|
|
92
|
-
*/
|
|
93
|
-
export function writeGistDraft(
|
|
94
|
-
sink: EntrySink,
|
|
95
|
-
ctx: SessionDataCtx & ReportTarget,
|
|
96
|
-
opts: { prose: string; title?: string; scope?: GistScope },
|
|
97
|
-
): GistDraftResult {
|
|
98
|
-
const fail = failFor(ctx, "gist-draft");
|
|
99
|
-
|
|
100
|
-
if (!opts.prose.trim()) {
|
|
101
|
-
return fail("no gist prose to write (pass the full working draft)", "invalid_input");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const runId = activeSessionRunId(ctx);
|
|
105
|
-
if (runId === null) {
|
|
106
|
-
return fail("session has no run_id — cannot write the gist-draft artifact", "no_run_id");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Deterministic key order via the explicit literal; `title`/`scope` are omitted when blank.
|
|
110
|
-
const title = opts.title?.trim();
|
|
111
|
-
const payload = {
|
|
112
|
-
schema_version: 1,
|
|
113
|
-
...(title ? { title } : {}),
|
|
114
|
-
...(opts.scope ? { scope: opts.scope } : {}),
|
|
115
|
-
prose: opts.prose,
|
|
116
|
-
};
|
|
117
|
-
const content = `${JSON.stringify(payload, null, 2)}\n`;
|
|
118
|
-
|
|
119
|
-
const written = writeSessionArtifact(sink, ctx, GIST_DRAFT_ARTIFACT, content);
|
|
120
|
-
if (written === null) {
|
|
121
|
-
return fail(
|
|
122
|
-
`could not write the ${GIST_DRAFT_ARTIFACT} artifact (see warnings)`,
|
|
123
|
-
"write_failed",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Derive digest/relative path consistently with the pointer the seam recorded.
|
|
128
|
-
const digest = digestSessionData(content);
|
|
129
|
-
const relPath = relative(ctx.cwd, written);
|
|
130
|
-
return ok(`Gist draft written → ${relPath} (${digest})`, {
|
|
131
|
-
name: GIST_DRAFT_ARTIFACT,
|
|
132
|
-
path: relPath,
|
|
133
|
-
digest,
|
|
134
|
-
bytes: Buffer.byteLength(content, "utf8"),
|
|
135
|
-
run_id: runId,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ------------------------------------------------------------------- the reader + the renderer
|
|
140
|
-
|
|
141
|
-
/** The validated working-gist draft shape consumers receive from `readGistDraft`. */
|
|
142
|
-
export interface GistDraft {
|
|
143
|
-
title?: string;
|
|
144
|
-
scope?: GistScope;
|
|
145
|
-
prose: string;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Read + validate the working-gist draft artifact. Fail-open `null` everywhere (mirroring
|
|
150
|
-
* `readSessionArtifact`'s loud tier): no pointer/file/digest → `null` (the seam already spoke);
|
|
151
|
-
* malformed JSON, a non-object payload, an unsupported `schema_version`, or blank prose → a
|
|
152
|
-
* stderr warning + `null`. `title` is kept only when a non-blank string; `scope` only when a
|
|
153
|
-
* member of the enum (an unknown scope degrades to absent, never poisons the draft). Never
|
|
154
|
-
* throws.
|
|
155
|
-
*/
|
|
156
|
-
export function readGistDraft(ctx: SessionDataCtx): GistDraft | null {
|
|
157
|
-
const artifact = readSessionArtifact(ctx, GIST_DRAFT_ARTIFACT);
|
|
158
|
-
if (artifact === null) return null;
|
|
159
|
-
|
|
160
|
-
const refuse = (why: string): null => {
|
|
161
|
-
console.error(`perk: warning: ${GIST_DRAFT_ARTIFACT} ${why} — refusing the draft`);
|
|
162
|
-
return null;
|
|
163
|
-
};
|
|
164
|
-
let parsed: unknown;
|
|
165
|
-
try {
|
|
166
|
-
parsed = JSON.parse(artifact.content);
|
|
167
|
-
} catch {
|
|
168
|
-
return refuse("is not valid JSON");
|
|
169
|
-
}
|
|
170
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
171
|
-
return refuse("is not a JSON object");
|
|
172
|
-
}
|
|
173
|
-
const payload = parsed as Record<string, unknown>;
|
|
174
|
-
if (payload.schema_version !== 1) {
|
|
175
|
-
return refuse(`has an unsupported schema_version (${JSON.stringify(payload.schema_version)})`);
|
|
176
|
-
}
|
|
177
|
-
const prose = payload.prose;
|
|
178
|
-
if (typeof prose !== "string" || !prose.trim()) {
|
|
179
|
-
return refuse("has no prose");
|
|
180
|
-
}
|
|
181
|
-
const title =
|
|
182
|
-
typeof payload.title === "string" && payload.title.trim() ? payload.title : undefined;
|
|
183
|
-
const scope =
|
|
184
|
-
typeof payload.scope === "string" && (GIST_SCOPES as readonly string[]).includes(payload.scope)
|
|
185
|
-
? (payload.scope as GistScope)
|
|
186
|
-
: undefined;
|
|
187
|
-
return {
|
|
188
|
-
...(title !== undefined ? { title } : {}),
|
|
189
|
-
...(scope !== undefined ? { scope } : {}),
|
|
190
|
-
prose,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Render the draft as the markdown review surface (JSON is storage/transport only — contracts
|
|
196
|
-
* §8.1): the optional `# title` heading, a `Scope:` line when the hint is set, and the prose
|
|
197
|
-
* verbatim. Pure; never throws.
|
|
198
|
-
*/
|
|
199
|
-
export function renderGistDraft(draft: GistDraft): string {
|
|
200
|
-
let out = "";
|
|
201
|
-
if (draft.title) out += `# ${draft.title}\n\n`;
|
|
202
|
-
if (draft.scope) out += `Scope: ${draft.scope}\n\n`;
|
|
203
|
-
return out + draft.prose;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const TOOL_GUIDELINES = [
|
|
207
|
-
"Call gist_draft to persist the current working gist as you author or revise it; pass the FULL prose each time (it rewrites the whole draft).",
|
|
208
|
-
"gist_draft never saves to the issue backend and never ends the turn — gist_save//gist-save remain the canonical save surface.",
|
|
209
|
-
"Pass gist_draft's `scope` only once the consumption tier is settled: `plan` for plan-sized intent, `objective` for objective-sized intent.",
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
/** Register the `gist_draft` tool (the carve-out producer; interior-only). */
|
|
213
|
-
export function registerGistDraft(pi: ExtensionAPI): void {
|
|
214
|
-
pi.registerTool({
|
|
215
|
-
name: "gist_draft",
|
|
216
|
-
label: "Gist draft",
|
|
217
|
-
description:
|
|
218
|
-
"Write (or overwrite) the working gist draft — the statement-of-intent prose + an " +
|
|
219
|
-
"optional scope hint — to the session data dir and record its provenance pointer. The " +
|
|
220
|
-
"only sanctioned write surface while read-only. NOT a save — gist_save//gist-save still " +
|
|
221
|
-
"persist the gist to the issue backend.",
|
|
222
|
-
promptSnippet:
|
|
223
|
-
"Persist the working gist draft (statement-of-intent prose) to the session data dir (full rewrite)",
|
|
224
|
-
promptGuidelines: TOOL_GUIDELINES,
|
|
225
|
-
executionMode: "sequential",
|
|
226
|
-
parameters: {
|
|
227
|
-
type: "object",
|
|
228
|
-
additionalProperties: false,
|
|
229
|
-
required: ["prose"],
|
|
230
|
-
properties: {
|
|
231
|
-
prose: {
|
|
232
|
-
type: "string",
|
|
233
|
-
description:
|
|
234
|
-
"The gist prose (the problem-space intent: what we want, why it matters, what " +
|
|
235
|
-
"bounds it, and any high-level solution leanings — no implementation steps).",
|
|
236
|
-
},
|
|
237
|
-
title: {
|
|
238
|
-
type: "string",
|
|
239
|
-
description: "Optional gist title (defaults to the prose's first heading).",
|
|
240
|
-
},
|
|
241
|
-
scope: {
|
|
242
|
-
type: "string",
|
|
243
|
-
enum: [...GIST_SCOPES],
|
|
244
|
-
description:
|
|
245
|
-
"Optional consumption tier: plan (plan-sized intent) or objective (objective-sized).",
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
250
|
-
// The shared param contract: the same decode as `gist_save`, so the two cannot drift.
|
|
251
|
-
const decoded = decodeGistSaveParams(params);
|
|
252
|
-
if (decoded === null) {
|
|
253
|
-
return failFor(
|
|
254
|
-
ctx,
|
|
255
|
-
"gist-draft",
|
|
256
|
-
"gist_draft",
|
|
257
|
-
)(
|
|
258
|
-
"gist_draft needs { prose: string, scope?: plan|objective } per the tool schema",
|
|
259
|
-
"bad_input",
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
return writeGistDraft(pi, ctx, decoded);
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
// The warm `gist_save` door, the gist mirror of objectiveSave.ts. The in-session twin of the
|
|
2
|
-
// Python cold door (`perk gist create`): a deterministic, terminating tool + command that WRAP
|
|
3
|
-
// the existing storage — they do NOT reimplement the backend write. `saveGist()` delegates to
|
|
4
|
-
// `perk gist create --json` via the shared cold-door client (`runColdDoor`, the prose rides the
|
|
5
|
-
// run-scratch stdin channel). Unlike the plan/objective twins there is NO session linkage after
|
|
6
|
-
// the save — nothing consumes a gist in-session (adoption happens later via the cold doors), so
|
|
7
|
-
// a successful save just relays the envelope's id/url/consumption story.
|
|
8
|
-
//
|
|
9
|
-
// APPROVAL→SAVE ORCHESTRATION (mirroring objectiveSave.ts's `objectiveApprovalSave`). The
|
|
10
|
-
// exported `gistApprovalSave` seam is the shared APPROVED-review → save flow: re-read the
|
|
11
|
-
// artifact (`readGistDraft` — never the rendered markdown, never the transcript) → `saveGist` →
|
|
12
|
-
// D1a gate exit on a successful save (snapshot `gating.isActive()` BEFORE the save; a failed
|
|
13
|
-
// save leaves the gate ON). `plan_review`'s gist arm (planReview.ts) wires its APPROVED outcome
|
|
14
|
-
// into it; the `/gist-save` command is the artifact-first MANUAL FAILSAFE invocation of the same
|
|
15
|
-
// seam, keeping the legacy drive-the-session behavior as the no-draft fallback (gists have no
|
|
16
|
-
// transcript scrape by design).
|
|
17
|
-
|
|
18
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
20
|
-
import {
|
|
21
|
-
booleanField,
|
|
22
|
-
type ColdJson,
|
|
23
|
-
objectField,
|
|
24
|
-
runColdDoor,
|
|
25
|
-
stringField,
|
|
26
|
-
} from "../substrate/coldDoor.ts";
|
|
27
|
-
import { registerPerkCommand } from "../substrate/command.ts";
|
|
28
|
-
import { render } from "../substrate/prompts.ts";
|
|
29
|
-
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
30
|
-
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
31
|
-
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
32
|
-
import { report, type Severity } from "../surfaces/report.ts";
|
|
33
|
-
import { decodeGistSaveParams, GIST_SCOPES, type GistScope, readGistDraft } from "./gistDraft.ts";
|
|
34
|
-
|
|
35
|
-
/** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
|
|
36
|
-
export interface GistSaveOk {
|
|
37
|
-
/** `id` is the opaque string gist id (GitHub "7", Linear "ENG-7"/project id) — §8.21. */
|
|
38
|
-
gist: { id: string; url: string };
|
|
39
|
-
scope: string | null;
|
|
40
|
-
existed: boolean | null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type GistSaveResult = Result<GistSaveOk>;
|
|
44
|
-
|
|
45
|
-
/** The decoded `perk gist create --json` payload slice the warm door consumes. */
|
|
46
|
-
interface GistCreatePayload {
|
|
47
|
-
gist: { id: string; url: string; existed: boolean | undefined };
|
|
48
|
-
scope: string | undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Narrow the `perk gist create --json` success payload; strict on `gist`. */
|
|
52
|
-
function decodeGistCreate(payload: ColdJson): GistCreatePayload | null {
|
|
53
|
-
const gist = objectField(payload, "gist");
|
|
54
|
-
if (gist === undefined) return null;
|
|
55
|
-
const id = stringField(gist, "id");
|
|
56
|
-
const url = stringField(gist, "url");
|
|
57
|
-
if (id === undefined || url === undefined) return null;
|
|
58
|
-
return {
|
|
59
|
-
gist: { id, url, existed: booleanField(gist, "existed") },
|
|
60
|
-
scope: stringField(payload, "scope"),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The consumption pointer relayed after a save — the adoption door matching the saved scope. */
|
|
65
|
-
function consumptionHint(id: string, scope: string | undefined): string {
|
|
66
|
-
return scope === "objective"
|
|
67
|
-
? `Consume with: perk objective author --from ${id}`
|
|
68
|
-
: `Consume with: perk plan from ${id}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The single save implementation both surfaces call. Delegates the backend write to the Python
|
|
73
|
-
* cold door (which owns scope resolution: an explicit `scope` wins, else the launch handoff's
|
|
74
|
-
* pre-seeded `gist_scope`, else `plan`). Returns a soft result (never throws); failures set
|
|
75
|
-
* `details.ok = false`. No session linkage on success — nothing consumes a gist in-session.
|
|
76
|
-
*/
|
|
77
|
-
export async function saveGist(
|
|
78
|
-
pi: ExtensionAPI,
|
|
79
|
-
ctx: ExtensionContext,
|
|
80
|
-
opts: { prose: string; title?: string; scope?: GistScope },
|
|
81
|
-
): Promise<GistSaveResult> {
|
|
82
|
-
const fail = failFor(ctx, "gist-save");
|
|
83
|
-
|
|
84
|
-
const prose = opts.prose.trim();
|
|
85
|
-
if (!prose) return fail("no gist prose to save (draft the gist first)", "invalid_input");
|
|
86
|
-
if (opts.scope !== undefined && !(GIST_SCOPES as readonly string[]).includes(opts.scope)) {
|
|
87
|
-
return fail("scope must be plan or objective", "invalid_input");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const runId = rebuildWorkflowState(branchOf(ctx)).run_id ?? "";
|
|
91
|
-
|
|
92
|
-
const args = ["gist", "create", "--json"];
|
|
93
|
-
if (opts.title) args.push("--title", opts.title);
|
|
94
|
-
if (opts.scope) args.push("--scope", opts.scope);
|
|
95
|
-
if (runId) args.push("--run-id", runId);
|
|
96
|
-
const r = await runColdDoor<GistCreatePayload>(pi, ctx, args, {
|
|
97
|
-
label: "perk gist create",
|
|
98
|
-
decode: decodeGistCreate,
|
|
99
|
-
stdin: { flag: "--body", content: prose, filename: "gist.md" },
|
|
100
|
-
});
|
|
101
|
-
if (!r.ok) return fail(r.message, r.errorType);
|
|
102
|
-
|
|
103
|
-
const gist = r.data.gist;
|
|
104
|
-
const verb = gist.existed ? "Found existing" : "Saved";
|
|
105
|
-
return ok(
|
|
106
|
-
`${verb} gist ${gist.id} → ${gist.url}\n${consumptionHint(gist.id, r.data.scope)}`,
|
|
107
|
-
{
|
|
108
|
-
gist: { id: gist.id, url: gist.url },
|
|
109
|
-
scope: r.data.scope ?? null,
|
|
110
|
-
existed: gist.existed ?? null,
|
|
111
|
-
},
|
|
112
|
-
{ terminate: true },
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** The approval→save orchestration outcome (the gist `ApprovalSaveOutcome`). */
|
|
117
|
-
export type GistApprovalSaveOutcome =
|
|
118
|
-
| { status: "no-draft" }
|
|
119
|
-
| { status: "saved" | "save-failed"; result: GistSaveResult; gateExited: boolean };
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* The shared approval→save orchestration seam (the gist sibling of objectiveSave.ts's
|
|
123
|
-
* `objectiveApprovalSave`): an APPROVED gist review (`plan_review`'s gist arm) and the manual
|
|
124
|
-
* `/gist-save` failsafe both run THIS. Flow: re-read the draft artifact at save time
|
|
125
|
-
* (`readGistDraft` — never the rendered markdown, never in-hand bytes) → `saveGist` → gate exit
|
|
126
|
-
* on a successful save while read-only (the D1a pattern: snapshot `gating.isActive()` before the
|
|
127
|
-
* save; a failed save leaves the gate ON). No draft → `no-draft` (nothing saved, the gate
|
|
128
|
-
* untouched); callers render their own fallback. Title/scope precedence: explicit opts win; else
|
|
129
|
-
* the draft's; else the cold door derives/defaults. The returned result keeps `saveGist`'s
|
|
130
|
-
* `terminate: true` for tool-path callers.
|
|
131
|
-
*/
|
|
132
|
-
export async function gistApprovalSave(
|
|
133
|
-
pi: ExtensionAPI,
|
|
134
|
-
ctx: ExtensionContext,
|
|
135
|
-
gating: ToolGating,
|
|
136
|
-
opts: { title?: string } = {},
|
|
137
|
-
): Promise<GistApprovalSaveOutcome> {
|
|
138
|
-
const draft = readGistDraft(ctx);
|
|
139
|
-
if (draft === null) return { status: "no-draft" };
|
|
140
|
-
// D1a: snapshot the gate BEFORE the save; on success, exit it so save marks the read-only →
|
|
141
|
-
// read-write boundary in one gesture. A failed save leaves the gate on.
|
|
142
|
-
const wasReadOnly = gating.isActive();
|
|
143
|
-
const result = await saveGist(pi, ctx, {
|
|
144
|
-
prose: draft.prose,
|
|
145
|
-
title: opts.title ?? draft.title,
|
|
146
|
-
scope: draft.scope,
|
|
147
|
-
});
|
|
148
|
-
let gateExited = false;
|
|
149
|
-
if (result.details.ok && wasReadOnly) {
|
|
150
|
-
gating.exit(ctx);
|
|
151
|
-
gateExited = true;
|
|
152
|
-
}
|
|
153
|
-
return { status: result.details.ok ? "saved" : "save-failed", result, gateExited };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const TOOL_GUIDELINES = [
|
|
157
|
-
"Use gist_save only after the gist says what it means; it creates the tracked gist in the issue backend and ends the turn.",
|
|
158
|
-
"Pass gist_save the statement-of-intent PROSE in `prose` — problem-focused, with at most high-level solution leanings; no implementation steps or roadmap.",
|
|
159
|
-
"Pass gist_save's `scope` only once the consumption tier is settled (plan or objective); omit it to keep the pre-seeded/default scope.",
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The seed guidance the warm `/gist-save` injects to drive the save (the perk-gist-author skill
|
|
164
|
-
* pointer rides the skill-binding suffix — not hardcoded here). Pure + exported for offline
|
|
165
|
-
* tests.
|
|
166
|
-
*/
|
|
167
|
-
export function gistSaveGuidance(title?: string): string {
|
|
168
|
-
const named = title?.trim() || "";
|
|
169
|
-
return render("stages/gist-save.md", { title: named });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Register the warm door: the `gist_save` tool (canonical) + the `/gist-save` twin. */
|
|
173
|
-
export function registerGistSave(pi: ExtensionAPI, gating: ToolGating): void {
|
|
174
|
-
pi.registerTool({
|
|
175
|
-
name: "gist_save",
|
|
176
|
-
label: "Save gist",
|
|
177
|
-
description:
|
|
178
|
-
"Persist a drafted gist (a statement of intent) to the issue backend as a tracked " +
|
|
179
|
-
"perk:gist. Terminating: ends the turn on save. Call only when the gist says what it " +
|
|
180
|
-
"means.",
|
|
181
|
-
promptSnippet: "Save the converged gist to the issue backend (terminates the turn)",
|
|
182
|
-
promptGuidelines: TOOL_GUIDELINES,
|
|
183
|
-
executionMode: "sequential",
|
|
184
|
-
parameters: {
|
|
185
|
-
type: "object",
|
|
186
|
-
additionalProperties: false,
|
|
187
|
-
required: ["prose"],
|
|
188
|
-
properties: {
|
|
189
|
-
prose: {
|
|
190
|
-
type: "string",
|
|
191
|
-
description:
|
|
192
|
-
"The gist prose (the problem-space intent: what we want, why it matters, what " +
|
|
193
|
-
"bounds it, and any high-level solution leanings — no implementation steps).",
|
|
194
|
-
},
|
|
195
|
-
title: {
|
|
196
|
-
type: "string",
|
|
197
|
-
description: "Optional gist title (defaults to the prose's first heading).",
|
|
198
|
-
},
|
|
199
|
-
scope: {
|
|
200
|
-
type: "string",
|
|
201
|
-
enum: [...GIST_SCOPES],
|
|
202
|
-
description:
|
|
203
|
-
"Optional consumption tier: plan (plan-sized intent) or objective (objective-sized).",
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
208
|
-
const decoded = decodeGistSaveParams(params);
|
|
209
|
-
if (decoded === null) {
|
|
210
|
-
return failFor(
|
|
211
|
-
ctx,
|
|
212
|
-
"gist-save",
|
|
213
|
-
"gist_save",
|
|
214
|
-
)(
|
|
215
|
-
"gist_save needs { prose: string, scope?: plan|objective } per the tool schema",
|
|
216
|
-
"bad_input",
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
return saveGist(pi, ctx, decoded);
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
registerPerkCommand(pi, "gist-save", {
|
|
224
|
-
description:
|
|
225
|
-
"Save the working gist draft to the issue backend — the manual failsafe for the " +
|
|
226
|
-
"approval→save flow (artifact-first; drives the save only when no draft exists).",
|
|
227
|
-
handler: async (args, ctx) => {
|
|
228
|
-
const title = args.trim() || undefined;
|
|
229
|
-
// The artifact-first manual-failsafe invocation of the shared approval→save seam (the D1a
|
|
230
|
-
// gate exit lives in the seam). The legacy drive-the-session behavior is kept as the
|
|
231
|
-
// NO-DRAFT fallback — gists have no transcript scrape by design, so a draftless session
|
|
232
|
-
// still needs a working save path.
|
|
233
|
-
const outcome = await gistApprovalSave(pi, ctx, gating, { title });
|
|
234
|
-
if (outcome.status === "no-draft") {
|
|
235
|
-
// Exit the read-only gate so the gist_save tool (excluded from READ_ONLY_TOOLS) becomes
|
|
236
|
-
// reachable on the driven turn, then drive the turn (mirrors /objective-save).
|
|
237
|
-
if (gating.isActive()) gating.exit(ctx);
|
|
238
|
-
report(ctx, "gist-save", "info", "handing the save to the session");
|
|
239
|
-
// The perk-gist-author pointer rides the skill-binding suffix (D5) since a warm
|
|
240
|
-
// /gist-save outside a stage:gist-author session gets none from Mechanism A.
|
|
241
|
-
pi.sendUserMessage(gistSaveGuidance(title) + bindingSuffix(ctx.cwd, "stage:gist-author"));
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
// Saved or save-failed: relay the save message (which carries the consumption hint).
|
|
245
|
-
const result = outcome.result;
|
|
246
|
-
const message = result.content[0]?.text ?? "gist-save done";
|
|
247
|
-
const severity: Severity = result.details.ok ? "info" : "error";
|
|
248
|
-
report(ctx, "gist-save", severity, message);
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// The sanctioned "implement here" exit from plan authoring (contracts.md §8.23): the read-only
|
|
2
|
-
// gate comes off WITHOUT saving to the issue backend, and the model is instructed to implement
|
|
3
|
-
// the reviewed plan draft directly in the current session/checkout — edits only, git gestures
|
|
4
|
-
// stay with the human. Human-only by construction: the two surfaces are the first-party review's
|
|
5
|
-
// 4th verdict (planReview.ts) and the `/implement-here` command registered here — no model tool
|
|
6
|
-
// exists, so the model can never choose to skip the backend on its own (the /btw posture).
|
|
7
|
-
//
|
|
8
|
-
// The seam, the guidance builder, and the command live in ONE module with no cycles:
|
|
9
|
-
// planReview.ts imports from here, never the reverse. `implementHereExit` is the
|
|
10
|
-
// gate-exit-WITHOUT-save sibling of `approvalSave`'s D1a arm — the review door composes the gate
|
|
11
|
-
// through this seam, never owns it (Invariant 1).
|
|
12
|
-
//
|
|
13
|
-
// Deliberately OUTSIDE the PR lifecycle: no issue, no plan-ref, no branch — /submit, /address,
|
|
14
|
-
// and /land all key off `cache.plan-ref` and stay inapplicable. The plan-draft artifact is left
|
|
15
|
-
// untouched, so /plan-save can still create the canonical issue afterwards.
|
|
16
|
-
|
|
17
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
19
|
-
import { registerPerkCommand } from "../substrate/command.ts";
|
|
20
|
-
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
21
|
-
import { report } from "../surfaces/report.ts";
|
|
22
|
-
import { readNodeClaim } from "./objectivePlan.ts";
|
|
23
|
-
|
|
24
|
-
/** The core instruction text (exported for the offline content pins). */
|
|
25
|
-
export const IMPLEMENT_HERE_GUIDANCE = `The human chose IMPLEMENT HERE: implement the reviewed plan directly in this session — no plan issue was created and none will be.
|
|
26
|
-
|
|
27
|
-
- The read-only gate is off: make the plan's edits now, in this checkout.
|
|
28
|
-
- Run the checks the plan calls for before declaring done.
|
|
29
|
-
- Do NOT commit, branch, or push unless the user explicitly asks — git gestures stay with the human.
|
|
30
|
-
- perk's lifecycle doors (/submit, /land, /learn) do not apply — there is no plan issue or plan-ref.
|
|
31
|
-
- The plan draft artifact is untouched: /plan-save can still create the canonical issue later.`;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Build the implement-here instruction text. When `editedPlan` is set (review-path human edits
|
|
35
|
-
* were written back to the draft pre-verdict), the final reviewed bytes are inlined so the model
|
|
36
|
-
* implements THOSE, not its stale in-context version. Always appends the Mechanism-B skill-binding
|
|
37
|
-
* suffix for `command:implement-here` (a `[[bindings]]` hook; delivers nothing by default).
|
|
38
|
-
*/
|
|
39
|
-
export function implementHereGuidance(cwd: string, opts: { editedPlan?: string }): string {
|
|
40
|
-
const edited =
|
|
41
|
-
opts.editedPlan === undefined
|
|
42
|
-
? ""
|
|
43
|
-
: `\n\nThe human edited the plan during review; implement THESE final bytes:\n\n${opts.editedPlan}`;
|
|
44
|
-
return `${IMPLEMENT_HERE_GUIDANCE}${edited}${bindingSuffix(cwd, "command:implement-here")}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The gate-exit-WITHOUT-save seam — the no-save sibling of `approvalSave`'s D1a arm. If the gate
|
|
49
|
-
* is active, exit it and report one info line; otherwise a no-op. Keeps Invariant 1: callers
|
|
50
|
-
* (the review door's implement-here verdict, the `/implement-here` command) compose the gate
|
|
51
|
-
* through this seam, never own it.
|
|
52
|
-
*/
|
|
53
|
-
export function implementHereExit(
|
|
54
|
-
ctx: ExtensionContext,
|
|
55
|
-
gating: ToolGating,
|
|
56
|
-
): { gateExited: boolean } {
|
|
57
|
-
if (!gating.isActive()) return { gateExited: false };
|
|
58
|
-
gating.exit(ctx);
|
|
59
|
-
report(
|
|
60
|
-
ctx,
|
|
61
|
-
"implement-here",
|
|
62
|
-
"info",
|
|
63
|
-
"plan mode off — implementing here; no issue saved (draft intact; /plan-save can still create it)",
|
|
64
|
-
);
|
|
65
|
-
return { gateExited: true };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Register `/implement-here` — the universal manual gesture for the no-save exit (and the ONLY
|
|
70
|
-
* reachable surface when the plannotator review is selected, whose browser envelope returns only
|
|
71
|
-
* approve/deny). Handler order: the objective-node carve-out (a node-linked plan must save — the
|
|
72
|
-
* node advance and backlink depend on it) → the gate-off warning (the command's meaning is
|
|
73
|
-
* exiting plan mode without saving) → gate exit + guidance injection (the submit.ts routing:
|
|
74
|
-
* idle → an immediate turn; streaming → a followUp).
|
|
75
|
-
*/
|
|
76
|
-
export function registerImplementHere(pi: ExtensionAPI, gating: ToolGating): void {
|
|
77
|
-
registerPerkCommand(pi, "implement-here", {
|
|
78
|
-
description:
|
|
79
|
-
"Exit plan mode WITHOUT saving an issue and implement the current plan draft in this " +
|
|
80
|
-
"session (the human-owned lightweight path).",
|
|
81
|
-
handler: async (_args, ctx) => {
|
|
82
|
-
// 1. Objective-node planning sessions must save: an implement-here would strand the node in
|
|
83
|
-
// `planning` (the claim is only cleared by a node-linked save or a non-planning
|
|
84
|
-
// transition). Gate untouched, nothing injected.
|
|
85
|
-
if (readNodeClaim(ctx) !== null) {
|
|
86
|
-
report(
|
|
87
|
-
ctx,
|
|
88
|
-
"implement-here",
|
|
89
|
-
"warning",
|
|
90
|
-
"this is an objective-node planning session — a node-linked plan must be saved " +
|
|
91
|
-
"(the node advance and backlink depend on it). Use plan_review / /plan-save instead.",
|
|
92
|
-
);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
// 2. Nothing to exit: the command's meaning is *exiting plan mode without saving*.
|
|
96
|
-
if (!gating.isActive()) {
|
|
97
|
-
report(
|
|
98
|
-
ctx,
|
|
99
|
-
"implement-here",
|
|
100
|
-
"warning",
|
|
101
|
-
"not in plan mode — nothing to exit; just ask the model to implement.",
|
|
102
|
-
);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// 3. Gate off → instruct the model. No inlined plan: the model authored the draft in its
|
|
106
|
-
// own context (the review-path edited-bytes inlining lives in planReview.ts).
|
|
107
|
-
implementHereExit(ctx, gating);
|
|
108
|
-
const message = implementHereGuidance(ctx.cwd, {});
|
|
109
|
-
if (ctx.isIdle()) {
|
|
110
|
-
pi.sendUserMessage(message);
|
|
111
|
-
} else {
|
|
112
|
-
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
}
|