one-code-extension 0.5.0 → 0.6.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/docs/guide/differences-from-claude-code.md +9 -6
- package/docs/guide/installation.md +3 -4
- package/docs/guide/troubleshooting.md +1 -1
- package/extensions/auto-mode/denials.ts +3 -2
- package/extensions/background/cron.ts +127 -0
- package/extensions/background/index.ts +135 -47
- package/extensions/background/loop-fire.ts +39 -2
- package/extensions/background/switch-guard.ts +10 -6
- package/extensions/background/wakeup.ts +21 -2
- package/extensions/bash/index.ts +8 -30
- package/extensions/btw/index.ts +32 -18
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-context/index.ts +157 -172
- package/extensions/compaction/index.ts +11 -8
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +15 -14
- package/extensions/context-management/index.ts +11 -28
- package/extensions/effort/index.ts +70 -7
- package/extensions/file-tracker/index.ts +111 -44
- package/extensions/file-tracker/replay.ts +76 -49
- package/extensions/file-tracker/restore.ts +12 -9
- package/extensions/file-tracker/tracker.ts +14 -1
- package/extensions/hooks/index.ts +60 -76
- package/extensions/hooks/prompt-context.ts +59 -0
- package/extensions/hooks/trust.ts +13 -7
- package/extensions/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +17 -1
- package/extensions/lib/agent-loader.ts +3 -2
- package/extensions/lib/anthropic-auth.ts +98 -0
- package/extensions/lib/child-extensions.ts +14 -4
- package/extensions/lib/context-facts.ts +75 -56
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +152 -12
- package/extensions/lib/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +24 -6
- package/extensions/lib/model-catalog/huggingface.json +5 -1
- package/extensions/lib/model-catalog/models-dev.json +10535 -10299
- package/extensions/lib/model-catalog/openrouter.json +191 -393
- package/extensions/lib/notifications.ts +57 -17
- package/extensions/lib/pi-version.ts +25 -8
- package/extensions/lib/plan-mode-channels.ts +29 -7
- package/extensions/lib/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +3 -2
- package/extensions/lib/reminders.ts +327 -621
- package/extensions/lib/request-replay.ts +186 -78
- package/extensions/lib/side-call.ts +11 -0
- package/extensions/lib/subagent-handback.ts +0 -1
- package/extensions/lib/tool-additions.ts +2 -22
- package/extensions/lib/turn-budget-layout.ts +18 -2
- package/extensions/lsp/index.ts +6 -8
- package/extensions/mcp/announce.ts +63 -98
- package/extensions/mcp/index.ts +51 -90
- package/extensions/mcp/trust.ts +13 -6
- package/extensions/model-default/index.ts +86 -19
- package/extensions/permissions/index.ts +157 -48
- package/extensions/permissions/modes.ts +5 -0
- package/extensions/plan-mode/index.ts +142 -45
- package/extensions/plan-mode/run-state.ts +26 -0
- package/extensions/plugins/index.ts +4 -1
- package/extensions/powershell/index.ts +2 -0
- package/extensions/recap/index.ts +11 -14
- package/extensions/search-tools/index.ts +2 -0
- package/extensions/skill/announce.ts +30 -0
- package/extensions/skill/index.ts +65 -48
- package/extensions/skill/invoke.ts +0 -67
- package/extensions/subagents/agents.ts +30 -8
- package/extensions/subagents/fork-cache.ts +9 -3
- package/extensions/subagents/index.ts +180 -107
- package/extensions/subagents/outcome.ts +3 -0
- package/extensions/subagents/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +29 -2
- package/extensions/subagents/runs.ts +5 -0
- package/extensions/system-prompt/index.ts +40 -6
- package/extensions/system-prompt/template.ts +10 -1
- package/extensions/system-reminder/index.ts +174 -202
- package/extensions/tool-search/index.ts +241 -167
- package/extensions/workflow/agent-session.ts +4 -1
- package/extensions/workflow/index.ts +16 -4
- package/extensions/workflow/run-manager.ts +4 -0
- package/extensions/worktree/index.ts +50 -14
- package/package.json +5 -2
- package/extensions/lib/context-stack.ts +0 -81
- package/extensions/subagents/announce.ts +0 -69
- package/extensions/tool-search/announce.ts +0 -58
|
@@ -77,7 +77,9 @@ import { canShowCustomUi, rpcReadOnlyText } from "../lib/headless-output.ts";
|
|
|
77
77
|
import { sessionResultsDir } from "../lib/persisted-output.ts";
|
|
78
78
|
import { privateSessionScratchpadDir } from "../lib/scratchpad.ts";
|
|
79
79
|
import { CONTEXT_ORDER, REMINDER_CHANNEL, type ReminderPayload } from "../lib/reminders.ts";
|
|
80
|
-
import {
|
|
80
|
+
import { readState, readStringsState, recordState } from "../lib/ledger/state.ts";
|
|
81
|
+
import { processState } from "../lib/process-state.ts";
|
|
82
|
+
import { type EntryLike, readRecord } from "../lib/ledger/records.ts";
|
|
81
83
|
import {
|
|
82
84
|
decide,
|
|
83
85
|
extractSubject,
|
|
@@ -89,7 +91,7 @@ import {
|
|
|
89
91
|
isPathSubjectTool,
|
|
90
92
|
} from "./matcher.ts";
|
|
91
93
|
import { type SessionGrant, sessionGrant } from "./session-grant.ts";
|
|
92
|
-
import { formatModel, modeBadge, nextMode, PERMISSION_STATUS_CHANNEL, type PermissionStatus, CYCLE_KEY } from "./modes.ts";
|
|
94
|
+
import { formatModel, modeBadge, nextMode, PERMISSION_MODE_STATE, PERMISSION_STATUS_CHANNEL, type PermissionStatus, CYCLE_KEY } from "./modes.ts";
|
|
93
95
|
import { intrinsicTier, usesClaudeCodeFastPaths } from "../lib/model-tier.ts";
|
|
94
96
|
import { type ChildToolCall, type ChildGateDecision, sessionPermissionBridge, SUBAGENT_GATE_CHANNEL } from "./subagent-gate.ts";
|
|
95
97
|
import { trackOriginalCommands } from "../lib/original-command.ts";
|
|
@@ -264,6 +266,24 @@ const denyReason = (result: { cause?: string; rule?: { raw?: string } }): string
|
|
|
264
266
|
* here, next to the emitter it belongs to.
|
|
265
267
|
*/
|
|
266
268
|
const STANDING_REMINDER_MODES: ReadonlySet<PermissionMode> = new Set<PermissionMode>(["plan", "auto"]);
|
|
269
|
+
/** The `one-code:state` key for the directories added to the workspace for this session. */
|
|
270
|
+
const WORKSPACE_DIRS_STATE = "workspace-dirs";
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* What the user granted "for this session" and what auto mode's loop-breaker
|
|
274
|
+
* counted. This is process state (lib/process-state.ts): pi re-runs this
|
|
275
|
+
* factory at a /reload, which is the same conversation and keeps them
|
|
276
|
+
* (decisions/modes.md), and every other session start clears them. Only the
|
|
277
|
+
* main session loads this extension (children reach it through the bridge),
|
|
278
|
+
* so one slot per process is one per conversation.
|
|
279
|
+
*/
|
|
280
|
+
interface SessionGate {
|
|
281
|
+
/** Rules minted by "Yes, and don't ask again … this session" (session-grant.ts). */
|
|
282
|
+
allows: PermissionRule[];
|
|
283
|
+
pause: PauseTracker;
|
|
284
|
+
/** Classifier denials, and the one-shot grants `/permissions` mints from them. */
|
|
285
|
+
denials: DenialStore;
|
|
286
|
+
}
|
|
267
287
|
|
|
268
288
|
const YES = "Yes";
|
|
269
289
|
const NO = "No, tell the agent what to do differently";
|
|
@@ -327,8 +347,10 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
327
347
|
let bypassInCycle = false;
|
|
328
348
|
/** Whether auto mode is a stop on the cycle — only when a classifier model is reachable. */
|
|
329
349
|
let autoInCycle = false;
|
|
330
|
-
/**
|
|
331
|
-
let
|
|
350
|
+
/** The mode the branch's latest `one-code:state` entry holds: the mode follows the branch. */
|
|
351
|
+
let branchMode: PermissionMode | undefined;
|
|
352
|
+
/** Whether a request went out on this branch: only then was its model told the branch's mode. */
|
|
353
|
+
let requestOnBranch = false;
|
|
332
354
|
let deny: PermissionRule[] = [];
|
|
333
355
|
let ask: PermissionRule[] = [];
|
|
334
356
|
let allow: PermissionRule[] = [];
|
|
@@ -346,9 +368,10 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
346
368
|
* Rules minted by "Yes, and don't ask again … this session" (session-grant.ts).
|
|
347
369
|
* Never applied in auto mode: there the classifier is the boundary and a
|
|
348
370
|
* standing grant would be a bypass of it (PERMISSIONS-REVIEW-2026-09-05 M2).
|
|
349
|
-
* Cleared with the session (L1).
|
|
371
|
+
* Cleared with the session (L1), kept by a reload (SessionGate).
|
|
350
372
|
*/
|
|
351
|
-
const
|
|
373
|
+
const sessionGate = processState<SessionGate>("permissions:session-gate", () => ({ allows: [], pause: new PauseTracker(), denials: new DenialStore() }));
|
|
374
|
+
const sessionAllows = sessionGate.allows;
|
|
352
375
|
const activeSessionAllows = () => (mode === "auto" ? [] : sessionAllows);
|
|
353
376
|
let unparsableRules: string[] = [];
|
|
354
377
|
let warnedUnparsable = "";
|
|
@@ -391,6 +414,11 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
391
414
|
*/
|
|
392
415
|
let settingsWorkspaceDirs: SourcedDirectory[] = [];
|
|
393
416
|
let flagWorkspaceDirs: string[] = [];
|
|
417
|
+
/**
|
|
418
|
+
* Added for this session. This is branch state: the /add-dir breadcrumb told
|
|
419
|
+
* the branch's model they are in its workspace. Recorded on every change and
|
|
420
|
+
* read back, validated again, at every session start and /tree switch.
|
|
421
|
+
*/
|
|
394
422
|
const sessionWorkspaceDirs: string[] = [];
|
|
395
423
|
/** The workspace directories in force, as real paths: what the user and the system prompt see. */
|
|
396
424
|
let workspacePaths: string[] = [];
|
|
@@ -405,6 +433,36 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
405
433
|
workspacePaths = [...new Set([...trusted, ...flagWorkspaceDirs, ...sessionWorkspaceDirs].map((dir) => tryRealpath(dir) ?? dir))];
|
|
406
434
|
workspaceDirs = [...new Set(workspacePaths.map(resolvedOrSelf))];
|
|
407
435
|
readableRoots = [...harnessReadableRoots, ...docsDirPaths, ...workspaceDirs];
|
|
436
|
+
if (sessionStarted) announceWorkspace();
|
|
437
|
+
};
|
|
438
|
+
/** What the system prompt was last told the workspace is (lib/workspace-channel.ts). */
|
|
439
|
+
let announcedWorkspace: string | undefined;
|
|
440
|
+
let sessionStarted = false;
|
|
441
|
+
/**
|
|
442
|
+
* The environment block lists the workspace: a change mid-session reaches
|
|
443
|
+
* the next epoch's message 1 (a compaction), while inside the epoch the
|
|
444
|
+
* /add-dir output already told the model (R3-L4).
|
|
445
|
+
*/
|
|
446
|
+
const announceWorkspace = () => {
|
|
447
|
+
const key = workspacePaths.join("\0");
|
|
448
|
+
if (key === announcedWorkspace) return;
|
|
449
|
+
announcedWorkspace = key;
|
|
450
|
+
pi.events.emit(WORKSPACE_CHANNEL, { dirs: workspacePaths } satisfies WorkspaceAnnouncement);
|
|
451
|
+
};
|
|
452
|
+
const recordSessionWorkspace = () => recordState(pi, WORKSPACE_DIRS_STATE, [...sessionWorkspaceDirs]);
|
|
453
|
+
/**
|
|
454
|
+
* Take the branch's session directories. Each is validated again, as a live
|
|
455
|
+
* /add-dir is: the stored list says only that the user once added it, so a
|
|
456
|
+
* directory that is gone, or one too broad for a workspace, is not applied.
|
|
457
|
+
*/
|
|
458
|
+
const restoreSessionWorkspace = (ctx: ExtensionContext) => {
|
|
459
|
+
const restored: string[] = [];
|
|
460
|
+
for (const dir of readStringsState(ctx, WORKSPACE_DIRS_STATE)) {
|
|
461
|
+
const checked = validateWorkspaceDirectory(dir, ctx.cwd, os.homedir(), restored);
|
|
462
|
+
if ("path" in checked) restored.push(checked.path);
|
|
463
|
+
else if (ctx.hasUI) ctx.ui.notify(`${dir}, added earlier in this conversation, is no longer in the workspace: ${checked.error}`, "warning");
|
|
464
|
+
}
|
|
465
|
+
sessionWorkspaceDirs.splice(0, sessionWorkspaceDirs.length, ...restored);
|
|
408
466
|
};
|
|
409
467
|
|
|
410
468
|
/**
|
|
@@ -467,6 +525,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
467
525
|
paused: pauseTracker.isPaused(),
|
|
468
526
|
classifier: display.classifier,
|
|
469
527
|
pinned: display.pinned,
|
|
528
|
+
bypassAvailable: bypassInCycle,
|
|
470
529
|
...(toolCallId ? { toolCallId } : {}),
|
|
471
530
|
} satisfies PermissionStatus);
|
|
472
531
|
};
|
|
@@ -501,7 +560,9 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
501
560
|
};
|
|
502
561
|
// Keyed by cwd: a worktree-isolated child classifies against ITS checkout's
|
|
503
562
|
// CLAUDE.md/AGENTS.md, and must not poison the cache the main agent's own
|
|
504
|
-
// calls (a different cwd) read for the rest of the session.
|
|
563
|
+
// calls (a different cwd) read for the rest of the session. The main
|
|
564
|
+
// session's calls read the entered worktree's. Dropped where the agent's own
|
|
565
|
+
// instructions are read again: a session start and a compaction.
|
|
505
566
|
const projectInstructionsByCwd = new Map<string, string | undefined>();
|
|
506
567
|
const instructionsFor = (cwd: string): string | undefined => {
|
|
507
568
|
if (!projectInstructionsByCwd.has(cwd)) {
|
|
@@ -509,9 +570,8 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
509
570
|
}
|
|
510
571
|
return projectInstructionsByCwd.get(cwd);
|
|
511
572
|
};
|
|
512
|
-
const pauseTracker =
|
|
513
|
-
|
|
514
|
-
const denials = new DenialStore();
|
|
573
|
+
const pauseTracker = sessionGate.pause;
|
|
574
|
+
const denials = sessionGate.denials;
|
|
515
575
|
/**
|
|
516
576
|
* Which model the classifier settled on. Held here so the choice is pinned for
|
|
517
577
|
* the session rather than re-resolved per call, and so a model that turns out
|
|
@@ -794,6 +854,8 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
794
854
|
/** `startup`: the session opens in this mode, so there is no switch to announce, only the mode's standing block. */
|
|
795
855
|
const setMode = (next: PermissionMode, startup = false, toolCallId?: string) => {
|
|
796
856
|
const changed = mode !== next;
|
|
857
|
+
/** The mode the branch recorded before this switch; its model was told it once a request went out. */
|
|
858
|
+
const told = branchMode;
|
|
797
859
|
mode = next;
|
|
798
860
|
process.env[MODE_ENV] = next;
|
|
799
861
|
// Close the standing block of the mode being left BEFORE the status
|
|
@@ -803,11 +865,15 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
803
865
|
// becomes plan — removing the key afterwards would undo that re-add and
|
|
804
866
|
// leave a turn entered mid-stream with no plan reminder until the next
|
|
805
867
|
// prompt (STEERING-REVIEW-2026-09-05 M3). Auto installs its own block below.
|
|
806
|
-
|
|
807
|
-
if (mode !== "auto" && !(mode === "plan" && (startup ? previousMode === "plan" : !changed))) {
|
|
868
|
+
if (mode !== "auto" && !(mode === "plan" && (startup ? branchMode === "plan" : !changed))) {
|
|
808
869
|
pi.events.emit(REMINDER_CHANNEL, { remove: true, key: "permission-mode" });
|
|
809
870
|
}
|
|
810
|
-
|
|
871
|
+
// The mode follows the branch: recorded on every change, so a resume, a
|
|
872
|
+
// fork, /reload or a /tree switch starts from it (decisions/modes.md).
|
|
873
|
+
if (mode !== branchMode) {
|
|
874
|
+
recordState(pi, PERMISSION_MODE_STATE, mode);
|
|
875
|
+
branchMode = mode;
|
|
876
|
+
}
|
|
811
877
|
applyBadge(undefined, toolCallId);
|
|
812
878
|
// Every switch is announced once on the tail of the next request (the next
|
|
813
879
|
// tool result mid-turn, the prompt between turns), so the model learns of
|
|
@@ -822,8 +888,9 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
822
888
|
: "";
|
|
823
889
|
// Not at startup: "is now" beside the mode's own standing block read as a
|
|
824
890
|
// duplicate to both GPT-6 models in the 2026-10-04 self-test, and Claude
|
|
825
|
-
// Code announces only a switch.
|
|
826
|
-
|
|
891
|
+
// Code announces only a switch. A startup that leaves the mode the branch
|
|
892
|
+
// recorded (a flag, a stored bypass that needs its flag again) is one.
|
|
893
|
+
if (!startup || (told !== undefined && told !== mode && requestOnBranch)) {
|
|
827
894
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
828
895
|
text: `The user's permission mode is now "${mode}".${planNote}`,
|
|
829
896
|
key: "permission-mode-change",
|
|
@@ -836,7 +903,6 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
836
903
|
"Auto mode is active: your tool calls run without per-action prompts, but each one is screened by an approval classifier that blocks anything irreversible, destructive, or aimed outside this environment. " +
|
|
837
904
|
"Work normally — do not narrate the classifier or try to phrase calls to get past it. If a call is blocked, take the block at face value: do not retry it, and do not reach the same result another way. Continue with work that does not depend on it, or explain what you were trying to do and let the user decide. " +
|
|
838
905
|
"Attempting to weaken this gate (editing permission settings, changing the mode, or routing work around it) is itself blocked.",
|
|
839
|
-
scope: "every-turn",
|
|
840
906
|
key: "permission-mode",
|
|
841
907
|
// Session state: rides every user message since auto mode came on, so
|
|
842
908
|
// the cached prefix holds turn to turn (lib/reminders.ts).
|
|
@@ -896,7 +962,11 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
896
962
|
return settings;
|
|
897
963
|
};
|
|
898
964
|
|
|
899
|
-
|
|
965
|
+
/**
|
|
966
|
+
* `launching`: the process is starting (reason "startup"), when its flags
|
|
967
|
+
* pick the mode. `carried`: the mode a /clear or /new carries onto its empty branch.
|
|
968
|
+
*/
|
|
969
|
+
const reloadSettings = (ctx: ExtensionContext, launching: boolean, carried: PermissionMode | undefined) => {
|
|
900
970
|
const settings = reloadRules(ctx);
|
|
901
971
|
projectAllowDeclined = false;
|
|
902
972
|
// Dropped so edited autoMode rules and instruction files are picked up on
|
|
@@ -907,14 +977,15 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
907
977
|
// Claude Code's resolution order (flags, then settings' defaultMode), with
|
|
908
978
|
// bypassPermissions refused wherever a settings source disabled it — before
|
|
909
979
|
// 2026-09-05 (H3) that policy was silently not in force here.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
980
|
+
// The mode follows the branch: at launch the flags win over it, and after
|
|
981
|
+
// that (/reload, /resume, /fork, /clear in the same process) the branch's
|
|
982
|
+
// own mode wins over the launch flags, so a switch the user made is kept.
|
|
983
|
+
// A stored bypassPermissions is never restored by itself: it needs its flag.
|
|
984
|
+
const bypassFlag = pi.getFlag("dangerously-skip-permissions") === true ? ("bypassPermissions" as const) : undefined;
|
|
985
|
+
const modeFlag = normalizePermissionMode(pi.getFlag("permission-mode"));
|
|
986
|
+
const fromBranch = branchMode ?? carried;
|
|
987
|
+
const restored = fromBranch === "bypassPermissions" ? undefined : fromBranch;
|
|
988
|
+
const startup = resolveStartupMode(launching ? [bypassFlag, modeFlag, restored, settings.defaultMode] : [restored, bypassFlag, modeFlag, settings.defaultMode], settings);
|
|
918
989
|
if (startup.mode) mode = startup.mode;
|
|
919
990
|
if (startup.bypassRefused && ctx.hasUI) ctx.ui.notify("Bypass permissions mode was disabled by settings", "warning");
|
|
920
991
|
bypassInCycle = mode === "bypassPermissions";
|
|
@@ -934,16 +1005,17 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
934
1005
|
};
|
|
935
1006
|
|
|
936
1007
|
// Claude Code's auto-mode note joins the mid-conversation system message
|
|
937
|
-
// when the conversation opens in auto mode
|
|
938
|
-
//
|
|
1008
|
+
// when the conversation opens in auto mode (auto-mode/system-note.ts). It is
|
|
1009
|
+
// live state, rendered only by a request that opens an epoch, so message 1
|
|
1010
|
+
// never changes inside one; a compaction or a model switch decides it again.
|
|
939
1011
|
pi.on("turn_start", (_event, ctx) => {
|
|
940
|
-
if (autoNoteDecided) return;
|
|
941
|
-
autoNoteDecided = true;
|
|
942
1012
|
const note = mode === "auto" && ctx.model ? autoModeSystemNote(ctx.model) : undefined;
|
|
943
|
-
if (!note)
|
|
1013
|
+
if (!note) {
|
|
1014
|
+
pi.events.emit(REMINDER_CHANNEL, { remove: true, key: "auto-mode-note" });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
944
1017
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
945
1018
|
text: note,
|
|
946
|
-
scope: "every-turn",
|
|
947
1019
|
key: "auto-mode-note",
|
|
948
1020
|
placement: "first-prepend",
|
|
949
1021
|
order: CONTEXT_ORDER.autoModeNote,
|
|
@@ -979,18 +1051,21 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
979
1051
|
});
|
|
980
1052
|
// A new session (`/clear`, `/resume`, a fork) starts with a clean gate: the
|
|
981
1053
|
// previous conversation's user messages are not intent evidence for this
|
|
982
|
-
// one, its "don't ask again" grants and
|
|
983
|
-
// (PERMISSIONS-REVIEW-2026-09-05 L1). A reload keeps
|
|
1054
|
+
// one, and its "don't ask again" grants, approved denials and pause state
|
|
1055
|
+
// do not carry over (PERMISSIONS-REVIEW-2026-09-05 L1). A reload keeps
|
|
1056
|
+
// them, the same conversation: they are process state (SessionGate),
|
|
1057
|
+
// which the factory pi re-runs there does not reset.
|
|
984
1058
|
if (event.reason !== "reload") {
|
|
985
|
-
autoNoteDecided = false;
|
|
986
1059
|
sessionAllows.length = 0;
|
|
987
1060
|
currentMainToolCallId = undefined;
|
|
988
1061
|
inFlightMainCalls = [];
|
|
989
1062
|
pendingInputs.length = 0;
|
|
990
1063
|
pauseTracker.reset();
|
|
991
1064
|
denials.reset();
|
|
992
|
-
sessionWorkspaceDirs.length = 0;
|
|
993
1065
|
}
|
|
1066
|
+
// The directories this branch was told it added follow it, so a reload, a
|
|
1067
|
+
// resume and a fork keep them and a /clear starts without.
|
|
1068
|
+
restoreSessionWorkspace(ctx);
|
|
994
1069
|
// With auto-memory off the folder is nobody's working space: no grant.
|
|
995
1070
|
memoryDirPath = autoMemoryEnabled(ctx.cwd) ? projectMemoryDir(ctx.cwd) : undefined;
|
|
996
1071
|
// Working space only when it is private to this user: another local user
|
|
@@ -1012,21 +1087,43 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1012
1087
|
oneCodeProjectSettingsFile = oneCodeProjectSettingsPath(ctx.cwd, os.homedir());
|
|
1013
1088
|
protectedDirs = runtimeProtectedDirs();
|
|
1014
1089
|
secretPaths = runtimeSecretPaths(os.homedir());
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
pi.events.emit(CONTEXT_BASELINE_CHANNEL, { key: "permission-mode", value: mode });
|
|
1090
|
+
branchMode = normalizePermissionMode(readState(ctx, PERMISSION_MODE_STATE));
|
|
1091
|
+
requestOnBranch = (ctx.sessionManager.getBranch() as EntryLike[]).some((entry) => readRecord(entry)?.kind === "request");
|
|
1092
|
+
// Claude Code keeps the mode across /clear: the new branch starts from the
|
|
1093
|
+
// mode this process last published (MODE_ENV), and records it.
|
|
1094
|
+
const carried = event.reason === "new" && !branchMode ? normalizePermissionMode(process.env[MODE_ENV]) : undefined;
|
|
1095
|
+
reloadSettings(ctx, event.reason === "startup", carried);
|
|
1096
|
+
if (!STANDING_REMINDER_MODES.has(mode)) setMode(mode, true);
|
|
1023
1097
|
// The system prompt lists the workspace as the session starts (lib/workspace-channel.ts).
|
|
1024
|
-
|
|
1098
|
+
sessionStarted = true;
|
|
1099
|
+
announcedWorkspace = undefined;
|
|
1100
|
+
announceWorkspace();
|
|
1025
1101
|
resetClassifierChoice(ctx.model, "batched");
|
|
1026
1102
|
// The bridge reads live mode/rules, but belongs to this parent session only.
|
|
1027
1103
|
pi.events.emit(SUBAGENT_GATE_CHANNEL, { decide: sessionPermissionBridge(evaluateChildToolCall, childPermissionSession.signal) });
|
|
1028
1104
|
});
|
|
1029
1105
|
|
|
1106
|
+
// A /tree switch selects a branch whose model was told that branch's mode:
|
|
1107
|
+
// take it, as a resume would, without announcing a switch. A stored
|
|
1108
|
+
// bypassPermissions is never taken.
|
|
1109
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1110
|
+
// The selected branch's model was told its own session directories.
|
|
1111
|
+
restoreSessionWorkspace(ctx);
|
|
1112
|
+
refreshWorkspace();
|
|
1113
|
+
const onBranch = normalizePermissionMode(readState(ctx, PERMISSION_MODE_STATE));
|
|
1114
|
+
if (!onBranch || onBranch === "bypassPermissions" || onBranch === mode) return;
|
|
1115
|
+
branchMode = onBranch;
|
|
1116
|
+
setMode(onBranch, true);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// claude-context re-reads the instruction files at a compaction, and the
|
|
1120
|
+
// summary's epoch renders them, so the classifier judges against the same
|
|
1121
|
+
// set from then on. Unchanged files give the same bytes, so its cache holds;
|
|
1122
|
+
// an edit costs one classifier cache write at this boundary.
|
|
1123
|
+
pi.on("session_compact", () => {
|
|
1124
|
+
projectInstructionsByCwd.clear();
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1030
1127
|
// The published bridge stays: aborted, it denies every call a retained child
|
|
1031
1128
|
// makes, while a parent without this extension keeps the local gate.
|
|
1032
1129
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -1087,7 +1184,12 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1087
1184
|
pi.events.on(MODE_CHANNEL, (data) => {
|
|
1088
1185
|
const request = data as { mode?: unknown; toolCallId?: unknown } | undefined;
|
|
1089
1186
|
const requested = normalizePermissionMode(request?.mode);
|
|
1090
|
-
if (requested)
|
|
1187
|
+
if (!requested) return;
|
|
1188
|
+
// Another extension asks (plan mode's exit restores the mode it came from,
|
|
1189
|
+
// read from the branch). A stored value never grants a permission by
|
|
1190
|
+
// itself: bypass only where this session has it, else the default mode.
|
|
1191
|
+
const allowed = requested === "bypassPermissions" && !bypassInCycle ? "default" : requested;
|
|
1192
|
+
setMode(allowed, false, typeof request?.toolCallId === "string" ? request.toolCallId : undefined);
|
|
1091
1193
|
});
|
|
1092
1194
|
|
|
1093
1195
|
// The plan-mode extension announces the plan file on PLAN_FILE_CHANNEL;
|
|
@@ -1941,7 +2043,10 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1941
2043
|
reloadRules(ctx);
|
|
1942
2044
|
return `Added directory ${path} to workspace and saved to ${tildify(target, home)}`;
|
|
1943
2045
|
}
|
|
1944
|
-
if (!sessionWorkspaceDirs.includes(path))
|
|
2046
|
+
if (!sessionWorkspaceDirs.includes(path)) {
|
|
2047
|
+
sessionWorkspaceDirs.push(path);
|
|
2048
|
+
recordSessionWorkspace();
|
|
2049
|
+
}
|
|
1945
2050
|
refreshWorkspace();
|
|
1946
2051
|
return `Added directory ${path} to workspace for this session`;
|
|
1947
2052
|
};
|
|
@@ -2026,7 +2131,10 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
2026
2131
|
const [source, settingsPath, raw] = key.split("\0");
|
|
2027
2132
|
if (source === "session") {
|
|
2028
2133
|
const index = sessionWorkspaceDirs.indexOf(raw);
|
|
2029
|
-
if (index >= 0)
|
|
2134
|
+
if (index >= 0) {
|
|
2135
|
+
sessionWorkspaceDirs.splice(index, 1);
|
|
2136
|
+
recordSessionWorkspace();
|
|
2137
|
+
}
|
|
2030
2138
|
refreshWorkspace();
|
|
2031
2139
|
} else if (source === ("onecode-user" satisfies RuleSource) || source === ("onecode-project" satisfies RuleSource)) {
|
|
2032
2140
|
if (!removeWorkspaceDirectory(raw, settingsPath)) throw new Error(`${raw} is no longer in ${tildify(settingsPath, home)}.`);
|
|
@@ -2195,7 +2303,8 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
2195
2303
|
toolName: "subagent-review",
|
|
2196
2304
|
transcript: [...transcript, ...childEntries],
|
|
2197
2305
|
userMessages: [...userMessages],
|
|
2198
|
-
|
|
2306
|
+
// The main session's instructions, read where its own calls read them.
|
|
2307
|
+
claudeMd: instructionsFor(workCwd(ctx.cwd)),
|
|
2199
2308
|
username: classifierUsername,
|
|
2200
2309
|
environment: autoConfig.environment,
|
|
2201
2310
|
// AutoModeConfig is structurally a RuleExtras; the user's own deny rules
|
|
@@ -128,6 +128,9 @@ function badgeBase(mode: PermissionMode, opts?: { paused?: boolean; classifier?:
|
|
|
128
128
|
*/
|
|
129
129
|
export const PERMISSION_STATUS_CHANNEL = "one-code:permission-status";
|
|
130
130
|
|
|
131
|
+
/** The `one-code:state` key the permission mode is recorded under on the branch; a fork's file carries the parent's. */
|
|
132
|
+
export const PERMISSION_MODE_STATE = "permission-mode";
|
|
133
|
+
|
|
131
134
|
export interface PermissionStatus {
|
|
132
135
|
mode: string;
|
|
133
136
|
paused: boolean;
|
|
@@ -137,6 +140,8 @@ export interface PermissionStatus {
|
|
|
137
140
|
classifier?: string;
|
|
138
141
|
/** Whether `classifier` is pinned by a real call, or still the planned first candidate. */
|
|
139
142
|
pinned?: boolean;
|
|
143
|
+
/** Whether this session can enter bypassPermissions (it started in it); a request for it otherwise lands in default. */
|
|
144
|
+
bypassAvailable?: boolean;
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
/**
|