@mgiles/perk 2.3.0 → 3.1.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 +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +135 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +412 -0
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +537 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +475 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +180 -63
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +57 -30
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +397 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +153 -30
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +90 -2
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +304 -46
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +169 -57
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +166 -23
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +41 -12
- package/extension/substrate/git.ts +95 -2
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +16 -7
- package/extension/substrate/sessionData.ts +8 -6
- package/extension/substrate/sessionPointers.ts +4 -5
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +179 -23
- package/extension/substrate/workflowState.ts +50 -2
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +213 -275
- package/extension/vendor/btw/btw.ts +94 -17
- package/extension/waves/adversarialReviewWave.ts +181 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +205 -0
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -0
- package/extension/waves/harvestWave.ts +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +383 -72
- package/extension/waves/prReviewWave.ts +175 -25
- package/extension/waves/reportWave.ts +503 -107
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +4 -5
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +18 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +115 -65
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -3
- package/prompts/contexts/adapters/plannotator-plan.md +8 -3
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +6 -6
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +5373 -281
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +42 -18
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
package/extension/index.ts
CHANGED
|
@@ -4,26 +4,36 @@
|
|
|
4
4
|
// `session_start` (verified-linkage), rebuild `perk:workflow-state` on `session_start` AND
|
|
5
5
|
// `session_tree` (per-field LWW), and derive a child run_id on fork.
|
|
6
6
|
|
|
7
|
-
import { existsSync, mkdirSync
|
|
7
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { basename, join } from "node:path";
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { registerPlanAdapterPlannotator } from "./adapters/planAdapterPlannotator.ts";
|
|
11
11
|
import { registerPlanAdapterTombell } from "./adapters/planAdapterTombell.ts";
|
|
12
|
-
import { registerTodoAdapterJuicesharp } from "./adapters/todoAdapterJuicesharp.ts";
|
|
13
|
-
import { registerCheckpoints } from "./checkpoints/checkpoints.ts";
|
|
14
12
|
import { registerAddress } from "./doors/address.ts";
|
|
15
|
-
import {
|
|
13
|
+
import { registerAnnotationPushTool } from "./doors/annotationPush.ts";
|
|
14
|
+
import { registerAuditWave } from "./doors/auditWaveTools.ts";
|
|
16
15
|
import { registerCiExecutor } from "./doors/ciExecutor.ts";
|
|
17
16
|
import { registerCommitAndCompact } from "./doors/commitCompact.ts";
|
|
17
|
+
import { registerDraftReviewWaveTools } from "./doors/draftReviewWaveTools.ts";
|
|
18
|
+
import { registerDreamWave } from "./doors/dreamWaveTools.ts";
|
|
19
|
+
import { registerHarvestWave } from "./doors/harvestWaveTools.ts";
|
|
18
20
|
import { registerLand } from "./doors/land.ts";
|
|
19
21
|
import { registerLearn } from "./doors/learn.ts";
|
|
20
22
|
import { CODE_DOOR, DOCS_DOOR, registerLearnFactoryDoor } from "./doors/learnFactory.ts";
|
|
21
23
|
import { registerLifecycleGates } from "./doors/lifecycleGates.ts";
|
|
24
|
+
import {
|
|
25
|
+
openObjectiveReviewSurface,
|
|
26
|
+
registerObjectiveReviewBrowser,
|
|
27
|
+
} from "./doors/objectiveReviewBrowser.ts";
|
|
28
|
+
import { registerObjectiveStack } from "./doors/objectiveStack.ts";
|
|
29
|
+
import { plannotatorPresent } from "./doors/plannotatorHandoff.ts";
|
|
30
|
+
import { openPlanReviewSurface, registerPlanReviewBrowser } from "./doors/planReviewBrowser.ts";
|
|
22
31
|
import { registerPrReview } from "./doors/prReview.ts";
|
|
23
32
|
import { registerPrReviewBrowser } from "./doors/prReviewBrowser.ts";
|
|
24
33
|
import { registerPrReviewDynamic } from "./doors/prReviewDynamic.ts";
|
|
25
34
|
import { registerPrReviewTerminal } from "./doors/prReviewTerminal.ts";
|
|
26
35
|
import { registerReady } from "./doors/ready.ts";
|
|
36
|
+
import { registerReviewWaveTools } from "./doors/reviewWaveTools.ts";
|
|
27
37
|
import { registerSelfcheck } from "./doors/selfcheck.ts";
|
|
28
38
|
import { registerSubmit } from "./doors/submit.ts";
|
|
29
39
|
import { registerSubmitPrReview } from "./doors/submitPrReview.ts";
|
|
@@ -40,8 +50,11 @@ import { registerPlanDraft } from "./factories/planDraft.ts";
|
|
|
40
50
|
import { registerPlanMode } from "./factories/planMode.ts";
|
|
41
51
|
import { registerPlanReview } from "./factories/planReview.ts";
|
|
42
52
|
import { registerPlanSave } from "./factories/planSave.ts";
|
|
53
|
+
import { createHunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
|
|
54
|
+
import { createAgentScratchProvisioner, registerAgentScratch } from "./substrate/agentScratch.ts";
|
|
43
55
|
import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
|
|
44
56
|
import {
|
|
57
|
+
atomicWriteFileSync,
|
|
45
58
|
ensureRunScratch,
|
|
46
59
|
markHandoffConsumed,
|
|
47
60
|
readHandoff,
|
|
@@ -50,7 +63,7 @@ import {
|
|
|
50
63
|
workflowDir,
|
|
51
64
|
} from "./substrate/cache.ts";
|
|
52
65
|
import { loadRegistry, type Registry, stageConsumesPlanRef } from "./substrate/registry.ts";
|
|
53
|
-
import { perkVersion, sharedDir } from "./substrate/resources.ts";
|
|
66
|
+
import { perkVersion, sharedDir, versionStamp } from "./substrate/resources.ts";
|
|
54
67
|
import { mintRunId } from "./substrate/runId.ts";
|
|
55
68
|
import { captureSessionPointer } from "./substrate/sessionPointers.ts";
|
|
56
69
|
import { registerToolGating } from "./substrate/toolGating.ts";
|
|
@@ -70,7 +83,9 @@ import {
|
|
|
70
83
|
createPerkStatus,
|
|
71
84
|
installPerkFooter,
|
|
72
85
|
latestCacheHitRate,
|
|
86
|
+
REPORT_DETAIL_TYPE,
|
|
73
87
|
registerTranscriptRenderer,
|
|
88
|
+
reportDetailEntryRenderer,
|
|
74
89
|
workflowStateEntryRenderer,
|
|
75
90
|
} from "./surfaces/surfaces.ts";
|
|
76
91
|
import { registerBtw } from "./vendor/btw/btw.ts";
|
|
@@ -88,7 +103,7 @@ function writeT3Sentinel(
|
|
|
88
103
|
try {
|
|
89
104
|
const dir = workflowDir(cwd);
|
|
90
105
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
91
|
-
|
|
106
|
+
atomicWriteFileSync(
|
|
92
107
|
join(dir, ".perk-t3.json"),
|
|
93
108
|
`${JSON.stringify({
|
|
94
109
|
source,
|
|
@@ -102,7 +117,6 @@ function writeT3Sentinel(
|
|
|
102
117
|
pi_session_id: state.pi_session_id ?? null,
|
|
103
118
|
active_plan_ref: state.active_plan_ref ?? null,
|
|
104
119
|
})}\n`,
|
|
105
|
-
"utf8",
|
|
106
120
|
);
|
|
107
121
|
} catch {
|
|
108
122
|
// never throw from a probe
|
|
@@ -116,12 +130,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
116
130
|
// both session_start AND session_tree below. enter/exit are the surface the gated stages consume.
|
|
117
131
|
const gating = registerToolGating(pi);
|
|
118
132
|
|
|
133
|
+
// Run-owned disposable scratch guidance for every eligible write-capable model turn. One
|
|
134
|
+
// activation-scoped provisioner shares retry/warning suppression with the isolated /btw side
|
|
135
|
+
// session; no model tool or process-global temp environment is introduced.
|
|
136
|
+
const agentScratch = createAgentScratchProvisioner();
|
|
137
|
+
registerAgentScratch(pi, agentScratch);
|
|
138
|
+
|
|
119
139
|
// Vendored `btw`: a `/btw` human-only side-chat popover backed by an isolated in-memory
|
|
120
140
|
// AgentSession. Takes `gating` for the gate-mirror — its side-session toolset + cache key follow
|
|
121
141
|
// perk's read-only gate (`sideSessionTools`), so the isolated session never bypasses the read-only
|
|
122
142
|
// guarantee. Its `ctx.ui.custom` overlay is the ONE sanctioned charter exception (§6 D6): human-
|
|
123
143
|
// invoked only, `hasUI`-gated, no model tool, not a stage/door — never machine-reachable.
|
|
124
|
-
registerBtw(pi, gating);
|
|
144
|
+
registerBtw(pi, gating, agentScratch);
|
|
125
145
|
|
|
126
146
|
// Vendored `whimsical`: flavors pi's default working-message label with a random phrase per
|
|
127
147
|
// turn, via the headless-no-op `setWorkingMessage` surfaces seam. Always on, no config toggle.
|
|
@@ -150,8 +170,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
150
170
|
// `plan_review`, perk's UNIVERSAL review door: plannotator-selected → the event-bus
|
|
151
171
|
// bridge; ANY other selection → the first-party in-TUI editor review. It takes `gating` only to
|
|
152
172
|
// COMPOSE the approvalSave seam on an APPROVED review (auto-save → D1a gate exit) — Invariant 1
|
|
153
|
-
// holds: the door composes the gate through the seam, never owns it.
|
|
154
|
-
|
|
173
|
+
// holds: the door composes the gate through the seam, never owns it. The injected wave-launch
|
|
174
|
+
// deps power the plannotator launch chooser (§8.23): the presence probe + the two door open
|
|
175
|
+
// cores are composed HERE so planReview.ts imports nothing from door modules (the value-import
|
|
176
|
+
// cycle break — planReviewBrowser.ts already value-imports planReview.ts).
|
|
177
|
+
registerPlanReview(pi, gating, {
|
|
178
|
+
present: () => plannotatorPresent(pi),
|
|
179
|
+
plan: (ctx, opts) => openPlanReviewSurface(pi, ctx, gating, opts),
|
|
180
|
+
objective: (ctx, opts) => openObjectiveReviewSurface(pi, ctx, gating, opts),
|
|
181
|
+
});
|
|
155
182
|
|
|
156
183
|
// Objective-author context injection (the objective mirror of plan mode's authoring
|
|
157
184
|
// half). Keyed off (read-only gate AND stage === objective-author); planMode defers to it.
|
|
@@ -179,27 +206,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
206
|
}
|
|
180
207
|
const registryOk = registryStages > 0;
|
|
181
208
|
|
|
182
|
-
// The
|
|
183
|
-
//
|
|
184
|
-
//
|
|
209
|
+
// The single-value `perk` status handle (charter D2): one slot carrying the objective
|
|
210
|
+
// segment. Created once here (no hidden module state) and threaded into the objective
|
|
211
|
+
// publisher below; the footer reads it back via get/subscribe.
|
|
185
212
|
const perkStatus = createPerkStatus();
|
|
186
213
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Transcript marker for `perk:workflow-state` deltas (audit §2.3): the renderer body lives in
|
|
193
|
-
// surfaces.ts, this registration is wiring, and the seam carries the typeof feature-detect
|
|
194
|
-
// (pre-0.80.4 hosts stay inert). One registration covers every workflow-state appender.
|
|
214
|
+
// The generic full report-detail entry and the `perk:workflow-state` transition marker. Renderer
|
|
215
|
+
// bodies live in surfaces.ts; registration is wiring through the pre-0.80.4-safe seam. The report
|
|
216
|
+
// family is appended by command-attached sinks; one workflow registration covers every appender.
|
|
217
|
+
registerTranscriptRenderer(pi, REPORT_DETAIL_TYPE, reportDetailEntryRenderer);
|
|
195
218
|
registerTranscriptRenderer(pi, WORKFLOW_STATE_TYPE, workflowStateEntryRenderer);
|
|
196
219
|
|
|
220
|
+
// The hunk watch feedback receiver controller (contracts §8.58) — factory-scoped (no module
|
|
221
|
+
// globals). Synced from session_start/session_tree below; closed on session_shutdown so the
|
|
222
|
+
// consumer lease releases with the session. A stale /reload predecessor instance is retired
|
|
223
|
+
// by the lease fencing (fresh token per same-identity reacquire + verify-before-inject).
|
|
224
|
+
const feedbackReceiver = createHunkFeedbackReceiver(pi);
|
|
225
|
+
pi.on("session_shutdown", async () => {
|
|
226
|
+
feedbackReceiver.close();
|
|
227
|
+
});
|
|
228
|
+
|
|
197
229
|
pi.on("session_start", async (_event, ctx) => {
|
|
198
230
|
const branchEntries = () => branchOf(ctx);
|
|
199
231
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
200
232
|
const currentSessionId = sessionFile ? basename(sessionFile) : null;
|
|
201
233
|
|
|
202
|
-
//
|
|
234
|
+
// Terminal-safe linkage failure: managed headline when headful, complete stderr when headless
|
|
235
|
+
// (plus the explicit RPC mirror); non-fatal and leaves the run unclaimed.
|
|
203
236
|
const reportError = (message: string) => {
|
|
204
237
|
report(ctx, "workflow-state linkage error", "error", message, { alsoLog: true });
|
|
205
238
|
};
|
|
@@ -211,6 +244,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
211
244
|
cwd: ctx.cwd,
|
|
212
245
|
});
|
|
213
246
|
|
|
247
|
+
// The session-audit exact-vintage stamp (§8.3), recorded by every run-identity arm below
|
|
248
|
+
// (claim/fork/adopt/mint); undefined on the perkVersion() failure sentinel, which drops the
|
|
249
|
+
// key on serialize and leaves the session on the timestamp-estimate arm.
|
|
250
|
+
const stamp = versionStamp(version);
|
|
251
|
+
|
|
214
252
|
// `claim`/`adopt` carry no prior branch state (adopt's is written by its arm below).
|
|
215
253
|
let resolved: WorkflowState =
|
|
216
254
|
decision.action === "claim" || decision.action === "adopt" ? {} : decision.state;
|
|
@@ -226,6 +264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
264
|
run_id: decision.runId,
|
|
227
265
|
pi_session_id: currentSessionId ?? undefined,
|
|
228
266
|
mode: handoff.mode,
|
|
267
|
+
perk_version: stamp,
|
|
229
268
|
// Record the launched stage so the interior can tell e.g. objective-author from plan
|
|
230
269
|
// (both are read-only) and inject the right authoring context (planMode vs objectiveAuthor).
|
|
231
270
|
stage: handoff.stage,
|
|
@@ -247,13 +286,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
247
286
|
}
|
|
248
287
|
}
|
|
249
288
|
} else if (decision.action === "fork") {
|
|
250
|
-
// Inherited a run_id from a different session file → isolate the child's scratch.
|
|
251
|
-
|
|
289
|
+
// Inherited a run_id from a different session file → isolate the child's scratch. A static
|
|
290
|
+
// redirect or filesystem failure is loud but does not prevent the derived workflow identity
|
|
291
|
+
// from settling; later eligible turns retry through the agent-scratch resolver.
|
|
292
|
+
try {
|
|
293
|
+
ensureRunScratch(ctx.cwd, decision.childRunId);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
report(
|
|
296
|
+
ctx,
|
|
297
|
+
"run scratch",
|
|
298
|
+
"warning",
|
|
299
|
+
`could not create fork run root for ${decision.childRunId}: ${String(error)}`,
|
|
300
|
+
{ alsoLog: true },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
252
303
|
const data: WorkflowState = {
|
|
253
304
|
run_id: decision.childRunId,
|
|
254
305
|
pi_session_id: currentSessionId ?? undefined,
|
|
255
306
|
predecessor: decision.parentRunId,
|
|
256
307
|
mode: decision.state.mode,
|
|
308
|
+
perk_version: stamp,
|
|
257
309
|
};
|
|
258
310
|
pi.appendEntry(WORKFLOW_STATE_TYPE, data);
|
|
259
311
|
resolved = data;
|
|
@@ -264,12 +316,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
264
316
|
// the launched session: never re-consume the handoff (its pi_session_id keeps the true
|
|
265
317
|
// claimer), no `stage` (no stage impersonation / stage-binding injection), and no
|
|
266
318
|
// implementation/main pointer capture (resolveRunStage stays null for adopt).
|
|
267
|
-
|
|
319
|
+
try {
|
|
320
|
+
ensureRunScratch(ctx.cwd, decision.childRunId);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
report(
|
|
323
|
+
ctx,
|
|
324
|
+
"run scratch",
|
|
325
|
+
"warning",
|
|
326
|
+
`could not create adopted run root for ${decision.childRunId}: ${String(error)}`,
|
|
327
|
+
{ alsoLog: true },
|
|
328
|
+
);
|
|
329
|
+
}
|
|
268
330
|
const data: WorkflowState = {
|
|
269
331
|
run_id: decision.childRunId,
|
|
270
332
|
pi_session_id: currentSessionId ?? undefined,
|
|
271
333
|
predecessor: decision.parentRunId,
|
|
272
334
|
mode: decision.mode,
|
|
335
|
+
perk_version: stamp,
|
|
273
336
|
};
|
|
274
337
|
pi.appendEntry(WORKFLOW_STATE_TYPE, data);
|
|
275
338
|
resolved = data;
|
|
@@ -279,7 +342,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
279
342
|
// dirs are the accessor's job; provenance is recorded separately. A failed cold claim above never
|
|
280
343
|
// falls here (claim stays a loud unclaimed error).
|
|
281
344
|
const runId = mintRunId();
|
|
282
|
-
const data: WorkflowState = {
|
|
345
|
+
const data: WorkflowState = {
|
|
346
|
+
run_id: runId,
|
|
347
|
+
pi_session_id: currentSessionId ?? undefined,
|
|
348
|
+
perk_version: stamp,
|
|
349
|
+
};
|
|
283
350
|
const okAppend = appendWorkflowState(pi, ctx, {
|
|
284
351
|
data,
|
|
285
352
|
field: "run_id",
|
|
@@ -377,6 +444,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
377
444
|
});
|
|
378
445
|
}
|
|
379
446
|
|
|
447
|
+
// The hunk watch feedback receiver (§8.58): sync strictly AFTER the run-identity claim and
|
|
448
|
+
// the plan-ref reconciliation above, so an unclaimed or mislinked session never touches the
|
|
449
|
+
// outbox. Eligibility (interactive TUI + implement stage + non-adopted + settled identity +
|
|
450
|
+
// plan-ref match against one fresh cache read) is evaluated inside sync; every ineligible
|
|
451
|
+
// shape closes any open inbox. Never throws (the controller contains its own failures).
|
|
452
|
+
feedbackReceiver.sync(ctx, {
|
|
453
|
+
stage: implStage,
|
|
454
|
+
adopted: decision.action === "adopt",
|
|
455
|
+
runId: resolved.run_id ?? null,
|
|
456
|
+
piSessionId: currentSessionId,
|
|
457
|
+
activePlanRef: resolved.active_plan_ref ?? null,
|
|
458
|
+
mode: ctx.mode ?? null,
|
|
459
|
+
});
|
|
460
|
+
|
|
380
461
|
// Soft version-parity drift signal: pi can lazy-install / load a stale `npm:@mgiles/perk`, so the
|
|
381
462
|
// extension actually running may differ from the `perk` CLI that launched it. The local launch
|
|
382
463
|
// seam injects PERK_CLI_VERSION; compare it against this extension's own `perkVersion()`. Soft +
|
|
@@ -397,11 +478,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
478
|
// Charter D7: perk identity is standing footer state, not a transition — the
|
|
398
479
|
// `v<version> loaded` toast (and its headless stderr mirror) is retired. D5 is rescinded:
|
|
399
480
|
// perk keeps pi's default working indicator (no setWorkingIndicator call anywhere).
|
|
481
|
+
// Install on EVERY headful session_start: pi ≥ 0.84's `setExtensionFooter` explicitly
|
|
482
|
+
// disposes a replaced footer factory (verified at 0.84.1), and `resetExtensionUI` restores
|
|
483
|
+
// the built-in footer on /reload and before session replacement — both paths also re-run
|
|
484
|
+
// this extension factory, so repeated installs leak nothing and each install's deps
|
|
485
|
+
// closures capture the current event's ctx.
|
|
400
486
|
// Footer-seam install-site vacating: under a foreign `[providers] footer` selection perk does
|
|
401
487
|
// NOT install its own footer, leaving the foreign footer (`pi-powerline-footer` / `pi-bar`) as
|
|
402
|
-
// the sole footer surface. perk's objective
|
|
403
|
-
//
|
|
404
|
-
if (ctx.hasUI &&
|
|
488
|
+
// the sole footer surface. perk's objective progress still reaches it via the
|
|
489
|
+
// single-value `perk` setStatus slot. Fail-safe: any config-read error resolves to install.
|
|
490
|
+
if (ctx.hasUI && isPerkFooterReferenceSelected(ctx.cwd)) {
|
|
405
491
|
installPerkFooter(ctx, {
|
|
406
492
|
identity: `perk v${version}`,
|
|
407
493
|
status: perkStatus,
|
|
@@ -413,7 +499,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
499
|
return usage ? { percent: usage.percent, contextWindow: usage.contextWindow } : null;
|
|
414
500
|
},
|
|
415
501
|
});
|
|
416
|
-
footerInstalled = true;
|
|
417
502
|
}
|
|
418
503
|
|
|
419
504
|
if (process.env.PERK_SELFCHECK) {
|
|
@@ -421,7 +506,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
506
|
const dir = workflowDir(ctx.cwd);
|
|
422
507
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
423
508
|
// The gate sentinel (unchanged — those gates parse this line).
|
|
424
|
-
|
|
509
|
+
atomicWriteFileSync(
|
|
425
510
|
join(dir, ".perk-loaded"),
|
|
426
511
|
`perk ${version} loaded; shared=${sharedOk ? "ok" : "miss"}; ` +
|
|
427
512
|
`registry=${registryOk ? "ok" : "miss"} stages=${registryStages}; hasUI=${ctx.hasUI}\n`,
|
|
@@ -444,6 +529,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
444
529
|
} catch (error) {
|
|
445
530
|
console.error(`perk: tool-gating sync failed on session_tree — ${error}`);
|
|
446
531
|
}
|
|
532
|
+
// Re-sync the feedback receiver from the LWW-rebuilt state (§8.58). `adopted: false` is
|
|
533
|
+
// right here: an env-adopted child's fresh branch carries no stage, so the stage gate
|
|
534
|
+
// alone keeps it inert on tree navigation.
|
|
535
|
+
feedbackReceiver.sync(ctx, {
|
|
536
|
+
stage: state.stage ?? null,
|
|
537
|
+
adopted: false,
|
|
538
|
+
runId: state.run_id ?? null,
|
|
539
|
+
piSessionId: state.pi_session_id ?? null,
|
|
540
|
+
activePlanRef: state.active_plan_ref ?? null,
|
|
541
|
+
mode: ctx.mode ?? null,
|
|
542
|
+
});
|
|
447
543
|
if (process.env.PERK_SELFCHECK) {
|
|
448
544
|
writeT3Sentinel(ctx.cwd, "tree", state, ctx.mode ?? null);
|
|
449
545
|
}
|
|
@@ -469,11 +565,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
469
565
|
// The `gist_draft` working-gist file tool (the third draft carve-out).
|
|
470
566
|
registerGistDraft(pi);
|
|
471
567
|
|
|
472
|
-
// The universal `ask_user_question` tool: lets a model interactively ask the human a
|
|
473
|
-
// clarifying question (free-text or multiple-choice). Registered in the factory so it exists
|
|
474
|
-
// before the gate snapshots tools; its name is in READ_ONLY_TOOLS so it survives plan mode.
|
|
475
|
-
registerAskUser(pi);
|
|
476
|
-
|
|
477
568
|
// Lifecycle gates: the dirty-repo switch/fork guard + the guard-only `/implement`.
|
|
478
569
|
registerLifecycleGates(pi);
|
|
479
570
|
|
|
@@ -487,9 +578,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
487
578
|
registerLand(pi);
|
|
488
579
|
registerLearn(pi);
|
|
489
580
|
|
|
490
|
-
// The warm
|
|
581
|
+
// The warm stacked-delivery surface (§8.51): `/objective-stack` (read) +
|
|
582
|
+
// `/objective-sync`/`/objective-recover` (drives) + the four typed stack tools. Takes
|
|
583
|
+
// `gating` for the driving commands' gate-on soft refusal (stack sync/recovery mutates
|
|
584
|
+
// published branches; the stack tools never join READ_ONLY_TOOLS).
|
|
585
|
+
registerObjectiveStack(pi, gating);
|
|
586
|
+
|
|
587
|
+
// The warm `/address` review loop: the submit-then-resolve `finalize_address` tool + `/address`
|
|
491
588
|
// command. Classify-then-act (the verbose feedback fetch + classification runs in an isolated
|
|
492
|
-
// spawned child; the parent fixes actionable items and
|
|
589
|
+
// spawned child; the parent fixes actionable items and finalizes the committed repairs).
|
|
493
590
|
registerAddress(pi);
|
|
494
591
|
|
|
495
592
|
// The warm `/pr-review` door: automated code review in a FRESH, isolated subagent that
|
|
@@ -506,6 +603,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
506
603
|
// doors ride (contracts §8.4) — neither door registers tools of its own.
|
|
507
604
|
registerSubmitPrReview(pi);
|
|
508
605
|
|
|
606
|
+
// The flow-scoped review-wave pair (`start_review_wave`/`collect_review_wave`) both human
|
|
607
|
+
// review doors drive: non-blocking adversarial-review launch + the typed collect, flow-scoped
|
|
608
|
+
// via the session's pending-wave guard.
|
|
609
|
+
registerReviewWaveTools(pi);
|
|
610
|
+
registerAuditWave(pi);
|
|
611
|
+
registerHarvestWave(pi);
|
|
612
|
+
registerDreamWave(pi);
|
|
613
|
+
|
|
614
|
+
// The flow-scoped draft-review-wave pair (`start_draft_review_wave`/
|
|
615
|
+
// `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review
|
|
616
|
+
// launch over the door-primed context + the typed collect.
|
|
617
|
+
registerDraftReviewWaveTools(pi);
|
|
618
|
+
|
|
619
|
+
// The door-primed browser annotation tool (`push_annotations`): the browser door primes the
|
|
620
|
+
// surface handle on open and clears it on settle/degrade — the tool refuses outside a
|
|
621
|
+
// door-opened flow.
|
|
622
|
+
registerAnnotationPushTool(pi);
|
|
623
|
+
|
|
509
624
|
// The warm `/pr-review-terminal` door: the terminal review entry — hunk always, no provider
|
|
510
625
|
// dispatch (the command IS the selection); posting rides `submit_pr_review` above.
|
|
511
626
|
registerPrReviewTerminal(pi);
|
|
@@ -515,34 +630,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
515
630
|
// human's own platform-post from the UI, with `submit_pr_review` for request-changes only.
|
|
516
631
|
registerPrReviewBrowser(pi);
|
|
517
632
|
|
|
633
|
+
// The warm `/plan-review-browser` door: the summonable streaming draft review — the
|
|
634
|
+
// plannotator plan-review browser on the working plan draft, draft reviewers streaming
|
|
635
|
+
// phrase-anchored findings in; APPROVE auto-saves via the approvalSave seam, DENY returns a
|
|
636
|
+
// model-mediated revision round.
|
|
637
|
+
registerPlanReviewBrowser(pi, gating);
|
|
638
|
+
|
|
639
|
+
// The warm `/objective-review-browser` door: the summonable streaming objective-draft review
|
|
640
|
+
// — the plannotator plan-review browser on the RENDERED working objective draft, draft
|
|
641
|
+
// reviewers streaming phrase-anchored findings in; APPROVE auto-saves via the
|
|
642
|
+
// objectiveApprovalSave seam, Direct Edits = a model-mediated revise round (never auto-saved).
|
|
643
|
+
registerObjectiveReviewBrowser(pi, gating);
|
|
644
|
+
|
|
518
645
|
// The read-only CI executor: the `run_ci` tool + `/ci` command + `--allow-project-ci`
|
|
519
646
|
// flag. Runs the project's `[ci]` named checks deterministically and reports (never fixes/loops).
|
|
520
647
|
registerCiExecutor(pi);
|
|
521
648
|
|
|
522
|
-
// perk-owned checkpoints: seed from the plan body's `## Steps`, advance on `[DONE:n]`.
|
|
523
|
-
// Inert when no step list is present (perk plans are prose). Own `session_start`/`session_tree`/
|
|
524
|
-
// `turn_end` handlers (coexist with the others; pi.on supports multiple handlers per event).
|
|
525
|
-
// Todo-seam deferral: perk is the reference todo provider (`perk-checkpoints`); these
|
|
526
|
-
// runtime surfaces step aside when a foreign `[providers] todo` is selected (the todo-seam mirror
|
|
527
|
-
// of planMode's plan-seam deferral) — silent on the event handlers, announced on `/checkpoints`.
|
|
528
|
-
registerCheckpoints(pi, perkStatus);
|
|
529
|
-
|
|
530
|
-
// The FIRST 3rd-party todo adapter (the todo-seam mirror of registerPlanAdapterTombell).
|
|
531
|
-
// Injection-only: inert unless `[providers] todo = "juicesharp-todo"` is selected AND the session
|
|
532
|
-
// is an active workflow. It carries perk's implement-progress discipline onto `@juicesharp/rpiv-
|
|
533
|
-
// todo`'s checklist overlay (perk's own checkpoints deferred). No `gating` argument —
|
|
534
|
-
// the shim NEVER arbitrates tools (Invariant 1); no registration-time vacating (no command-name
|
|
535
|
-
// collision on the todo seam, unlike the plan seam); never writes `perk:checkpoint`.
|
|
536
|
-
registerTodoAdapterJuicesharp(pi);
|
|
537
|
-
|
|
538
649
|
// The objective substrate: `/objective` set/clear, budget accounting, threshold
|
|
539
650
|
// compaction, all keyed off the now-live `active_objective`. Inert when no objective is active.
|
|
540
651
|
// (The deterministic objective mechanics live in the Python plane: `perk objective …`.)
|
|
541
652
|
registerObjective(pi, perkStatus);
|
|
542
653
|
|
|
543
|
-
// The warm `/commit-and-compact` utility door: drive a commit of the work so far,
|
|
544
|
-
//
|
|
545
|
-
// immediately; no commit → compaction
|
|
654
|
+
// The warm `/commit-and-compact` utility door: drive a commit of the work so far, compact once
|
|
655
|
+
// a successful outcome is known, then completion-gate an automatic evidence-first continuation
|
|
656
|
+
// (clean/read-only trees compact immediately; no commit → no compaction or continuation).
|
|
657
|
+
// Human-only — no tool twin.
|
|
546
658
|
registerCommitAndCompact(pi, gating);
|
|
547
659
|
|
|
548
660
|
// The warm `objective_save` door: the `objective_save` tool + `/objective-save` command
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Run-owned agent scratch provisioning and hidden model guidance (contracts.md §8.1).
|
|
2
|
+
//
|
|
3
|
+
// This module is guidance, not enforcement: it registers no tool and changes no process-global
|
|
4
|
+
// temp environment. Eligible write-capable model turns receive the repository-relative current-run
|
|
5
|
+
// path after the confined directory has been established. The context filter removes inherited or
|
|
6
|
+
// stale direct scratch custom blocks. A compaction summary may quote old prose/path text; that is
|
|
7
|
+
// not a live guidance delivery or authoritative provenance, and is deliberately left intact.
|
|
8
|
+
|
|
9
|
+
import { relative, sep } from "node:path";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { type ReportTarget, report } from "../surfaces/report.ts";
|
|
12
|
+
import { agentScratchDir, ensureAgentScratch } from "./cache.ts";
|
|
13
|
+
import { activeSessionRunId, type SessionDataCtx } from "./sessionData.ts";
|
|
14
|
+
import {
|
|
15
|
+
activeContextWindow,
|
|
16
|
+
type BranchEntry,
|
|
17
|
+
type BranchSource,
|
|
18
|
+
branchOf,
|
|
19
|
+
rebuildWorkflowState,
|
|
20
|
+
} from "./workflowState.ts";
|
|
21
|
+
|
|
22
|
+
export const AGENT_SCRATCH_CONTEXT_TYPE = "perk:agent-scratch";
|
|
23
|
+
|
|
24
|
+
/** Perk-owned children whose canonical definitions are report-only. */
|
|
25
|
+
export const REPORT_ONLY_CHILD_AGENTS = [
|
|
26
|
+
"perk.adversarial-reviewer",
|
|
27
|
+
"perk.draft-reviewer",
|
|
28
|
+
"perk.dream-analyst",
|
|
29
|
+
"perk.dream-reducer",
|
|
30
|
+
"perk.harvest-analyst",
|
|
31
|
+
"perk.learn-analyst",
|
|
32
|
+
"perk.objective-explorer",
|
|
33
|
+
"perk.pr-reviewer",
|
|
34
|
+
"perk.review-angle-selector",
|
|
35
|
+
"perk.review-classifier",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
const REPORT_ONLY_CHILD_SET = new Set<string>(REPORT_ONLY_CHILD_AGENTS);
|
|
39
|
+
|
|
40
|
+
export interface AgentScratchBlock {
|
|
41
|
+
runId: string;
|
|
42
|
+
/** Repository-relative POSIX-style path carried in model context. */
|
|
43
|
+
path: string;
|
|
44
|
+
marker: string;
|
|
45
|
+
content: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type AgentScratchContext = SessionDataCtx & ReportTarget;
|
|
49
|
+
|
|
50
|
+
/** Render the exact run-aware hidden block; provisioning stays in the resolver below. */
|
|
51
|
+
export function renderAgentScratchBlock(cwd: string, runId: string): AgentScratchBlock {
|
|
52
|
+
const path = relative(cwd, agentScratchDir(cwd, runId)).split(sep).join("/");
|
|
53
|
+
const marker = `[PERK AGENT SCRATCH run=${runId} path=${path}]`;
|
|
54
|
+
const content = [
|
|
55
|
+
marker,
|
|
56
|
+
`Put disposable command/model intermediate files for this run in \`${path}/\` instead of shared \`/tmp\`.`,
|
|
57
|
+
"Use descriptive, non-colliding names. These files are non-authoritative: re-read canonical repository or backend sources before making durable decisions.",
|
|
58
|
+
].join("\n");
|
|
59
|
+
return { runId, path, marker, content };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Eligibility follows the branch-LWW workflow mode and the locally exposed pi-subagents child
|
|
64
|
+
* identity. Unknown/custom children remain eligible because no generic report-only metadata exists.
|
|
65
|
+
*/
|
|
66
|
+
export function isAgentScratchEligible(
|
|
67
|
+
ctx: BranchSource,
|
|
68
|
+
childAgent: string | undefined = process.env.PI_SUBAGENT_CHILD_AGENT,
|
|
69
|
+
): boolean {
|
|
70
|
+
try {
|
|
71
|
+
if (rebuildWorkflowState(branchOf(ctx)).mode === "read-only") return false;
|
|
72
|
+
} catch {
|
|
73
|
+
// No rebuilt explicit read-only mode means the parent posture remains the fallback.
|
|
74
|
+
}
|
|
75
|
+
return childAgent === undefined || !REPORT_ONLY_CHILD_SET.has(childAgent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AgentScratchProvisioner {
|
|
79
|
+
resolve(ctx: AgentScratchContext): AgentScratchBlock | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build one extension-activation-scoped resolver. Failures warn once per run but are retried on
|
|
84
|
+
* every call; one success clears suppression so a later regression is reported again.
|
|
85
|
+
*/
|
|
86
|
+
export function createAgentScratchProvisioner(
|
|
87
|
+
deps: {
|
|
88
|
+
ensure?: typeof ensureAgentScratch;
|
|
89
|
+
warn?: (ctx: AgentScratchContext, runId: string, error: unknown) => void;
|
|
90
|
+
} = {},
|
|
91
|
+
): AgentScratchProvisioner {
|
|
92
|
+
const ensure = deps.ensure ?? ensureAgentScratch;
|
|
93
|
+
const warn =
|
|
94
|
+
deps.warn ??
|
|
95
|
+
((ctx: AgentScratchContext, runId: string, error: unknown) => {
|
|
96
|
+
report(
|
|
97
|
+
ctx,
|
|
98
|
+
"agent scratch",
|
|
99
|
+
"warning",
|
|
100
|
+
`could not provision scratch for run ${runId}: ${String(error)}`,
|
|
101
|
+
{ alsoLog: true },
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
const suppressedRuns = new Set<string>();
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
resolve(ctx): AgentScratchBlock | null {
|
|
108
|
+
const runId = activeSessionRunId(ctx);
|
|
109
|
+
if (runId === null) return null;
|
|
110
|
+
try {
|
|
111
|
+
ensure(ctx.cwd, runId);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!suppressedRuns.has(runId)) {
|
|
114
|
+
suppressedRuns.add(runId);
|
|
115
|
+
warn(ctx, runId, error);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
suppressedRuns.delete(runId);
|
|
120
|
+
return renderAgentScratchBlock(ctx.cwd, runId);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Whether this exact current-run block remains directly represented after compaction. */
|
|
126
|
+
function branchHasBlock(branch: readonly BranchEntry[], block: AgentScratchBlock): boolean {
|
|
127
|
+
return activeContextWindow(branch).some(
|
|
128
|
+
(entry) =>
|
|
129
|
+
entry.customType === AGENT_SCRATCH_CONTEXT_TYPE &&
|
|
130
|
+
((entry.type === "custom_message" && entry.content === block.content) ||
|
|
131
|
+
(entry.type === "custom" && entry.data?.content === block.content)),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Register eligible-turn delivery and direct scratch-custom context hygiene. */
|
|
136
|
+
export function registerAgentScratch(
|
|
137
|
+
pi: ExtensionAPI,
|
|
138
|
+
provisioner: AgentScratchProvisioner = createAgentScratchProvisioner(),
|
|
139
|
+
): void {
|
|
140
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
141
|
+
if (!isAgentScratchEligible(ctx)) return;
|
|
142
|
+
|
|
143
|
+
// Provision before dedup: an externally deleted directory is repaired even while the live
|
|
144
|
+
// branch still carries this run's exact guidance block.
|
|
145
|
+
const block = provisioner.resolve(ctx);
|
|
146
|
+
if (block === null) return;
|
|
147
|
+
if (branchHasBlock(branchOf(ctx), block)) return;
|
|
148
|
+
return {
|
|
149
|
+
message: {
|
|
150
|
+
customType: AGENT_SCRATCH_CONTEXT_TYPE,
|
|
151
|
+
content: block.content,
|
|
152
|
+
display: false,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pi.on("context", async (event, ctx) => {
|
|
158
|
+
const eligible = isAgentScratchEligible(ctx);
|
|
159
|
+
const block = eligible ? provisioner.resolve(ctx) : null;
|
|
160
|
+
let keptCurrent = false;
|
|
161
|
+
return {
|
|
162
|
+
messages: event.messages.filter((message) => {
|
|
163
|
+
const candidate = message as { customType?: string; content?: unknown };
|
|
164
|
+
if (candidate.customType !== AGENT_SCRATCH_CONTEXT_TYPE) return true;
|
|
165
|
+
if (block === null || candidate.content !== block.content || keptCurrent) return false;
|
|
166
|
+
keptCurrent = true;
|
|
167
|
+
return true;
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
//
|
|
15
15
|
// This is the SINGLE delivery path for perk's own nudges. Delivery NEVER double-delivers: the
|
|
16
16
|
// cold↔warm dedup marker is `BINDING_HEADER` itself — the cold door's initial prompt and every warm
|
|
17
|
-
// injection carry it, so Mechanism A injects ONLY when neither the branch
|
|
18
|
-
// turn's prompt already carries the header (idempotent across turns/reloads;
|
|
19
|
-
// drops the original it re-delivers). The prompt scan is
|
|
17
|
+
// injection carry it, so Mechanism A injects ONLY when neither the compaction-active branch window
|
|
18
|
+
// NOR the submitting turn's prompt already carries the header (idempotent across turns/reloads;
|
|
19
|
+
// after compaction drops the original from model context it re-delivers). The prompt scan is
|
|
20
|
+
// load-bearing on the launch turn: at
|
|
20
21
|
// `before_agent_start` the just-submitted prompt is NOT yet on the branch, so the branch scan
|
|
21
22
|
// alone would miss a cold seed's binding suffix and double-deliver.
|
|
22
23
|
//
|
|
@@ -30,6 +31,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
30
31
|
import { loadDefaultBindings, resolveBindings, type SkillBinding } from "./bindings.ts";
|
|
31
32
|
import { loadPerkConfig } from "./config.ts";
|
|
32
33
|
import {
|
|
34
|
+
activeContextWindow,
|
|
33
35
|
type BranchEntry,
|
|
34
36
|
branchCarries,
|
|
35
37
|
branchOf,
|
|
@@ -154,13 +156,9 @@ function stripFrontmatter(text: string): string {
|
|
|
154
156
|
return text; // no closing delimiter — leave the text unchanged
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
/**
|
|
158
|
-
* Whether anything on the branch already carries `BINDING_HEADER` — the cold launch's initial
|
|
159
|
-
* prompt OR a prior warm injection. Serializing each entry is the robust, shape-agnostic scan: the
|
|
160
|
-
* header is a distinctive literal, so a substring hit means "already delivered on this branch".
|
|
161
|
-
*/
|
|
159
|
+
/** Whether a cold prompt or warm injection still active in model context carries the marker. */
|
|
162
160
|
function branchHasHeader(branch: readonly BranchEntry[]): boolean {
|
|
163
|
-
return branchCarries(branch, BINDING_HEADER);
|
|
161
|
+
return branchCarries(activeContextWindow(branch), BINDING_HEADER);
|
|
164
162
|
}
|
|
165
163
|
|
|
166
164
|
/** The launched stage's `stage:<id>` render, or `null` when there is no stage / nothing matches. */
|
|
@@ -178,8 +176,8 @@ function activeStageRender(cwd: string, branch: readonly BranchEntry[]): Binding
|
|
|
178
176
|
*/
|
|
179
177
|
export function registerBindingDelivery(pi: ExtensionAPI): void {
|
|
180
178
|
// Mechanism A — inject the launched stage's resolved bindings as a hidden context message,
|
|
181
|
-
// but ONLY when no entry
|
|
182
|
-
// BINDING_HEADER (the cold door's initial prompt or a prior warm inject) — the cold↔warm
|
|
179
|
+
// but ONLY when no entry in the compaction-active branch window AND not the submitting turn's
|
|
180
|
+
// prompt already carries BINDING_HEADER (the cold door's initial prompt or a prior warm inject) — the cold↔warm
|
|
183
181
|
// idempotency guard. The `event.prompt` scan covers the launch turn, where the just-submitted
|
|
184
182
|
// prompt is not yet on the branch; a worker prompt carries no header, so Mechanism A still
|
|
185
183
|
// fires there (contracts.md §8.38).
|