// perk Pi extension — the session *interior*: the composition root. // // The tier-3 session-state mechanics (contracts.md §8.2/§8.3) — claim PERK_RUN_ID on // `session_start` (verified-linkage), rebuild `perk:workflow-state` on `session_start` AND // `session_tree` (per-field LWW), derive a child run_id on fork, reconcile the stage-gated plan // linkage — are OWNED by `session/lifecycle.ts` (identity arms + the two-phase startup facts + // the navigation facts). This file binds the production ports and keeps the Pi effects visibly // ORDERED: gate sync from the pure scope slice → claimed-only refinement import → the post-gate // facts → implementation pointer capture → feedback receiver sync → presentation/probe tail. import { existsSync, mkdirSync } from "node:fs"; import { basename, join } from "node:path"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { createDraftReviewWaveState } from "./authoring/review/draftContext.ts"; import { createHunkFeedbackReceiver, type HunkFeedbackReceiver } from "./hunkFeedback/receiver.ts"; import { registerBashScanTimeout } from "./pi/v1/bashScanTimeout.ts"; import { installAutomatedReviewBindings } from "./pi/v1/codeReview/automated.ts"; import { installPrReviewBrowserBindings } from "./pi/v1/codeReview/browser.ts"; import { installReviewWaveBindings } from "./pi/v1/codeReview/reviewWave.ts"; import { installStackReviewBindings } from "./pi/v1/codeReview/stack.ts"; import { installCuratedSubmissionBindings } from "./pi/v1/codeReview/submit.ts"; import { installPrReviewTerminalBindings } from "./pi/v1/codeReview/terminal.ts"; import { installAddressBindings } from "./pi/v1/delivery/address.ts"; import { installCiBindings } from "./pi/v1/delivery/ci.ts"; import { installCommitCompactBindings } from "./pi/v1/delivery/commitCompact.ts"; import { type ConflictResolverEngineOptions, createConflictResolverEngine, } from "./pi/v1/delivery/conflictResolverEngine.ts"; import { installLandBindings } from "./pi/v1/delivery/land.ts"; import { installReadyBindings } from "./pi/v1/delivery/ready.ts"; import { createStackConflictResolver } from "./pi/v1/delivery/stackConflictResolver.ts"; import { installStackLandBindings } from "./pi/v1/delivery/stackLand.ts"; import { installStackRecoverBindings } from "./pi/v1/delivery/stackRecover.ts"; import { installStackStatusBindings } from "./pi/v1/delivery/stackStatus.ts"; import { installStackSyncBindings, type StackResolutionDelivery, } from "./pi/v1/delivery/stackSync.ts"; import { installSubmitBindings } from "./pi/v1/delivery/submit.ts"; import { installSubmitConflictBindings } from "./pi/v1/delivery/submitConflict.ts"; import { createDraftReviewSlot } from "./pi/v1/draftReview.ts"; import { registerDraftReviewWaveTools } from "./pi/v1/draftReviewWaveTools.ts"; import { installGistBindings } from "./pi/v1/gist.ts"; import { installAuditBindings } from "./pi/v1/learning/audit.ts"; import { installDreamBindings } from "./pi/v1/learning/dream.ts"; import { installLearnFactoryBindings } from "./pi/v1/learning/factory.ts"; import { installHarvestBindings } from "./pi/v1/learning/harvest.ts"; import { installLearnBindings } from "./pi/v1/learning/learn.ts"; import { registerLifecycleGates } from "./pi/v1/lifecycleGates.ts"; import { installObjectiveBindings } from "./pi/v1/objective.ts"; import { installObjectiveAuthoringBindings } from "./pi/v1/objectiveAuthoring.ts"; import { installObjectivePlanningBindings } from "./pi/v1/objectivePlanning.ts"; import { importRefinementContextOnClaim, installObjectiveRefinementBindings, } from "./pi/v1/objectiveRefinement.ts"; import { openObjectiveReviewSurface, registerObjectiveReviewBrowser, } from "./pi/v1/objectiveReviewBrowser.ts"; import { installPlanBindings } from "./pi/v1/plan.ts"; import { openPlanReviewSurface, registerPlanReviewBrowser } from "./pi/v1/planReviewBrowser.ts"; import { createAnnotationState, installAnnotationBindings } from "./pi/v1/providers/annotations.ts"; import { installPlannotatorPlanAdapter } from "./pi/v1/providers/plannotator.ts"; import { plannotatorPresent } from "./pi/v1/providers/plannotatorHandoff.ts"; import { installTombellPlanAdapter } from "./pi/v1/providers/tombell.ts"; import { installScoutWaveBindings } from "./pi/v1/scoutWave.ts"; import { registerSelfcheck } from "./pi/v1/selfcheck.ts"; import { branchSessionStateStore, establishSessionIdentity, reflectSessionReadOnlyFloor, resolveSessionStartFacts, sessionStartToolScope, sessionTreeFacts, } from "./session/lifecycle.ts"; import { createAgentScratchProvisioner, registerAgentScratch } from "./substrate/agentScratch.ts"; import { registerBindingDelivery } from "./substrate/bindingDelivery.ts"; import { atomicWriteFileSync, ensureRunScratch, listRunIds, markHandoffConsumed, readHandoff, readPlanRef, setMarker, workflowDir, } from "./substrate/cache.ts"; import { decodeReadOnlyFloor, isRunnerChild } from "./substrate/childRestrictions.ts"; import { loadRegistry, type Registry } from "./substrate/registry.ts"; import { perkVersion, sharedDir, versionStamp } from "./substrate/resources.ts"; import { mintRunId } from "./substrate/runId.ts"; import { captureSessionPointer } from "./substrate/sessionPointers.ts"; import { registerToolGating } from "./substrate/toolGating.ts"; import { branchOf, rebuildWorkflowState, WORKFLOW_STATE_TYPE, type WorkflowState, } from "./substrate/workflowState.ts"; import { isPerkFooterReferenceSelected } from "./surfaces/footerProvider.ts"; import { report } from "./surfaces/report.ts"; import { createPerkStatus, installPerkFooter, latestCacheHitRate, REPORT_DETAIL_TYPE, registerTranscriptRenderer, reportDetailEntryRenderer, workflowStateEntryRenderer, } from "./surfaces/surfaces.ts"; import { registerBtw } from "./vendor/btw/btw.ts"; import { registerWhimsical } from "./vendor/whimsical/whimsical.ts"; import { createReportWave } from "./waves/reportWave.ts"; // Cross-plane proof marker (TS writes via cache.ts; the Python helper reads it — gate check 3). const T3_MARKER = "t3-extension-cache-write"; function writeT3Sentinel( cwd: string, source: string, state: WorkflowState, runMode: string | null, ): void { try { const dir = workflowDir(cwd); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); atomicWriteFileSync( join(dir, ".perk-t3.json"), `${JSON.stringify({ source, // The launch *workflow* mode (read-only/read-write) — drives tool gating. run_id: state.run_id ?? null, mode: state.mode ?? null, // The Pi *run* mode (tui/rpc/json/print) — observability `hasUI` can't express. Distinct // from the workflow `mode` above; recorded straight from `ctx.mode`. run_mode: runMode, predecessor: state.predecessor ?? null, pi_session_id: state.pi_session_id ?? null, active_plan_ref: state.active_plan_ref ?? null, })}\n`, ); } catch { // never throw from a probe } } export default function perk( pi: ExtensionAPI, options: { resolverEngine?: Pick; stackResolutionDelivery?: StackResolutionDelivery; /** * Construction-only: the hunk feedback receiver factory (default `createHunkFeedbackReceiver`). * Constructed ONCE per activation exactly like production; the registered-path suites bind a * recording receiver to observe the startup/navigation sync order and inputs. */ feedbackReceiverFactory?: (pi: ExtensionAPI) => HunkFeedbackReceiver; } = {}, ) { const version = perkVersion(); // The read-only tool-gating primitive. Attaches to perk:workflow-state.mode; synced on // both session_start AND session_tree below. enter/exit are the surface the gated stages consume. // The two native-child booleans (§8.3), re-read at every session_start: the runner bit (also the // `() => runnerChild` closure every injected authoring/adapter context takes as its fence — the // REAL closure, never a constant), and the read-only floor a runner child derives from the // report restriction packet — latched for the activation (`||=`) so no later session_start, // gate exit, or tree navigation can clear it. let runnerChild = false; let readOnlyFloor = false; const gating = registerToolGating(pi, () => readOnlyFloor); // The bash scan-timeout guard: always on in every perk session — gated or not, runner children // included (the slow gitignore-blind scans were observed in read-write sessions too). The gate's // `tool_call` hook runs first only because it registers first; a gate block short-circuits // before injection matters. registerBashScanTimeout(pi); // Run-owned disposable scratch guidance for every eligible write-capable model turn. One // activation-scoped provisioner shares retry/warning suppression with the isolated /btw side // session; no model tool or process-global temp environment is introduced. const agentScratch = createAgentScratchProvisioner(); registerAgentScratch(pi, agentScratch, () => !gating.isActive() && !runnerChild); // Vendored `btw`: a `/btw` human-only side-chat popover backed by an isolated in-memory // AgentSession. Takes `gating` for the gate-mirror — its side-session toolset + cache key follow // perk's read-only gate (`sideSessionTools`), so the isolated session never bypasses the read-only // guarantee. Its `ctx.ui.custom` overlay is the ONE sanctioned charter exception (§6 D6): human- // invoked only, `hasUI`-gated, no model tool, not a stage/door — never machine-reachable. registerBtw(pi, gating, agentScratch); // Vendored `whimsical`: flavors pi's default working-message label with a random phrase per // turn, via the headless-no-op `setWorkingMessage` surfaces seam. Always on, no config toggle. registerWhimsical(pi); // The draft-review pair's per-activation state: ONE instance serves the `plan_review` wave // arm (via the closures below), the two draft-review browser doors, and the // `start_draft_review_wave`/`collect_draft_review_wave` tool pair — per activation, never per // process (two bound sessions in one process share nothing). Plain object construction, so // creating it before any registration is order-safe. const draftReviewWave = createDraftReviewWaveState(); // The annotation-push per-activation state: ONE instance serves the `push_annotations` // installer and every priming door — the PR/stack review doors (review mode) and the // plan/objective review doors (plan mode) — per activation, never per process (the // `draftReviewWave` threading pattern). const annotations = createAnnotationState(); // The composition root's ONE per-activation report-wave instance (the `draftReviewWave` // threading pattern): the wave owns adapter selection (a fresh rpc adapter per launch over // pi's event bus) and pending execution (instance-owned refs), and is threaded into every // wave-consuming installer — no installer touches the transport tier. Plain construction, no // Pi registration, order-safe. const reportWave = createReportWave(pi.events); const conflictResolver = createConflictResolverEngine({ events: pi.events, enginePresent: () => pi.getAllTools().some((tool) => tool.name === "subagent"), readOnly: () => gating.isActive(), authorized: (request) => request.mode === "pr-rebase" ? submitConflict.authorized(request) : stackConflict.authorized(request), ...options.resolverEngine, }); const submitConflict = installSubmitConflictBindings(pi, conflictResolver, () => gating.isActive(), ); const stackConflict = createStackConflictResolver(conflictResolver, () => gating.isActive()); // The v1 plan installer: perk-owned plan mode (the `/plan` + Ctrl+Alt+P + `--plan` toggle // surface over the read-only gate, plus the plan-authoring context injection — this call // sits at the frozen hooks-ordering slot the mode surface always held), the // `plan_draft`/`plan_save` tools, the `/plan-save` + `/implement-here` commands, and // `plan_review` — perk's UNIVERSAL review door (plannotator-selected → the event-bus bridge; // ANY other selection → the first-party in-TUI editor review). Takes `gating` to toggle plan // mode and to COMPOSE the approval→save seam (auto-save → D1a gate exit) — Invariant 1 holds: // the surfaces compose the gate through the seams, never own it. The injected wave-launch // deps power the plannotator launch chooser (§8.23): the presence probe + the two door open // cores are composed HERE so plan.ts/planReview.ts import nothing from the browser modules // (planReviewBrowser.ts/objectiveReviewBrowser.ts — the value-import cycle break: // planReviewBrowser.ts value-imports the review arms). `draftReviews` is the ONE // per-activation current-review slot + unconfirmed-save latch every review surface shares // (§8.23 "Draft-review guards") — in-memory, nothing persisted. const draftReviews = createDraftReviewSlot(pi); // The composed `perk` status handle (charter D2): one slot carrying the objective segment + // the ref-counted browser-wait activity. Created once here (no hidden module state) and // threaded into the objective publisher, the plan installer (the warm plannotator arm) and // the browser doors; the footer reads it back via get/subscribe. const perkStatus = createPerkStatus(); installPlanBindings(pi, gating, draftReviews, () => runnerChild, perkStatus, { present: () => plannotatorPresent(pi), plan: (ctx, opts) => openPlanReviewSurface( pi, ctx, gating, opts, draftReviewWave, annotations, draftReviews, perkStatus, ), objective: (ctx, opts) => openObjectiveReviewSurface( pi, ctx, gating, opts, draftReviewWave, annotations, draftReviews, perkStatus, ), }); // The first 3rd-party plan adapter: a perk-owned, injection-only bridge that re-enables // `@tombell/pi-plan` as a real plan provider. Always registered, but INERT unless // `[providers] plan = "tombell-plan"`; it directs the foreign free-form prose `/plan` surface into // perk's canonical `plan_save` → `cache.plan-ref` contract. It needs no `gating` (Invariant 1: the // read-only gate stays perk's, engaged by the cold-door launch — the shim never arbitrates tools). installTombellPlanAdapter(pi, () => runnerChild); // The second 3rd-party plan adapter — AUGMENT posture: `@plannotator/pi-extension` contributes // its browser plan-review UI while perk's plan surface + gate stay (the plan installer skips // only `--plan`/`Ctrl+Alt+P` under this selection). Always registered, but INERT unless // `[providers] plan = "plannotator-plan"`. Injection-only — the `plan_review` tool lives in // the plan installer (above), which dispatches to this adapter's event-bus bridge when // plannotator is selected. installPlannotatorPlanAdapter(pi, () => runnerChild); // The v1 objective-authoring installer: the objective-author context hook pair (this call // sits at the frozen hooks-ordering slot the injection always held — keyed off (read-only // gate AND stage === objective-author); planMode defers to it), plus the // `objective_draft`/`objective_save` tools and the `/objective-save` command (registration is // name-keyed — only the hooks ordering is frozen). installObjectiveAuthoringBindings(pi, gating, draftReviews, () => runnerChild); // The v1 gist installer: the gist-authoring context hook pair (this call sits at the frozen // hooks-ordering slot the injection always held; planMode defers to it too), plus the // `gist_draft`/`gist_save` tools and the `/gist-save` command (registration is name-keyed — // only the hooks ordering is frozen). installGistBindings(pi, gating, draftReviews, () => runnerChild); // The v1 objective-refinement installer (contracts.md §8.67/§8.68): the refinement context hook // pair (keyed off (read-only gate AND stage === objective-refine); plan mode and the provider // adapters defer to it), the ONE model-facing `objective_refinement_draft` tool, the warm // `/objective-refine` entry and the human `/objective-refinement-save` failsafe. Registered // before the tool snapshots. installObjectiveRefinementBindings(pi, gating, draftReviews, () => runnerChild); let sharedOk = false; try { sharedDir(); sharedOk = true; } catch { sharedOk = false; } let registry: Registry | null = null; let registryStages = -1; try { registry = loadRegistry(); registryStages = registry.stages.length; } catch { registry = null; registryStages = -1; } const registryOk = registryStages > 0; // The generic full report-detail entry and the `perk:workflow-state` transition marker. Renderer // bodies live in surfaces.ts; registration is wiring through the pre-0.80.4-safe seam. The report // family is appended by command-attached sinks; one workflow registration covers every appender. registerTranscriptRenderer(pi, REPORT_DETAIL_TYPE, reportDetailEntryRenderer); registerTranscriptRenderer(pi, WORKFLOW_STATE_TYPE, workflowStateEntryRenderer); // The hunk watch feedback receiver controller (contracts §8.58) — factory-scoped (no module // globals). Synced from session_start/session_tree below; closed on session_shutdown so the // consumer lease releases with the session. A stale /reload predecessor instance is retired // by the lease fencing (fresh token per same-identity reacquire + verify-before-inject). const feedbackReceiver = (options.feedbackReceiverFactory ?? createHunkFeedbackReceiver)(pi); pi.on("session_shutdown", async (_event, ctx) => { submitConflict.shutdown(); stackConflict.shutdown(); await conflictResolver.shutdown(); feedbackReceiver.close(); // A browser-wait activity cannot outlive the session. perkStatus.clearActivity(ctx); }); pi.on("session_start", async (_event, ctx) => { // Read the two native-child booleans and latch the floor before lifecycle work or tool rebuilds. runnerChild = isRunnerChild(process.env); readOnlyFloor ||= decodeReadOnlyFloor(runnerChild, process.env.PI_SUBAGENT_EXTENSION_BINDINGS); submitConflict.setContext(ctx); stackConflict.setContext(ctx); const sessionFile = ctx.sessionManager.getSessionFile(); const currentSessionId = sessionFile ? basename(sessionFile) : null; // Terminal-safe linkage failure: managed headline when headful, complete stderr when headless // (plus the explicit RPC mirror); non-fatal and leaves the run unclaimed. const reportError = (message: string) => { report(ctx, "workflow-state linkage error", "error", message, { alsoLog: true }); }; // The session-audit exact-vintage stamp (§8.3), recorded by every run-identity arm // (claim/fork/adopt/mint); undefined on the perkVersion() failure sentinel, which drops the // key on serialize and leaves the session on the timestamp-estimate arm. const stamp = versionStamp(version); // The identity lifecycle (claim / fork / adopt / mint / keep) is the named session // operation (session/lifecycle.ts owns the arms); this handler binds the production ports // and renders the outcome's per-arm problems/warnings with the exact report scopes the // arms always used. The strict appends keep reporting read-back failures through the // strict-append seam's own loudness channel. The same cwd-bound handoff reader serves the // post-gate facts below (ONE handoff authority). const identityPorts = { readHandoff: (runId: string) => readHandoff(ctx.cwd, runId), listRunIds: () => listRunIds(ctx.cwd), markHandoffConsumed: (runId: string, opts: { piSessionId?: string }) => markHandoffConsumed(ctx.cwd, runId, opts), ensureRunScratch: (runId: string) => { ensureRunScratch(ctx.cwd, runId); }, mintRunId, versionStamp: stamp, }; const stateStore = branchSessionStateStore(pi, ctx); let identity = establishSessionIdentity(stateStore, identityPorts, { currentSessionId, envRunId: process.env.PERK_RUN_ID ?? null, }); if (readOnlyFloor) { const reflected = reflectSessionReadOnlyFloor(stateStore, identity); identity = reflected.outcome; if (reflected.unexpectedFailure) { report( ctx, "child restriction", "error", "could not persist child read-only restriction; in-memory restriction remains active", { alsoLog: true }, ); } } for (const problem of identity.problems) reportError(problem); for (const warning of identity.warnings) { report(ctx, "run scratch", "warning", warning, { alsoLog: true }); } const decision = identity.decision; const minted = identity.arm === "minted"; // PHASE 1 — reapply the read-only allowlist + stage scoping from the established identity // FIRST, before the fallible post-gate facts below. The scope derivation is pure (no store, // handoff, registry, or checkout read), so no read failure can leave the gate unsynced // (defense in depth on top of the total cache readers); `resolved.mode` is final once the // arms settle. Fail-closed on the gate: if the sync throws, leave it as-is (a failed sync // never opens it). const toolScope = sessionStartToolScope(identity); try { gating.syncFromState(toolScope.mode, toolScope.stage); } catch (error) { console.error(`perk: tool-gating sync failed on session_start — ${error}`); } // The refinement cold claim's ONE-TIME context import (contracts.md §8.67): only on the // actual cold claim of an `objective-refine` handoff (never keep/fork/adopt/mint), after the // identity settled and the gate synced. A refusal is loud and leaves the session gated // without a usable context (no orphan repair, no reimport on reload). if (identity.arm === "claimed" && typeof identity.resolved.run_id === "string") { importRefinementContextOnClaim(pi, ctx, { runId: identity.resolved.run_id, stage: identity.resolved.stage, }); } // PHASE 2 — the post-gate facts (session/lifecycle.ts owns the decision tree): the lazy, // stage-gated `cache.plan-ref` → `active_plan_ref` reconciliation (only a launched stage // that *consumes* the ref reads the checkout; claim/keep read the handoff, fork/adopt/none // never do; one strict verified append, idempotent by (provider, pr_id)), plus the derived // implementation-capture and receiver inputs. Called HERE — after the gate and the // refinement import — never while constructing gate inputs. A throwing branch/handoff read // propagates to Pi's hook error boundary with the gate already synced: unreadability is // never turned into confirmed absence, and no later effect runs from guessed facts. const facts = resolveSessionStartFacts( stateStore, { readHandoff: identityPorts.readHandoff, readPlanRef: () => readPlanRef(ctx.cwd) }, { identity, registry, currentSessionId }, ); const resolved: WorkflowState = facts.resolved; // Implementation session pointer (contracts.md §8.35): an implement session self-keys its own // session file into the shared main checkout so a later/other session resolves it cross-run. // The headless worker's inner session lands here too (.main); runStage records the matching // .worker. A forked implement session inherits the parent's launched stage + threads the // inherited parent session id as fork provenance. Best-effort + non-fatal (carrier warns). // First-write-wins (`preserveForeign`): this is the corroborated shadowing defect site — the // claimer's original capture stays authoritative, and any future shadow vector warns loudly // instead of silently corrupting /learn evidence. if (facts.implementationCapture !== null) { captureSessionPointer({ cwd: ctx.cwd, runId: facts.implementationCapture.runId, klass: "implementation", site: "main", sessionFile, parentSessionId: facts.implementationCapture.parentSessionId, preserveForeign: true, }); } // The hunk watch feedback receiver (§8.58): sync strictly AFTER the run-identity claim and // the plan-ref reconciliation above, so an unclaimed or mislinked session never touches the // outbox. Eligibility (interactive TUI + implement stage + non-adopted + settled identity + // plan-ref match against one fresh cache read) is evaluated inside sync; every ineligible // shape closes any open inbox. Never throws (the controller contains its own failures). feedbackReceiver.sync(ctx, { ...facts.feedback, mode: ctx.mode ?? null }); // Soft version-parity drift signal: pi can lazy-install / load a stale `npm:@mgiles/perk`, so the // extension actually running may differ from the `perk` CLI that launched it. The local launch // seam injects PERK_CLI_VERSION; compare it against this extension's own `perkVersion()`. Soft + // non-fatal (warning), headless-safe via report(). No once-guard — may re-emit on reload, fine // for a soft warning. Silent for ad-hoc `pi` (no env) and the self-repo (versions equal). const cliVersion = (process.env.PERK_CLI_VERSION ?? "").trim(); if (cliVersion && version && cliVersion !== version) { report( ctx, "version parity", "warning", `the loaded @mgiles/perk extension (v${version}) differs from the running perk CLI ` + `(v${cliVersion}) — run 'perk doctor --fix' to reinstall the pinned version`, { alsoLog: true }, ); } // Charter D7: perk identity is standing footer state, not a transition — the // `v loaded` toast (and its headless stderr mirror) is retired. D5 is rescinded: // perk keeps pi's default working indicator (no setWorkingIndicator call anywhere). // Install on EVERY headful session_start: pi ≥ 0.84's `setExtensionFooter` explicitly // disposes a replaced footer factory (verified at 0.84.1), and `resetExtensionUI` restores // the built-in footer on /reload and before session replacement — both paths also re-run // this extension factory, so repeated installs leak nothing and each install's deps // closures capture the current event's ctx. // Footer-seam install-site vacating: under a foreign `[providers] footer` selection perk does // NOT install its own footer, leaving the foreign footer (`pi-powerline-footer` / `pi-bar`) as // the sole footer surface. perk's objective progress still reaches it via the // single-value `perk` setStatus slot. Fail-safe: any config-read error resolves to install. if (ctx.hasUI && isPerkFooterReferenceSelected(ctx.cwd)) { installPerkFooter(ctx, { identity: `perk v${version}`, status: perkStatus, getModelId: () => ctx.model?.id ?? null, getThinkingLevel: () => (ctx.model ? pi.getThinkingLevel() : null), getCacheHitRate: () => latestCacheHitRate(ctx.sessionManager.getEntries()), getContext: () => { const usage = ctx.getContextUsage(); return usage ? { percent: usage.percent, contextWindow: usage.contextWindow } : null; }, }); } if (process.env.PERK_SELFCHECK) { try { const dir = workflowDir(ctx.cwd); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); // The gate sentinel (unchanged — those gates parse this line). atomicWriteFileSync( join(dir, ".perk-loaded"), `perk ${version} loaded; shared=${sharedOk ? "ok" : "miss"}; ` + `registry=${registryOk ? "ok" : "miss"} stages=${registryStages}; hasUI=${ctx.hasUI}\n`, ); writeT3Sentinel(ctx.cwd, minted ? "mint" : decision.source, resolved, ctx.mode ?? null); setMarker(ctx.cwd, T3_MARKER); // cross-plane cache write (gate check 3) } catch { // never throw from a load probe } } }); // Non-negotiable: rebuild on branch navigation too, or state goes stale after /tree (§8.3). pi.on("session_tree", async (_event, ctx) => { stackConflict.setContext(ctx); // ONE fresh full-branch rebuild; the navigation facts derive purely from it (no handoff/ // checkout read, claim, linkage, or capture on navigation — session/lifecycle.ts owns the // asymmetry with startup). const state = rebuildWorkflowState(branchOf(ctx)); const facts = sessionTreeFacts(state); // Non-negotiable: re-sync the gate + stage scoping on tree navigation too (mode and stage are // per-field LWW — the branch-rebuilt stage is the §8.40 key). Fail-closed on the gate. try { gating.syncFromState(facts.toolScope.mode, facts.toolScope.stage); } catch (error) { console.error(`perk: tool-gating sync failed on session_tree — ${error}`); } // Re-sync the feedback receiver from the LWW-rebuilt state (§8.58) — gate first, then // receiver; `adopted: false` is right here: an env-adopted child's fresh branch carries no // stage, so the stage gate alone keeps it inert on tree navigation. feedbackReceiver.sync(ctx, { ...facts.feedback, mode: ctx.mode ?? null }); if (process.env.PERK_SELFCHECK) { writeT3Sentinel(ctx.cwd, "tree", state, ctx.mode ?? null); } }); // Lifecycle gates: the dirty-repo switch/fork guard + the guard-only `/implement`. registerLifecycleGates(pi); // Warm door: the `submit` tool + `/submit` command. installSubmitBindings(pi, submitConflict); // The warm ready + handoff bindings: the deliberate draft→ready review gate (submit keeps // draft). Takes `gating`: the warm ready→reconcile continuation refuses (loudly) to drive // the ready-time pass into a read-only session (contracts.md §8.66). installReadyBindings(pi, gating); // Warm bindings: `land` merges + sets pending-learn; the v1 learn installer (the `learn` + // `run_learn_wave` tools and the `/learn` command over the `learning/` feature ops) clears it. installLandBindings(pi); installLearnBindings(pi, reportWave); // The warm stacked-delivery mutating surface (§8.51): the `/objective-sync`/ // `/objective-recover`/`/objective-land` drives + the four typed stack tools over the // `delivery/stackConflict.ts` + `delivery/stackReconcile.ts` feature ops. Takes `gating` for // the driving commands' gate-on soft refusal (stack sync/recovery mutates published // branches; the stack tools never join READ_ONLY_TOOLS). installStackSyncBindings(pi, gating, stackConflict, options.stackResolutionDelivery); installStackRecoverBindings(pi, gating); installStackLandBindings(pi, gating); // The stacked-delivery status read: the `objective_stack_status` tool + the `/objective-stack` // command (read-only end to end — the command works gate-on; the tool stays gate-off). installStackStatusBindings(pi); // The warm `/address` review loop: the submit-then-resolve `finalize_address` tool + `/address` // command. Classify-then-act (the verbose feedback fetch + classification runs in an isolated // spawned child; the parent fixes actionable items and finalizes the committed repairs). installAddressBindings(pi, reportWave, submitConflict); // The warm `/pr-review` door: automated code review in a FRESH, isolated subagent that // POSTS its review to the PR (the deliberate departure from /address's read-only-child rule). installAutomatedReviewBindings(pi, reportWave); // The warm `submit_pr_review` tool: the human-gated curated-posting surface both review // doors ride (contracts §8.4) — neither door registers tools of its own. installCuratedSubmissionBindings(pi); // The flow-scoped review-wave pair (`start_review_wave`/`collect_review_wave`) both human // review doors drive: non-blocking adversarial-review launch + the typed collect, flow-scoped // via the session's pending-wave guard. installReviewWaveBindings(pi, reportWave); installAuditBindings(pi, reportWave); installHarvestBindings(pi, reportWave); installDreamBindings(pi, reportWave); // The scout launcher (`run_scout_wave`, contracts.md §8.70): the authoring sessions' blocking // fan-out of self-contained read-only briefs onto fresh `perk.scout` lanes — one attempt, // no retry, reachable in every gated stage except refinement. installScoutWaveBindings(pi, reportWave); // The flow-scoped draft-review-wave pair (`start_draft_review_wave`/ // `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review // launch over the door-primed context + the typed collect. registerDraftReviewWaveTools(pi, draftReviewWave, reportWave); // The door-primed browser annotation tool (`push_annotations`): the browser door primes the // surface handle on open and clears it on settle/degrade — the tool refuses outside a // door-opened flow. installAnnotationBindings(pi, annotations); // The warm `/pr-review-terminal` door: the terminal review entry — hunk always, no provider // dispatch (the command IS the selection); posting rides `submit_pr_review` above. installPrReviewTerminalBindings(pi); // The warm `/pr-review-browser` door: the browser review entry — plannotator always, opened // in the background (pre-PR it absorbs the since-base local browser review); posting is the // human's own platform-post from the UI, with `submit_pr_review` for request-changes only. installPrReviewBrowserBindings(pi, annotations, perkStatus); // The warm `/stack-review-browser` door + its cold-launch twin (`open_stack_review`): the // stacked-PR browser review over the combined base→top diff — one reviewer wave with // `stack: true`, then judgment-routed per-PR posting through `submit_pr_review`. installStackReviewBindings(pi, annotations, perkStatus); // The warm `/plan-review-browser` door: the summonable streaming draft review — the // plannotator plan-review browser on the working plan draft, draft reviewers streaming // phrase-anchored findings in; APPROVE auto-saves via the approvalSave seam, DENY returns a // model-mediated revision round. registerPlanReviewBrowser(pi, gating, draftReviewWave, annotations, draftReviews, perkStatus); // The warm `/objective-review-browser` door: the summonable streaming objective-draft review // — the plannotator plan-review browser on the RENDERED working objective draft, draft // reviewers streaming phrase-anchored findings in; APPROVE auto-saves via the // objectiveApprovalSave seam, Direct Edits = a model-mediated revise round (never auto-saved). registerObjectiveReviewBrowser( pi, gating, draftReviewWave, annotations, draftReviews, perkStatus, ); // The read-only CI executor: the `run_ci` tool + `/ci` command + `--allow-project-ci` // flag. Runs the project's `[ci]` named checks deterministically and reports (never fixes/loops). installCiBindings(pi); // The objective substrate: `/objective` set/clear, budget accounting, threshold // compaction, all keyed off the now-live `active_objective`. Inert when no objective is active. // (The deterministic objective mechanics live in the Python plane: `perk objective …`.) installObjectiveBindings(pi, perkStatus); // The warm `/commit-and-compact` utility door: drive a commit of the work so far, compact once // a successful outcome is known, then completion-gate an automatic evidence-first continuation // (clean/read-only trees compact immediately; no commit → no compaction or continuation). // Human-only — no tool twin. installCommitCompactBindings(pi, gating); // The objective plan factory's warm transition surface: the `objective_node` bounded // tool (delegates to the Python cold door; `status:"done"` requires a completion audit) + the // `/objective-plan` command (select the next node and author a bounded plan). The command now // enters the read-only gate on invocation (parity with the cold door's `mode: read-only` // handoff; exit stays with plan_save / `/plan` off) — hence `gating`. installObjectivePlanningBindings(pi, gating, reportWave); // The two learn plan factories' warm surfaces: `/learn-docs` gathers open perk:learn issues // (via the `perk learn docs --gather` cold door) toward a docs/learned consolidation plan; // `/learn-code` gathers pre-stamped SHOULD_BE_CODE issues (via `perk learn code --gather`) // toward a code-routing plan. Guidance-injection only (no model tool). installLearnFactoryBindings(pi); // Warm-door skill-binding delivery: Mechanism A's `before_agent_start` injection of // the launched stage's user-originated bindings (+ the stale-context strip). Mechanism B (the // `command:` suffix) is wired into the `/objective-reconcile` + `/learn-docs` + // `/learn-code` guidance. registerBindingDelivery(pi); // `/perk-selfcheck` — the session-wiring verifier (turned from a liveness ping into a real check // that the converged ambient index reached `appendSystemPrompt` and the managed `AGENTS.md` block // reached `contextFiles`). doctor checks disk; selfcheck checks the prompt. registerSelfcheck(pi, { version, sharedOk }); }