@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,8 +1,9 @@
|
|
|
1
1
|
// The tool-gating primitive (the keystone). Structural read-only enforcement, NOT
|
|
2
2
|
// prompting. Mirrors pi's authoritative `examples/extensions/plan-mode/` recipe (the
|
|
3
3
|
// `setActiveTools` allowlist + `tool_call` bash sub-allowlist + `before_agent_start` injection
|
|
4
|
-
// (once-only: branch-scan dedup'd on the marker, so a
|
|
5
|
-
//
|
|
4
|
+
// (once-only per SELECTED BRANCH: full-branch-scan dedup'd on the marker — historical, so a
|
|
5
|
+
// copy compaction has summarized out of model context still suppresses; enforcement never rode
|
|
6
|
+
// the prose) + `context` strip-when-off) and `preset.ts`'s snapshot-then-restore. The gate attaches to the
|
|
6
7
|
// existing `perk:workflow-state.mode` field (`read-only`/`read-write`) — no new registry stage.
|
|
7
8
|
// Beside the gate lives STAGE_TOOLS: per-stage active-tool scoping for the scoped universe
|
|
8
9
|
// (perk's OWN registered tools + the enumerated borrowed-package census), keyed off the
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
//
|
|
12
13
|
// Substrate only: perk-owned plan mode and the read-only CI executor are the consumers of the
|
|
13
14
|
// `enter`/`exit` surface; the allowlist-restore is wired into the existing
|
|
14
|
-
// `session_start`/`session_tree` rebuild points.
|
|
15
|
+
// `session_start`/`session_tree` rebuild points plus one `resources_discover` re-apply.
|
|
15
16
|
|
|
16
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import { render } from "./prompts.ts";
|
|
@@ -83,12 +84,12 @@ export const LINEAR_MUTATING_TOOLS: readonly string[] = [
|
|
|
83
84
|
];
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
|
-
* pi-subagents' delegation family. `subagent`/`wait` register at load time; the parent
|
|
87
|
-
*
|
|
88
|
-
* (perk is the first `packages` entry)
|
|
89
|
-
* (
|
|
90
|
-
*
|
|
91
|
-
*
|
|
87
|
+
* pi-subagents' delegation family. `subagent`/`wait` register at load time; the parent supervisor
|
|
88
|
+
* tool `subagent_supervisor` registers during pi-subagents' own `session_start` — AFTER perk's
|
|
89
|
+
* sync (perk is the first `packages` entry) — and is admitted by the `resources_discover`
|
|
90
|
+
* re-apply as a late registrant (see `admitLate`): inside the diet at launch, kept where a stage
|
|
91
|
+
* list carries it. `intercom` is the separate pi-intercom bridge's tool name — a static census
|
|
92
|
+
* entry, inert unless that package is present. Child-side tools (`contact_supervisor`,
|
|
92
93
|
* `structured_output`) are out of scope for the STAGE census — spawned children stay
|
|
93
94
|
* stage-unscoped by design (§8.40 adopt-never-impersonates) — but they DO ride READ_ONLY_TOOLS,
|
|
94
95
|
* because the read-only gate IS inherited by adopted children (see SUBAGENT_CHILD_TOOLS).
|
|
@@ -102,36 +103,31 @@ export const SUBAGENT_TOOLS: readonly string[] = [
|
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
105
|
* pi-subagents' CHILD-side engine tools. `structured_output` and `contact_supervisor` register
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* (pi-subagents 0.45.1's `registerWaitTool`) — the accepted widening: a wait-only,
|
|
109
|
-
* non-repo-mutating tool active in gated parents. A gated ADOPTED child (mode inherited via the
|
|
110
|
-
* `adopt` arm, contracts.md §8.3) must keep them active:
|
|
106
|
+
* inside spawned child sessions through the engine's prompt runtime / native supervisor bridge.
|
|
107
|
+
* Absent tools are inert in gated parents (`setActiveTools` ignores unknown names). A gated
|
|
108
|
+
* ADOPTED child (mode inherited via the `adopt` arm, contracts.md §8.3) must keep them active:
|
|
111
109
|
* - `structured_output` is the engine-REQUIRED completion call when the launch carries an
|
|
112
110
|
* `outputSchema` — stripping it makes the child physically unable to finish and fails the
|
|
113
111
|
* run with `structuredOutputFailed`;
|
|
114
|
-
* - `contact_supervisor` is the child→parent
|
|
115
|
-
*
|
|
112
|
+
* - `contact_supervisor` is the child→parent supervisor door.
|
|
113
|
+
* Native wakes need no wait-tool widening in this census.
|
|
116
114
|
* None mutates the repo (`structured_output` writes only the engine's capture file under
|
|
117
115
|
* `.pi-subagents/` scratch). Census decision, recorded: these names deliberately join NEITHER
|
|
118
116
|
* PERK_TOOLS nor BORROWED_TOOLS — the stage-filter universe never sees them because children
|
|
119
117
|
* are stage-unscoped by design (adopt never impersonates a stage), so gate membership is their
|
|
120
118
|
* only governance surface.
|
|
121
119
|
*/
|
|
122
|
-
export const SUBAGENT_CHILD_TOOLS: readonly string[] = [
|
|
123
|
-
"structured_output",
|
|
124
|
-
"contact_supervisor",
|
|
125
|
-
"subagent_wait",
|
|
126
|
-
];
|
|
120
|
+
export const SUBAGENT_CHILD_TOOLS: readonly string[] = ["structured_output", "contact_supervisor"];
|
|
127
121
|
|
|
128
122
|
/**
|
|
129
123
|
* @ff-labs/pi-fff's search tools. BOTH mode name-sets are enumerated (static names, inert
|
|
130
124
|
* when absent — the code_search version-tolerance precedent): warm sessions run pi-fff's
|
|
131
|
-
* default tools-and-ui mode (fffind/ffgrep [+ fff-multi-grep when enabled upstream])
|
|
132
|
-
* perk
|
|
133
|
-
*
|
|
134
|
-
*
|
|
125
|
+
* default tools-and-ui mode (fffind/ffgrep [+ fff-multi-grep when enabled upstream]), and
|
|
126
|
+
* perk launches inject PI_FFF_MODE=tools-and-ui (the same additive mode) so pi's builtin
|
|
127
|
+
* find/grep stay host-builtins for pi-subagents' host-tool intersection (an override-mode
|
|
128
|
+
* pi-fff shadows them by name and fails review/scout lanes closed). The override name-set
|
|
129
|
+
* (multi_grep; find/grep already allowlisted/pass-through) stays enumerated for an operator
|
|
130
|
+
* `PI_FFF_MODE=override` opt-in. All register at load time. Frecency/history state lives under ~/.pi/agent/fff/ — outside the worktree
|
|
135
131
|
* (the fetch_content cache-write precedent), so the read-only bar holds.
|
|
136
132
|
*/
|
|
137
133
|
export const FFF_SEARCH_TOOLS: readonly string[] = [
|
|
@@ -149,12 +145,12 @@ export const FFF_SEARCH_TOOLS: readonly string[] = [
|
|
|
149
145
|
* stage filter (fail-open — enumeration here is diet-completeness, not correctness).
|
|
150
146
|
*
|
|
151
147
|
* Audit records (the per-package census):
|
|
152
|
-
* - Registration timing: every census name registers at load time EXCEPT pi-subagents'
|
|
153
|
-
*
|
|
148
|
+
* - Registration timing: every census name registers at load time EXCEPT pi-subagents'
|
|
149
|
+
* `subagent_supervisor` (SUBAGENT_TOOLS — session_start; admitted at `resources_discover`).
|
|
154
150
|
* - Foreign `setActiveTools` owners: plannotator's phase machinery and @tombell/pi-plan's plan
|
|
155
|
-
* mode run their OWN toggles — perk re-applies only at
|
|
156
|
-
*
|
|
157
|
-
*
|
|
151
|
+
* mode run their OWN toggles — perk re-applies only at its reconciliation points (the rebuilds
|
|
152
|
+
* + the startup `resources_discover` re-apply), so a toggle between them wins (fail-open) and
|
|
153
|
+
* every reconciliation re-installs perk's set over it, admitted late tools included (§8.40).
|
|
158
154
|
* - Zero-tool packages: @tombell/pi-diff (commands only), the footer providers, and the hunk
|
|
159
155
|
* review CLI (not a Pi package) register nothing — nothing to enumerate.
|
|
160
156
|
* - Single-governance rule: a name is governed ONCE — it lives in exactly one census. perk
|
|
@@ -223,9 +219,9 @@ export const READ_ONLY_TOOLS = [
|
|
|
223
219
|
// FFF local search belongs in read-only exploration (the override names find/grep are
|
|
224
220
|
// already present above; these are the additive tools-and-ui names + multi_grep).
|
|
225
221
|
...FFF_SEARCH_TOOLS,
|
|
226
|
-
// The delegation carve-in: `subagent`/`wait` (+
|
|
227
|
-
//
|
|
228
|
-
//
|
|
222
|
+
// The delegation carve-in: `subagent`/`wait` (+ pi-subagents' late-registered
|
|
223
|
+
// `subagent_supervisor`, kept by every gate-ON re-apply because it is allowlisted here —
|
|
224
|
+
// letting the parent answer child `contact_supervisor` asks) stay
|
|
229
225
|
// reachable while gated for the other delegation flows (the gated objective-plan guidance now
|
|
230
226
|
// names the `explore_objective_node` tool below, not a direct spawn). ACCEPTED LENIENCY,
|
|
231
227
|
// deliberately documented: spawned children are unscoped by design (§8.40
|
|
@@ -238,6 +234,11 @@ export const READ_ONLY_TOOLS = [
|
|
|
238
234
|
// the already-carved-in SUBAGENT tools (the draft-review-wave precedent) and writes nothing to
|
|
239
235
|
// the worktree.
|
|
240
236
|
"explore_objective_node",
|
|
237
|
+
// The scout-wave carve-in: the authoring sessions' launcher — `run_scout_wave` spawns
|
|
238
|
+
// read-only `perk.scout` lanes over the already-carved-in delegation family and writes
|
|
239
|
+
// nothing to the worktree; reachable in every gated stage except `objective-refine` on the
|
|
240
|
+
// `explore_objective_node` precedent (contracts.md §8.70).
|
|
241
|
+
"run_scout_wave",
|
|
241
242
|
// The child-side carve-in: gated adopt-children must keep the engine's injected tools — see
|
|
242
243
|
// SUBAGENT_CHILD_TOOLS.
|
|
243
244
|
...SUBAGENT_CHILD_TOOLS,
|
|
@@ -273,6 +274,46 @@ export const READ_ONLY_TOOLS = [
|
|
|
273
274
|
"run_dream_wave",
|
|
274
275
|
];
|
|
275
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The refinement stage id — the ONE stage whose gate-ON selection differs from READ_ONLY_TOOLS.
|
|
279
|
+
* (Registry vocabulary; the feature module re-declares the same literal as its stage constant.)
|
|
280
|
+
*/
|
|
281
|
+
export const REFINE_STAGE_ID = "objective-refine";
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The refinement session's gate-ON selection (contracts.md §8.68): read / research / question
|
|
285
|
+
* / local exploration, the backend-neutral review door, and the ONE refinement draft writer.
|
|
286
|
+
* Deliberately NOT READ_ONLY_TOOLS: no `objective_node` (a refinement never claims), no other
|
|
287
|
+
* draft tools (no plan/objective/gist artifact can be authored or routed from here), no save
|
|
288
|
+
* tools, no delegation spawn surface (children are unscoped by design) — and consequently no
|
|
289
|
+
* SUBAGENT_CHILD_TOOLS either: a child that inherited `stage: objective-refine` would lose
|
|
290
|
+
* `structured_output`/`contact_supervisor` under this allowlist. Unreachable today (no perk wave
|
|
291
|
+
* spawns from a refinement session; the lifecycle test pins `gatedToolsFor(stage)`) — revisit
|
|
292
|
+
* the moment refinement gains any delegation. Same static-name posture as READ_ONLY_TOOLS;
|
|
293
|
+
* `setActiveTools` ignores absent names.
|
|
294
|
+
*/
|
|
295
|
+
export const REFINEMENT_READ_ONLY_TOOLS: readonly string[] = [
|
|
296
|
+
"read",
|
|
297
|
+
"grep",
|
|
298
|
+
"find",
|
|
299
|
+
"ls",
|
|
300
|
+
"bash",
|
|
301
|
+
"ask_user_question",
|
|
302
|
+
"plan_review",
|
|
303
|
+
// The objective_refinement_draft carve-out: it writes only the one working-refinement
|
|
304
|
+
// artifact in the session data dir (fixed artifact name, seam-derived path, bound to the
|
|
305
|
+
// session's grounding context); the gate's edit/write/bash blocking is unchanged.
|
|
306
|
+
"objective_refinement_draft",
|
|
307
|
+
...WEB_RESEARCH_TOOLS,
|
|
308
|
+
...LINEAR_READ_TOOLS,
|
|
309
|
+
...FFF_SEARCH_TOOLS,
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
/** The gate-ON allowlist for a stage: refinement's own selection, else READ_ONLY_TOOLS. */
|
|
313
|
+
export function gatedToolsFor(stage: string | null): readonly string[] {
|
|
314
|
+
return stage === REFINE_STAGE_ID ? REFINEMENT_READ_ONLY_TOOLS : READ_ONLY_TOOLS;
|
|
315
|
+
}
|
|
316
|
+
|
|
276
317
|
/**
|
|
277
318
|
* Every tool perk itself registers (contracts.md §8.40). Name-keyed: `setActiveTools` ignores
|
|
278
319
|
* unknown names, so an absent tool is inert (e.g. a borrowed census name whose package stripped
|
|
@@ -291,6 +332,10 @@ export const PERK_TOOLS: readonly string[] = [
|
|
|
291
332
|
"objective_draft",
|
|
292
333
|
"gist_draft",
|
|
293
334
|
"gist_save",
|
|
335
|
+
// The refinement session's ONE model-facing writer (contracts.md §8.67). Never in
|
|
336
|
+
// READ_ONLY_TOOLS: only the refinement gate-ON selection carries it, so no other gated
|
|
337
|
+
// stage can author a refinement draft.
|
|
338
|
+
"objective_refinement_draft",
|
|
294
339
|
"learn",
|
|
295
340
|
"run_learn_wave",
|
|
296
341
|
"run_audit_wave",
|
|
@@ -302,8 +347,8 @@ export const PERK_TOOLS: readonly string[] = [
|
|
|
302
347
|
"classify_review_feedback",
|
|
303
348
|
"finalize_address",
|
|
304
349
|
"explore_objective_node",
|
|
350
|
+
"run_scout_wave",
|
|
305
351
|
"run_pr_review_wave",
|
|
306
|
-
"run_pr_review_dynamic_wave",
|
|
307
352
|
"submit_pr_review",
|
|
308
353
|
"start_review_wave",
|
|
309
354
|
"collect_review_wave",
|
|
@@ -312,6 +357,10 @@ export const PERK_TOOLS: readonly string[] = [
|
|
|
312
357
|
"collect_draft_review_wave",
|
|
313
358
|
"run_ci",
|
|
314
359
|
"submit",
|
|
360
|
+
"resolve_submit_conflicts",
|
|
361
|
+
// The stack-review launch-recovery tool (contracts.md §8.4): parameterless — the snapshot
|
|
362
|
+
// comes only from the `perk objective stack review` launch handoff.
|
|
363
|
+
"open_stack_review",
|
|
315
364
|
// The stacked-delivery warm surface (contracts.md §8.51/§8.56): read + control tools over
|
|
316
365
|
// the cold `objective stack` workers. Never in READ_ONLY_TOOLS — sync/adopt/recover/land
|
|
317
366
|
// mutate published branches and PRs; the gated posture is the driving commands' soft
|
|
@@ -350,6 +399,7 @@ const RESEARCH_TOOLS: readonly string[] = [
|
|
|
350
399
|
const WORKTREE_STAGE_TOOLS: readonly string[] = [
|
|
351
400
|
"ask_user_question",
|
|
352
401
|
"submit",
|
|
402
|
+
"resolve_submit_conflicts",
|
|
353
403
|
"ready",
|
|
354
404
|
"run_ci",
|
|
355
405
|
"land",
|
|
@@ -359,7 +409,6 @@ const WORKTREE_STAGE_TOOLS: readonly string[] = [
|
|
|
359
409
|
"finalize_address",
|
|
360
410
|
"post_pr_review",
|
|
361
411
|
"run_pr_review_wave",
|
|
362
|
-
"run_pr_review_dynamic_wave",
|
|
363
412
|
"submit_pr_review",
|
|
364
413
|
// The human review doors' companion tools (/pr-review-terminal, /pr-review-browser): the
|
|
365
414
|
// review-wave pair + the door-primed annotation push. The plan-stage widening landed via the
|
|
@@ -411,6 +460,15 @@ const WORKTREE_STAGE_TOOLS: readonly string[] = [
|
|
|
411
460
|
* review arm; the drive-coverage guard forces both the moment the guidance names them).
|
|
412
461
|
*/
|
|
413
462
|
export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
|
|
463
|
+
// The isolated refinement stage (contracts.md §8.68): the gate-OFF (defensive) arm scopes the
|
|
464
|
+
// draft writer + the review door + research only — no PR-loop, save, claim or other draft
|
|
465
|
+
// tools. The session normally runs GATED, where REFINEMENT_READ_ONLY_TOOLS is the set.
|
|
466
|
+
[REFINE_STAGE_ID]: [
|
|
467
|
+
"ask_user_question",
|
|
468
|
+
"objective_refinement_draft",
|
|
469
|
+
"plan_review",
|
|
470
|
+
...RESEARCH_TOOLS,
|
|
471
|
+
],
|
|
414
472
|
"gist-author": ["ask_user_question", "gist_draft", "gist_save", ...RESEARCH_TOOLS],
|
|
415
473
|
"gist-save": ["ask_user_question", "gist_draft", "gist_save", ...RESEARCH_TOOLS],
|
|
416
474
|
"objective-author": [
|
|
@@ -420,6 +478,10 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
|
|
|
420
478
|
"reconcile_objective",
|
|
421
479
|
"add_objective_node",
|
|
422
480
|
"objective_node",
|
|
481
|
+
// The scout launcher rides the three AUTHORING stages (plan / objective-plan /
|
|
482
|
+
// objective-author) and no other — the one name by which this list and objective-save
|
|
483
|
+
// now differ (contracts.md §8.70).
|
|
484
|
+
"run_scout_wave",
|
|
423
485
|
// The /objective-review-browser companions (gate-OFF coverage: after objectiveApprovalSave
|
|
424
486
|
// exits the gate mid-flow, late collects/pushes must not dead-end) + plan_review (the door
|
|
425
487
|
// guidance names it; it routes to the objective review arm here).
|
|
@@ -450,6 +512,8 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
|
|
|
450
512
|
"plan_save",
|
|
451
513
|
"objective_node",
|
|
452
514
|
"explore_objective_node",
|
|
515
|
+
// The scout launcher (see the objective-author note).
|
|
516
|
+
"run_scout_wave",
|
|
453
517
|
"reconcile_objective",
|
|
454
518
|
"add_objective_node",
|
|
455
519
|
// The /plan-review-browser companions (gate-OFF coverage: after approvalSave exits the gate
|
|
@@ -465,6 +529,8 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
|
|
|
465
529
|
"plan_draft",
|
|
466
530
|
"plan_review",
|
|
467
531
|
"plan_save",
|
|
532
|
+
// The scout launcher (see the objective-author note).
|
|
533
|
+
"run_scout_wave",
|
|
468
534
|
// The /plan-review-browser companions (see the objective-plan note).
|
|
469
535
|
"start_draft_review_wave",
|
|
470
536
|
"collect_draft_review_wave",
|
|
@@ -491,18 +557,62 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
|
|
|
491
557
|
// (read-only mode), where this list is inert; it exists for the keys≡registry pin and the
|
|
492
558
|
// defensive gate-off arm.
|
|
493
559
|
audit: ["ask_user_question", "run_audit_wave", ...RESEARCH_TOOLS],
|
|
560
|
+
// The stacked-PR browser-review launcher (`perk objective stack review`): exactly the flow
|
|
561
|
+
// set the stack.md guidance names — the launch-recovery tool, the review-wave pair, the
|
|
562
|
+
// annotation push, per-PR posting, delegation (the wave's relay loop), and research.
|
|
563
|
+
"stack-review": [
|
|
564
|
+
"ask_user_question",
|
|
565
|
+
"open_stack_review",
|
|
566
|
+
"start_review_wave",
|
|
567
|
+
"collect_review_wave",
|
|
568
|
+
"push_annotations",
|
|
569
|
+
"submit_pr_review",
|
|
570
|
+
...SUBAGENT_TOOLS,
|
|
571
|
+
...RESEARCH_TOOLS,
|
|
572
|
+
],
|
|
494
573
|
};
|
|
495
574
|
|
|
496
575
|
/** The read-only marker / custom-message type injected into context while active. */
|
|
497
576
|
const MODE_CONTEXT_TYPE = "perk:mode-context";
|
|
498
577
|
const READ_ONLY_MARKER = "[READ-ONLY MODE]";
|
|
578
|
+
/** The refinement flavor's DISTINCT dedup marker (not a superstring of the default marker, so
|
|
579
|
+
* neither flavor's presence masks the other's dedup scan). */
|
|
580
|
+
const REFINEMENT_READ_ONLY_MARKER = "[READ-ONLY REFINEMENT MODE]";
|
|
581
|
+
const MODE_MARKERS: readonly string[] = [READ_ONLY_MARKER, REFINEMENT_READ_ONLY_MARKER];
|
|
499
582
|
|
|
500
583
|
/** Exported for tests: the injected read-only mode context. (No tool enumeration — gate-ON
|
|
501
584
|
* already applies READ_ONLY_TOOLS via setActiveTools, so the active tool set IS the list.) */
|
|
502
585
|
export const READ_ONLY_CONTEXT = render("contexts/read-only.md", {
|
|
503
586
|
marker: READ_ONLY_MARKER,
|
|
587
|
+
writer: "plan_draft",
|
|
588
|
+
artifact: "working-plan artifact",
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
/** Exported for tests: the refinement flavor — names the ACTUAL sanctioned writer. */
|
|
592
|
+
export const REFINEMENT_READ_ONLY_CONTEXT = render("contexts/read-only.md", {
|
|
593
|
+
marker: REFINEMENT_READ_ONLY_MARKER,
|
|
594
|
+
writer: "objective_refinement_draft",
|
|
595
|
+
artifact: "working-refinement artifact",
|
|
504
596
|
});
|
|
505
597
|
|
|
598
|
+
/** The mode-context flavor for a stage (the refinement session names its own writer). */
|
|
599
|
+
function modeContextFor(stage: string | null): { marker: string; content: string } {
|
|
600
|
+
return stage === REFINE_STAGE_ID
|
|
601
|
+
? { marker: REFINEMENT_READ_ONLY_MARKER, content: REFINEMENT_READ_ONLY_CONTEXT }
|
|
602
|
+
: { marker: READ_ONLY_MARKER, content: READ_ONLY_CONTEXT };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function textCarries(content: unknown, needles: readonly string[]): boolean {
|
|
606
|
+
if (typeof content === "string") return needles.some((n) => content.includes(n));
|
|
607
|
+
if (Array.isArray(content)) {
|
|
608
|
+
return content.some((c) => {
|
|
609
|
+
const part = c as { type?: string; text?: string };
|
|
610
|
+
return part.type === "text" && needles.some((n) => (part.text ?? "").includes(n));
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
506
616
|
// --- pure policy (copied from plan-mode/utils.ts so this primitive is self-contained; perk-owned
|
|
507
617
|
// so retiring the borrowed pi-plan extension leaves no dangling import) -------
|
|
508
618
|
|
|
@@ -615,8 +725,19 @@ const SAFE_PATTERNS = [
|
|
|
615
725
|
// perk's own read-only objective queries (show/next + their s/n aliases, plus the non-mutating
|
|
616
726
|
// node-engagement read the objective-plan factory needs). The trailing \b keeps the `n` alias
|
|
617
727
|
// from matching the mutating `node` subcommand; node-engagement allowed; create/node/reconcile
|
|
618
|
-
// stay blocked.
|
|
728
|
+
// stay blocked. node-engagement materializes a present refinement under the run scratch dir —
|
|
729
|
+
// the same accepted leniency as `perk pr review-context` below (the CLI writes its own
|
|
730
|
+
// gitignored scratch file; the destructive veto still blocks `> file` redirects).
|
|
619
731
|
/^\s*perk\s+(objective|obj)\s+(show|s|next|n|node-engagement)\b/i,
|
|
732
|
+
// Report children need exactly these query forms, not arbitrary PR operations: plan-bound
|
|
733
|
+
// children (`/pr-review`, `/address`) use the `--expected-pr` form; the human-triage doors'
|
|
734
|
+
// adversarial children — and the stack-review routing step — use the foreign `--pr` /
|
|
735
|
+
// `--pr --stack` forms (a read-only parent would otherwise block the doors' children outright).
|
|
736
|
+
// ONE anchored alternation: `--json` last, nothing else; the flagless form stays out (only the
|
|
737
|
+
// write-capable foreground conflict-resolver uses it, outside the gate). The destructive veto
|
|
738
|
+
// still blocks `> file` redirects — the CLI writes its own scratch files.
|
|
739
|
+
/^\s*perk\s+pr\s+review-context\s+(?:--expected-pr\s+[1-9][0-9]*|--pr\s+[1-9][0-9]*(?:\s+--stack)?)\s+--json\s*$/,
|
|
740
|
+
/^\s*perk\s+pr\s+feedback\s+--json\s*$/,
|
|
620
741
|
// Read-only `gh` queries — the guidance in the managed AGENTS block ("GitHub access goes
|
|
621
742
|
// through gh") must be followable in read-only sessions. Query-shaped subcommands only;
|
|
622
743
|
// `gh api` stays blocked (it can POST/PATCH), as do all mutating subcommands (create/edit/
|
|
@@ -636,7 +757,7 @@ const SAFE_PATTERNS = [
|
|
|
636
757
|
* Known limitation: backslash-escaped quote characters are not handled. This is acceptable — the
|
|
637
758
|
* whole-string destructive veto in isReadOnlyBashCommand remains the backstop.
|
|
638
759
|
*/
|
|
639
|
-
function splitTopLevelSegments(command: string): string[] {
|
|
760
|
+
export function splitTopLevelSegments(command: string): string[] {
|
|
640
761
|
const segments: string[] = [];
|
|
641
762
|
let current = "";
|
|
642
763
|
let quote: '"' | "'" | null = null;
|
|
@@ -724,16 +845,37 @@ function isReadOnlyMode(mode: string | undefined): boolean {
|
|
|
724
845
|
return mode === "read-only";
|
|
725
846
|
}
|
|
726
847
|
|
|
727
|
-
|
|
848
|
+
/** The engagement record: the host's active starting set (the restore authority — never
|
|
849
|
+
* `getAllTools()`), the registry census at snapshot time, and the late registrants admitted so far. */
|
|
850
|
+
type Engaged = { snapshot: readonly string[]; census: ReadonlySet<string>; admitted: Set<string> };
|
|
851
|
+
|
|
852
|
+
export function registerToolGating(
|
|
853
|
+
pi: ExtensionAPI,
|
|
854
|
+
readOnlyFloor: () => boolean = () => false,
|
|
855
|
+
): ToolGating {
|
|
728
856
|
// In-memory gate (mirrors plan-mode's `planModeEnabled`): the authority `tool_call` consults.
|
|
729
857
|
// Fail-closed — a failed sync never opens this; tool_call blocks on any internal error.
|
|
730
858
|
let active = false;
|
|
731
859
|
// The branch-LWW stage id this session is scoped to (null = unscoped). Fail-open by contrast
|
|
732
860
|
// with `active`: no stage / unknown stage / any lookup miss → no filtering.
|
|
733
861
|
let stageId: string | null = null;
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
let
|
|
862
|
+
// Taken ONCE on the first engagement of either concern (the preset.ts discipline, shared by the
|
|
863
|
+
// gate and stage scoping); cleared when neither is engaged any more.
|
|
864
|
+
let engaged: Engaged | null = null;
|
|
865
|
+
|
|
866
|
+
function hasFloor(): boolean {
|
|
867
|
+
try {
|
|
868
|
+
return readOnlyFloor();
|
|
869
|
+
} catch {
|
|
870
|
+
// A broken restriction supplier cannot grant authority for this observation.
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const isActive = () => active || hasFloor();
|
|
876
|
+
// The gate-ON allowlist follows the scoped stage: refinement's own selection, else the
|
|
877
|
+
// shared READ_ONLY_TOOLS (recomputed per observation — a late stage sync re-scopes it).
|
|
878
|
+
const gatedToolNames = (): ReadonlySet<string> => new Set(gatedToolsFor(stageId));
|
|
737
879
|
|
|
738
880
|
/**
|
|
739
881
|
* Recompute + install the active tool set from both concerns (contracts.md §8.40):
|
|
@@ -742,10 +884,10 @@ export function registerToolGating(pi: ExtensionAPI): ToolGating {
|
|
|
742
884
|
* `/objective-plan` carve-out and recreate the seed/gate contradiction class). "The gate
|
|
743
885
|
* never widens a stage's set and vice versa" still holds: engaging the gate only ever
|
|
744
886
|
* narrows, and stage scoping never adds a tool.
|
|
745
|
-
* - gate OFF + stage scoped → a SUBTRACTIVE filter over the
|
|
746
|
-
* scoped universe (builtins, un-enumerated foreign tools) pass through untouched;
|
|
747
|
-
* names (PERK_TOOLS ∪ BORROWED_TOOLS) survive only when the stage's list carries them.
|
|
748
|
-
* - neither engaged → restore the
|
|
887
|
+
* - gate OFF + stage scoped → a SUBTRACTIVE filter over the baseline (`admitLate`): names
|
|
888
|
+
* outside the scoped universe (builtins, un-enumerated foreign tools) pass through untouched;
|
|
889
|
+
* scoped names (PERK_TOOLS ∪ BORROWED_TOOLS) survive only when the stage's list carries them.
|
|
890
|
+
* - neither engaged → restore the baseline and forget it (a session that never engages gets
|
|
749
891
|
* ZERO setActiveTools calls — bare warm sessions stay byte-identical).
|
|
750
892
|
* While engaged the set is re-installed on every sync (tree navigation across mode entries
|
|
751
893
|
* must recompute correctly).
|
|
@@ -754,40 +896,73 @@ export function registerToolGating(pi: ExtensionAPI): ToolGating {
|
|
|
754
896
|
// un-enumerated foreign names pass through every stage filter untouched (fail-open).
|
|
755
897
|
const SCOPED_TOOL_NAMES: ReadonlySet<string> = new Set([...PERK_TOOLS, ...BORROWED_TOOLS]);
|
|
756
898
|
|
|
899
|
+
/**
|
|
900
|
+
* The gate-OFF reconciliation baseline `snapshot ∪ admitted`. A tool the census never saw
|
|
901
|
+
* (pi-subagents' `subagent_supervisor` registers in its own `session_start`, after perk's sync)
|
|
902
|
+
* is admitted the first time it is seen active and stays admitted through perk's own filtering
|
|
903
|
+
* (so a navigation back to an admitting stage restores it); an owner that deactivates its late
|
|
904
|
+
* tool before perk ever sees it active is respected; a tool the census saw inactive is never
|
|
905
|
+
* re-activated. Gate-OFF paths only (the gate-ON set is by name — no bookkeeping there).
|
|
906
|
+
*/
|
|
907
|
+
function admitLate(e: Engaged): string[] {
|
|
908
|
+
for (const name of pi.getActiveTools()) if (!e.census.has(name)) e.admitted.add(name);
|
|
909
|
+
return [...new Set([...e.snapshot, ...e.admitted])];
|
|
910
|
+
}
|
|
911
|
+
|
|
757
912
|
function apply(nextActive: boolean, nextStage: string | null): void {
|
|
913
|
+
// Fail-closed: a read-only sync engages the in-memory gate BEFORE the fallible reads/installs.
|
|
914
|
+
if (nextActive) active = true;
|
|
915
|
+
const effective = nextActive || hasFloor();
|
|
758
916
|
const stageList = nextStage === null ? undefined : STAGE_TOOLS[nextStage];
|
|
759
|
-
// First engagement of either concern:
|
|
760
|
-
if ((
|
|
761
|
-
|
|
917
|
+
// First engagement of either concern: ONE literal (a throwing read records no half state).
|
|
918
|
+
if ((effective || stageList !== undefined) && engaged === null) {
|
|
919
|
+
engaged = {
|
|
920
|
+
snapshot: pi.getActiveTools(),
|
|
921
|
+
census: new Set(pi.getAllTools().map((t) => t.name)),
|
|
922
|
+
admitted: new Set(),
|
|
923
|
+
};
|
|
762
924
|
}
|
|
763
|
-
if (
|
|
764
|
-
pi.setActiveTools([...
|
|
765
|
-
} else if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
snapshot = null;
|
|
925
|
+
if (effective) {
|
|
926
|
+
pi.setActiveTools([...gatedToolsFor(nextStage)]);
|
|
927
|
+
} else if (engaged !== null) {
|
|
928
|
+
const base = admitLate(engaged);
|
|
929
|
+
if (stageList !== undefined) {
|
|
930
|
+
pi.setActiveTools(
|
|
931
|
+
base.filter((name) => !SCOPED_TOOL_NAMES.has(name) || stageList.includes(name)),
|
|
932
|
+
);
|
|
933
|
+
} else {
|
|
934
|
+
pi.setActiveTools(base);
|
|
935
|
+
engaged = null;
|
|
936
|
+
}
|
|
776
937
|
}
|
|
777
938
|
active = nextActive;
|
|
778
939
|
stageId = nextStage;
|
|
779
940
|
}
|
|
780
941
|
|
|
781
|
-
//
|
|
942
|
+
// Pi fires `resources_discover` after EVERY extension's `session_start` has run (pi-subagents
|
|
943
|
+
// registers `subagent_supervisor` there, after perk's sync) — the one point where startup-late
|
|
944
|
+
// registrants meet the gate/stage diet: re-apply the in-memory mode/stage (no rebuild, no other
|
|
945
|
+
// checkpoint). Idempotent on `reason: "reload"`; a throw is Pi-reported and never opens the gate.
|
|
946
|
+
pi.on("resources_discover", async () => {
|
|
947
|
+
apply(active, stageId);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Enforce the whole allowlist even if toolset narrowing failed or foreign tools registered late.
|
|
782
951
|
pi.on("tool_call", async (event) => {
|
|
783
952
|
try {
|
|
784
|
-
if (!
|
|
953
|
+
if (!isActive()) return;
|
|
785
954
|
if (event.toolName === "edit" || event.toolName === "write") {
|
|
786
955
|
return {
|
|
787
956
|
block: true,
|
|
788
957
|
reason: `perk read-only mode: ${event.toolName} is blocked (file modifications disabled).`,
|
|
789
958
|
};
|
|
790
959
|
}
|
|
960
|
+
if (!gatedToolNames().has(event.toolName)) {
|
|
961
|
+
return {
|
|
962
|
+
block: true,
|
|
963
|
+
reason: `perk read-only mode: ${event.toolName} is blocked (tool not allowlisted).`,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
791
966
|
if (event.toolName === "bash") {
|
|
792
967
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
793
968
|
if (!isReadOnlyBashCommand(command)) {
|
|
@@ -806,33 +981,51 @@ export function registerToolGating(pi: ExtensionAPI): ToolGating {
|
|
|
806
981
|
|
|
807
982
|
// Inject the hidden read-only mode context while active (display:false → not shown in transcript).
|
|
808
983
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
809
|
-
if (!
|
|
810
|
-
// Once-only: injected customs persist to the branch,
|
|
811
|
-
//
|
|
812
|
-
|
|
984
|
+
if (!isActive()) return;
|
|
985
|
+
// Once-only per selected branch: injected customs persist to the branch, and the FULL-branch
|
|
986
|
+
// scan (not live model context) dedups — a copy compaction summarized away still counts as
|
|
987
|
+
// delivered on this branch history and is NOT re-injected; navigating to a branch that never
|
|
988
|
+
// carried it injects again. Deliberate: this is the structural gate's guidance, and the gate
|
|
989
|
+
// itself (tool_call) enforces regardless of what the model can still read.
|
|
990
|
+
// The flavor follows the scoped stage: the refinement session names its actual writer. The
|
|
991
|
+
// dedup key is the SELECTED flavor's marker, so a session gated under the default flavor
|
|
992
|
+
// that then enters refinement still receives the refinement flavor once.
|
|
993
|
+
const flavor = modeContextFor(stageId);
|
|
994
|
+
try {
|
|
995
|
+
if (branchCarries(branchOf(ctx), flavor.marker)) return;
|
|
996
|
+
} catch {
|
|
997
|
+
// An unreadable branch cannot suppress required read-only guidance.
|
|
998
|
+
}
|
|
813
999
|
return {
|
|
814
|
-
message: { customType: MODE_CONTEXT_TYPE, content:
|
|
1000
|
+
message: { customType: MODE_CONTEXT_TYPE, content: flavor.content, display: false },
|
|
815
1001
|
};
|
|
816
1002
|
});
|
|
817
1003
|
|
|
818
|
-
//
|
|
1004
|
+
// Gate OFF: strip every stale mode-context flavor (so none lingers). Gate ON: retain ONLY the
|
|
1005
|
+
// current flavor's injected block — a stale `plan_draft`-only block from before a stage
|
|
1006
|
+
// change is dropped from model context (the injected customType only; user content is never
|
|
1007
|
+
// touched while the gate is on).
|
|
819
1008
|
pi.on("context", async (event) => {
|
|
820
|
-
if (
|
|
1009
|
+
if (isActive()) {
|
|
1010
|
+
const current = modeContextFor(stageId).marker;
|
|
1011
|
+
const stale = (m: unknown): boolean => {
|
|
1012
|
+
const msg = m as { customType?: string; content?: unknown };
|
|
1013
|
+
return (
|
|
1014
|
+
msg.customType === MODE_CONTEXT_TYPE &&
|
|
1015
|
+
textCarries(msg.content, MODE_MARKERS) &&
|
|
1016
|
+
!textCarries(msg.content, [current])
|
|
1017
|
+
);
|
|
1018
|
+
};
|
|
1019
|
+
// No stale flavor → no intervention (the hook stays a no-op for every other message).
|
|
1020
|
+
if (!event.messages.some(stale)) return;
|
|
1021
|
+
return { messages: event.messages.filter((m) => !stale(m)) };
|
|
1022
|
+
}
|
|
821
1023
|
return {
|
|
822
1024
|
messages: event.messages.filter((m) => {
|
|
823
1025
|
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
824
1026
|
if (msg.customType === MODE_CONTEXT_TYPE) return false;
|
|
825
1027
|
if (msg.role !== "user") return true;
|
|
826
|
-
|
|
827
|
-
if (typeof content === "string") return !content.includes(READ_ONLY_MARKER);
|
|
828
|
-
if (Array.isArray(content)) {
|
|
829
|
-
return !content.some(
|
|
830
|
-
(c) =>
|
|
831
|
-
(c as { type?: string; text?: string }).type === "text" &&
|
|
832
|
-
((c as { text?: string }).text ?? "").includes(READ_ONLY_MARKER),
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
return true;
|
|
1028
|
+
return !textCarries(msg.content, MODE_MARKERS);
|
|
836
1029
|
}),
|
|
837
1030
|
};
|
|
838
1031
|
});
|
|
@@ -846,11 +1039,13 @@ export function registerToolGating(pi: ExtensionAPI): ToolGating {
|
|
|
846
1039
|
apply(true, stageId);
|
|
847
1040
|
},
|
|
848
1041
|
exit(_ctx?: ExtensionContext): void {
|
|
1042
|
+
if (hasFloor()) {
|
|
1043
|
+
apply(true, stageId);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
849
1046
|
pi.appendEntry(WORKFLOW_STATE_TYPE, { mode: "read-write" });
|
|
850
1047
|
apply(false, stageId);
|
|
851
1048
|
},
|
|
852
|
-
isActive
|
|
853
|
-
return active;
|
|
854
|
-
},
|
|
1049
|
+
isActive,
|
|
855
1050
|
};
|
|
856
1051
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A deliberately strict unified-diff applier for the plannotator "Direct Edits" feedback only
|
|
2
|
-
// (`extension/
|
|
2
|
+
// (`extension/pi/v1/providers/plannotator.ts` extracts the ```diff fence; the plan arm of
|
|
3
3
|
// `plan_review` applies it to the exact draft bytes it submitted) — this is NOT a general-purpose
|
|
4
4
|
// patch tool.
|
|
5
5
|
//
|