pi-code 1.0.71 → 1.0.73
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/extensions/claude-rules.ts +5 -2
- package/extensions/commands.ts +45 -13
- package/extensions/context-imports.ts +12 -5
- package/extensions/hooks/index.ts +2 -2
- package/extensions/init.ts +2 -2
- package/extensions/internal/project-root.ts +23 -0
- package/extensions/mcp/index.ts +3 -3
- package/extensions/plan-mode/index.ts +14 -3
- package/extensions/status-line.ts +2 -2
- package/extensions/subagent/agents.ts +7 -2
- package/package.json +1 -1
|
@@ -27,7 +27,7 @@ import { publishInstructionLoad } from './internal/instruction-events.js'
|
|
|
27
27
|
import { readManagedSettings } from './internal/managed-settings.js'
|
|
28
28
|
import { type CompiledGlob, compileGlobs, matchesCompiledGlobs } from './internal/path-rules.js'
|
|
29
29
|
import { isProjectApproved } from './internal/project-approval.js'
|
|
30
|
-
import { findNearestDir } from './internal/project-root.js'
|
|
30
|
+
import { findNearestDir, sameLocation } from './internal/project-root.js'
|
|
31
31
|
import { stripBlockComments } from './internal/strip-comments.js'
|
|
32
32
|
import { fileToolTarget } from './internal/tool-target.js'
|
|
33
33
|
|
|
@@ -303,7 +303,10 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
|
|
|
303
303
|
globalRules = readRules(globalRulesDir, isExcluded)
|
|
304
304
|
// Nearest at-or-above cwd, so a subdirectory session still reads the rules the
|
|
305
305
|
// approval walk gated on.
|
|
306
|
-
|
|
306
|
+
// Not the user's own rules dir: from $HOME (or under a dotfiles repo rooted there)
|
|
307
|
+
// the nearest one is ~/.claude/rules, which the global load above already read.
|
|
308
|
+
const nearestRulesDir = approved ? findNearestDir(ctx.cwd, path.join('.claude', 'rules')) : null
|
|
309
|
+
const projectRulesDir = nearestRulesDir !== null && sameLocation(nearestRulesDir, globalRulesDir) ? null : nearestRulesDir
|
|
307
310
|
projectRules = projectRulesDir ? readRules(projectRulesDir, isExcluded) : EMPTY_RULES
|
|
308
311
|
|
|
309
312
|
// Global globs are relative to cwd; project globs to the project root (the dir
|
package/extensions/commands.ts
CHANGED
|
@@ -52,9 +52,10 @@ import { managedSettingsFile, readManagedSettings } from './internal/managed-set
|
|
|
52
52
|
import { findModel } from './internal/model-lookup.js'
|
|
53
53
|
import { capForContext } from './internal/output-guard.js'
|
|
54
54
|
import { matchesPathRules } from './internal/path-rules.js'
|
|
55
|
+
import { isPlanModeState, PLAN_MODE_CHANNEL } from './internal/plan-mode-state.js'
|
|
55
56
|
import { type InstalledPlugin, installedPlugins, pluginComponentPath } from './internal/plugins.js'
|
|
56
57
|
import { isProjectApproved } from './internal/project-approval.js'
|
|
57
|
-
import { ancestorDirs,
|
|
58
|
+
import { ancestorDirs, checkoutRoot } from './internal/project-root.js'
|
|
58
59
|
import { agentNamesIn, matchesAgentRules, matchesDomainRules, matchesSkillRules } from './internal/scope-rules.js'
|
|
59
60
|
import { claudeSettingsChain } from './internal/settings-chain.js'
|
|
60
61
|
import { fileToolTarget } from './internal/tool-target.js'
|
|
@@ -205,7 +206,7 @@ function commandVars(ctx: { cwd: string }, filePath: string, plugin?: CommandPlu
|
|
|
205
206
|
// low|medium|high|xhigh|max, and an unset one stays literal in the body.
|
|
206
207
|
CLAUDE_EFFORT: claudeEffortLevel(varCtx.thinkingLevel),
|
|
207
208
|
CLAUDE_SKILL_DIR: path.dirname(filePath),
|
|
208
|
-
CLAUDE_PROJECT_DIR:
|
|
209
|
+
CLAUDE_PROJECT_DIR: checkoutRoot(ctx.cwd),
|
|
209
210
|
CLAUDE_PLUGIN_ROOT: plugin?.root,
|
|
210
211
|
CLAUDE_PLUGIN_DATA: plugin?.dataDir,
|
|
211
212
|
}
|
|
@@ -227,7 +228,7 @@ interface SpanRunner {
|
|
|
227
228
|
* from running.
|
|
228
229
|
*/
|
|
229
230
|
export async function expandCommand(runner: SpanRunner, parsed: ParsedCommand, args: string, ctx: { cwd: string }, filePath: string, plugin?: CommandPlugin, options?: { allowShell?: boolean }): Promise<string> {
|
|
230
|
-
const projectRoot =
|
|
231
|
+
const projectRoot = checkoutRoot(ctx.cwd)
|
|
231
232
|
const vars = commandVars(ctx, filePath, plugin)
|
|
232
233
|
const { text: withArgs, consumed } = substituteArgsDetailed(parsed.body, args, parsed.argumentNames ?? [])
|
|
233
234
|
// `${user_config.KEY}` is a plugin-command variable only; leave it literal in an
|
|
@@ -319,6 +320,9 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
319
320
|
let projectApproved = false
|
|
320
321
|
/** Tool set to put back once the run a restricted command drove has ended. */
|
|
321
322
|
let pendingRestore: string[] | undefined
|
|
323
|
+
/** The tools this run's restriction took out of the set. Put back as a delta on whatever is
|
|
324
|
+
* active by then: overwriting with the snapshot undid every change made meanwhile. */
|
|
325
|
+
let tookAway = new Set<string>()
|
|
322
326
|
/** `Bash(...)` scopes enforced while that run lasts; lifted with the restriction. */
|
|
323
327
|
let pendingBashRules: string[] | undefined
|
|
324
328
|
/** `WebFetch(domain:...)` scopes enforced the same way. */
|
|
@@ -362,17 +366,17 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
362
366
|
// agent_settled fires exactly once, after the run has fully settled and no such
|
|
363
367
|
// continuation remains, which is the grant's true clearing point.
|
|
364
368
|
pi.on('agent_settled', async () => {
|
|
365
|
-
pendingBashRules = undefined
|
|
366
|
-
pendingDomainRules = undefined
|
|
367
|
-
pendingAgentRules = undefined
|
|
368
|
-
pendingSkillRules = undefined
|
|
369
|
-
pendingPathRules = undefined
|
|
370
369
|
void modelOverride.settle()
|
|
371
370
|
void effortOverride.settle()
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
liftToolScope()
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// "Execute the plan" continues the very run a scoped command started, so the scope it set
|
|
375
|
+
// for planning would judge every call of the execution and the tools it narrowed away
|
|
376
|
+
// would stay gone. Leaving plan mode is the point the user approved more than the command
|
|
377
|
+
// asked for; the model and effort overrides still last until the run settles.
|
|
378
|
+
pi.events?.on(PLAN_MODE_CHANNEL, (data) => {
|
|
379
|
+
if (isPlanModeState(data) && !data.active) liftToolScope()
|
|
376
380
|
})
|
|
377
381
|
|
|
378
382
|
// agent_settled never fires when pi is quit mid-run (double Ctrl+C, Ctrl+D, a closed
|
|
@@ -411,7 +415,7 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
411
415
|
// pi's read/edit/write accept `file_path` as an alias for `path`; the shared reader
|
|
412
416
|
// handles both, and the paired guard in hooks/matcher.ts reads both too.
|
|
413
417
|
const filePath = fileToolTarget(event) ?? ''
|
|
414
|
-
const anchors = { cwd: ctx.cwd, projectRoot:
|
|
418
|
+
const anchors = { cwd: ctx.cwd, projectRoot: checkoutRoot(ctx.cwd), home: os.homedir() }
|
|
415
419
|
if (filePath && matchesPathRules(filePath, rules, anchors)) return
|
|
416
420
|
return {
|
|
417
421
|
block: true,
|
|
@@ -427,6 +431,32 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
427
431
|
return original
|
|
428
432
|
}
|
|
429
433
|
|
|
434
|
+
/** Record what the restriction just applied took out, against the set captured before it. */
|
|
435
|
+
function noteTakenAway(): void {
|
|
436
|
+
if (!pendingRestore) return
|
|
437
|
+
const active = new Set(pi.getActiveTools())
|
|
438
|
+
tookAway = new Set(pendingRestore.filter((tool) => !active.has(tool)))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** End the run's tool restriction: drop its scopes and put back what it took away, as a
|
|
442
|
+
* delta on the set in force. A snapshot overwrite undid whatever else changed the set
|
|
443
|
+
* meanwhile: plan mode's Execute (the session finished read-only, plan mode off, with edit
|
|
444
|
+
* and write gone) and a tool registered during the run (dropped until a /reload). */
|
|
445
|
+
function liftToolScope(): void {
|
|
446
|
+
pendingBashRules = undefined
|
|
447
|
+
pendingDomainRules = undefined
|
|
448
|
+
pendingAgentRules = undefined
|
|
449
|
+
pendingSkillRules = undefined
|
|
450
|
+
pendingPathRules = undefined
|
|
451
|
+
const original = pendingRestore
|
|
452
|
+
if (!original) return
|
|
453
|
+
const active = pi.getActiveTools()
|
|
454
|
+
const keep = new Set([...active, ...tookAway])
|
|
455
|
+
pi.setActiveTools([...original.filter((tool) => keep.has(tool)), ...active.filter((tool) => !original.includes(tool))])
|
|
456
|
+
pendingRestore = undefined
|
|
457
|
+
tookAway = new Set()
|
|
458
|
+
}
|
|
459
|
+
|
|
430
460
|
/** Apply a command's allowed-tools to the run it drives: intersect with the tools
|
|
431
461
|
* pi actually has, keep the bash and path scopes for the tool_call guard, and let
|
|
432
462
|
* agent_settled restore the previous set. */
|
|
@@ -442,6 +472,7 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
442
472
|
// intersects to nothing named only tools pi has none of: that restriction cannot
|
|
443
473
|
// be expressed, and applying it as "no tools" is not what the command asked for.
|
|
444
474
|
if (granted.length > 0 || allowed.length === 0) pi.setActiveTools(granted)
|
|
475
|
+
noteTakenAway()
|
|
445
476
|
// The latest restricted command speaks for the turn: a later unscoped grant
|
|
446
477
|
// lifts an earlier command's scopes rather than stacking under them. Rules get
|
|
447
478
|
// the same ${CLAUDE_*} substitution as the body, so a rule can name a bundled
|
|
@@ -460,6 +491,7 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
460
491
|
if (!disallowed || disallowed.length === 0) return
|
|
461
492
|
captureRestorePoint()
|
|
462
493
|
pi.setActiveTools(pi.getActiveTools().filter((tool) => !disallowed.includes(tool)))
|
|
494
|
+
noteTakenAway()
|
|
463
495
|
}
|
|
464
496
|
|
|
465
497
|
/** Claude's `model:` frontmatter overrides the model for this run only, then the
|
|
@@ -76,7 +76,7 @@ import { managedSettingsPath, readManagedSettings } from './internal/managed-set
|
|
|
76
76
|
import { capForContext, sliceBytes } from './internal/output-guard.js'
|
|
77
77
|
import { globToRegExpSource } from './internal/path-rules.js'
|
|
78
78
|
import { isGatedFileApproved, isProjectApproved, isProjectApprovedSilently } from './internal/project-approval.js'
|
|
79
|
-
import { ancestorFiles, findNearestFile, repoRoot } from './internal/project-root.js'
|
|
79
|
+
import { ancestorFiles, checkoutRoot, findNearestFile, repoRoot, sameLocation } from './internal/project-root.js'
|
|
80
80
|
import { claudeSettingsChain, readSettingsChain } from './internal/settings-chain.js'
|
|
81
81
|
import { statToken } from './internal/stat-token.js'
|
|
82
82
|
import { type Fence, fenceMarker, stepFence, stripBlockComments } from './internal/strip-comments.js'
|
|
@@ -293,7 +293,10 @@ export function rootsForImporter(importer: string, home: string, cwd: string, ex
|
|
|
293
293
|
// granting the importer's own directory. pi also loads home-level context
|
|
294
294
|
// files (~/AGENTS.md) that sit outside the config roots; their directory is
|
|
295
295
|
// $HOME, and allowing it would let @.ssh/... read into every session's prompt.
|
|
296
|
-
|
|
296
|
+
// The checkout the session runs in, and the main checkout too: in a worktree they are
|
|
297
|
+
// siblings, and the checkout's own files (docs/, a sibling CLAUDE.md) are as much the
|
|
298
|
+
// project as the shared root a CLAUDE.local.md may live at.
|
|
299
|
+
return realRoots([cwd, checkoutRoot(cwd), repoRoot(cwd) ?? cwd])
|
|
297
300
|
}
|
|
298
301
|
|
|
299
302
|
/** Claude's env gate for loading memory files from --add-dir directories. */
|
|
@@ -972,7 +975,11 @@ export default function contextImportsExtension(pi: ExtensionAPI) {
|
|
|
972
975
|
// ./.claude/CLAUDE.md (nearest at or above cwd) is repo-controlled too, so both
|
|
973
976
|
// ride the one approval decision.
|
|
974
977
|
const candidates = ancestorFiles(ctx.cwd, 'CLAUDE.local.md')
|
|
975
|
-
const
|
|
978
|
+
const nearestDotClaudeMd = findNearestFile(ctx.cwd, path.join('.claude', 'CLAUDE.md'))
|
|
979
|
+
// From $HOME (or under a dotfiles repo rooted there) the nearest one is the user's own
|
|
980
|
+
// ~/.claude/CLAUDE.md, already loaded above as User memory: not a project file, so no
|
|
981
|
+
// second block and nothing to approve.
|
|
982
|
+
const dotClaudeMd = nearestDotClaudeMd !== null && sameLocation(nearestDotClaudeMd, userClaudeMd) ? null : nearestDotClaudeMd
|
|
976
983
|
if ((candidates.length === 0 && dotClaudeMd === null) || !(await isProjectApproved(ctx))) return memory
|
|
977
984
|
|
|
978
985
|
for (const candidate of candidates) {
|
|
@@ -1029,7 +1036,7 @@ export default function contextImportsExtension(pi: ExtensionAPI) {
|
|
|
1029
1036
|
|
|
1030
1037
|
if (envCache?.cwd !== cwd) {
|
|
1031
1038
|
const managedNow = readManagedSettings()
|
|
1032
|
-
envCache = { cwd, managed: managedNow, excludeGlobs: readClaudeMdExcludes(claudeMdExcludeFiles(cwd, home, projectApproved), managedNow), projectRoot:
|
|
1039
|
+
envCache = { cwd, managed: managedNow, excludeGlobs: readClaudeMdExcludes(claudeMdExcludeFiles(cwd, home, projectApproved), managedNow), projectRoot: checkoutRoot(cwd) }
|
|
1033
1040
|
}
|
|
1034
1041
|
const { managed, excludeGlobs, projectRoot } = envCache
|
|
1035
1042
|
// CLAUDE_CODE_DISABLE_CLAUDE_MDS also covers pi's own auto-discovered native context
|
|
@@ -1136,7 +1143,7 @@ export default function contextImportsExtension(pi: ExtensionAPI) {
|
|
|
1136
1143
|
if (touchedDir === undefined) return
|
|
1137
1144
|
|
|
1138
1145
|
const home = os.homedir()
|
|
1139
|
-
const projectRoot =
|
|
1146
|
+
const projectRoot = checkoutRoot(ctx.cwd)
|
|
1140
1147
|
const excludeGlobs = readClaudeMdExcludes(claudeMdExcludeFiles(ctx.cwd, home, projectApproved), readManagedSettings())
|
|
1141
1148
|
const load: NestedLoadContext = { home, cwd: ctx.cwd, realCwd, projectRoot, excludeGlobs, touched, launchLoaded: launchLoadedPaths }
|
|
1142
1149
|
const { bodies, events } = nestedContextAttachments(touchedDir, load, nestedLoaded, gatedFilesApproved)
|
|
@@ -104,7 +104,7 @@ import { resolveModelOverride } from '../internal/model-lookup.js'
|
|
|
104
104
|
import { isPlanModeState, PLAN_MODE_CHANNEL } from '../internal/plan-mode-state.js'
|
|
105
105
|
import { installedPlugins, managedForceEnabled } from '../internal/plugins.js'
|
|
106
106
|
import { approvalRecheck, isProjectApproved } from '../internal/project-approval.js'
|
|
107
|
-
import {
|
|
107
|
+
import { checkoutRoot } from '../internal/project-root.js'
|
|
108
108
|
import { watchSettingsFiles } from '../internal/settings-watch.js'
|
|
109
109
|
import { isSkillHooksEvent, SKILL_HOOKS_CHANNEL } from '../internal/skill-hooks.js'
|
|
110
110
|
import { isSubagentPhaseEvent, SUBAGENT_CHANNEL } from '../internal/subagent-events.js'
|
|
@@ -457,7 +457,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
457
457
|
// Claude's CLAUDE_PROJECT_DIR is the project root, not the session cwd; a hook
|
|
458
458
|
// referencing $CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh must resolve from a
|
|
459
459
|
// subdirectory session too.
|
|
460
|
-
projectDir =
|
|
460
|
+
projectDir = checkoutRoot(ctx.cwd)
|
|
461
461
|
resolveConfig(ctx.cwd, trusted)
|
|
462
462
|
// Claude picks up direct settings edits mid-session via a file watcher. The reload
|
|
463
463
|
// closes over the cwd value, never ctx: the poll has no awaiter, and every getter of
|
package/extensions/init.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { CONTEXT_FILE_CANDIDATES }
|
|
|
23
23
|
|
|
24
24
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { checkoutRoot } from './internal/project-root.js'
|
|
27
27
|
|
|
28
28
|
function statOf(target: string): fs.Stats | undefined {
|
|
29
29
|
try {
|
|
@@ -73,7 +73,7 @@ export default function initExtension(pi: ExtensionAPI) {
|
|
|
73
73
|
pi.registerCommand('init', {
|
|
74
74
|
description: 'Analyze the codebase and create or improve the project context file (AGENTS.md)',
|
|
75
75
|
handler: async (_args, ctx) => {
|
|
76
|
-
const root =
|
|
76
|
+
const root = checkoutRoot(ctx.cwd)
|
|
77
77
|
const existing = findExistingContextFile(root)
|
|
78
78
|
const cursorRules = statOf(path.join(root, '.cursor', 'rules'))?.isDirectory() === true || statOf(path.join(root, '.cursorrules'))?.isFile() === true
|
|
79
79
|
const copilotRules = statOf(path.join(root, '.github', 'copilot-instructions.md'))?.isFile() === true
|
|
@@ -29,6 +29,29 @@ export function repoRoot(from: string): string | undefined {
|
|
|
29
29
|
return mainCheckout(root)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** The root of the checkout a session runs in, or `from` itself outside one. Claude's
|
|
33
|
+
* `CLAUDE_PROJECT_DIR` is "the project root where the session started", and `/`-rooted path
|
|
34
|
+
* rules anchor there. In a worktree that is the worktree, not the main checkout repoRoot
|
|
35
|
+
* resolves to: right for shared state (settings.local.json, auto memory), wrong here, where
|
|
36
|
+
* a hook script or a rule anchor must land in the tree the session is editing. */
|
|
37
|
+
export function checkoutRoot(from: string): string {
|
|
38
|
+
return gitRoot(from) ?? from
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether two paths are the same file or directory once symlinks resolve. A session at
|
|
42
|
+
* `$HOME` finds the user's own `~/.claude/{rules,agents,CLAUDE.md}` as its "project" ones;
|
|
43
|
+
* comparing by realpath also catches a stow-style symlinked `~/.claude`. */
|
|
44
|
+
export function sameLocation(a: string, b: string): boolean {
|
|
45
|
+
const resolve = (target: string): string => {
|
|
46
|
+
try {
|
|
47
|
+
return fs.realpathSync(target)
|
|
48
|
+
} catch {
|
|
49
|
+
return path.resolve(target)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return resolve(a) === resolve(b)
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
/** The git checkout at or above `from`, or undefined outside one.
|
|
33
56
|
*
|
|
34
57
|
* Narrower than repoRoot on purpose: repoRoot resolves a worktree to its main checkout,
|
package/extensions/mcp/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { setMcpToolCaller } from '../internal/mcp-call.js'
|
|
|
42
42
|
import { capForContext } from '../internal/output-guard.js'
|
|
43
43
|
import { installedPlugins } from '../internal/plugins.js'
|
|
44
44
|
import { isProjectApproved, isProjectApprovedSilently } from '../internal/project-approval.js'
|
|
45
|
-
import {
|
|
45
|
+
import { checkoutRoot } from '../internal/project-root.js'
|
|
46
46
|
import { claudeSettingsChain } from '../internal/settings-chain.js'
|
|
47
47
|
import { errorMessage } from '../internal/values.js'
|
|
48
48
|
import { disabledServerNames, loadConfigFrom, loadPluginServers, loadUserScope, localScopeServerNames, projectConfigPaths, type ServerConfig, warnOnTypelessUrl } from './config.js'
|
|
@@ -539,7 +539,7 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
539
539
|
// Plugin servers merge under the user scope (plugins are user-installed);
|
|
540
540
|
// the user's own entry wins a name clash with a plugin's. A server toggled off
|
|
541
541
|
// in ~/.claude.json's per-project disabledMcpServers list never connects.
|
|
542
|
-
const pluginServers = loadPluginServers(installedPlugins(os.homedir()),
|
|
542
|
+
const pluginServers = loadPluginServers(installedPlugins(os.homedir()), checkoutRoot(ctx.cwd))
|
|
543
543
|
const disabled = disabledServerNames(os.homedir(), ctx.cwd)
|
|
544
544
|
const merged = Object.fromEntries(Object.entries({ ...pluginServers, ...loadUserScope(os.homedir(), ctx.cwd) }).filter(([name]) => !disabled.has(name)))
|
|
545
545
|
const scoped = applyServerPolicy(merged, policy)
|
|
@@ -622,7 +622,7 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
622
622
|
projectConnected = false
|
|
623
623
|
// Claude answers roots/list with the session's launch directory and exports the
|
|
624
624
|
// project root as CLAUDE_PROJECT_DIR to stdio servers; both derive from ctx.cwd.
|
|
625
|
-
sessionDirs = { projectDir:
|
|
625
|
+
sessionDirs = { projectDir: checkoutRoot(ctx.cwd), launchDir: ctx.cwd, sessionId: ctx.sessionManager?.getSessionId?.() }
|
|
626
626
|
const authUi = authUiFor(ctx)
|
|
627
627
|
sessionAuthUi = authUi
|
|
628
628
|
// The allow/deny lists filter every scope, including a managed-mcp.json set. They
|
|
@@ -64,18 +64,28 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
64
64
|
let todoItems: TodoItem[] = []
|
|
65
65
|
let planFromTool = false
|
|
66
66
|
let savedTools: string[] = []
|
|
67
|
+
/** Every tool pi had registered when the set was saved, active or not: a tool outside it
|
|
68
|
+
* was registered later, and pi activated it on top of whatever set was in force. */
|
|
69
|
+
let toolsAtSave = new Set<string>()
|
|
67
70
|
let stalledRuns = 0
|
|
68
71
|
let runProgress = false
|
|
69
72
|
|
|
73
|
+
const registeredTools = (): Set<string> => new Set((pi.getAllTools?.() ?? []).map((tool) => tool.name))
|
|
74
|
+
|
|
70
75
|
function enterPlanTools(): void {
|
|
71
76
|
savedTools = pi.getActiveTools()
|
|
77
|
+
toolsAtSave = registeredTools()
|
|
72
78
|
pi.setActiveTools(PLAN_MODE_TOOLS.filter((t) => savedTools.includes(t)))
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
/** Put back the saved set, plus any tool registered since: pi activates a late one (an MCP
|
|
82
|
+
* server past its connect window) on top of the restricted set, and overwriting with the
|
|
83
|
+
* snapshot dropped it for the rest of the session. A tool that was registered but not
|
|
84
|
+
* active at entry is not late, and stays out. */
|
|
75
85
|
function restoreTools(): void {
|
|
76
|
-
if (savedTools.length
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
if (savedTools.length === 0) return
|
|
87
|
+
const late = pi.getActiveTools().filter((name) => !toolsAtSave.has(name) && !savedTools.includes(name))
|
|
88
|
+
pi.setActiveTools([...savedTools, ...late])
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/** Hooks report Claude's permission_mode from this bus state. */
|
|
@@ -457,6 +467,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
457
467
|
// snapshot when plan mode is off would instead push a stale set over whatever
|
|
458
468
|
// pi has registered since, so it stays scoped to this branch.
|
|
459
469
|
savedTools = restored.savedTools ?? pi.getActiveTools()
|
|
470
|
+
toolsAtSave = registeredTools()
|
|
460
471
|
pi.setActiveTools(PLAN_MODE_TOOLS.filter((t) => savedTools.includes(t)))
|
|
461
472
|
// --plan enters plan mode without ever toggling, so nothing has persisted yet
|
|
462
473
|
// and a /reload would find no snapshot to restore from. Record it now, while
|
|
@@ -36,7 +36,7 @@ import { claudeEffortLevel } from './internal/effort.js'
|
|
|
36
36
|
import { readManagedSettings } from './internal/managed-settings.js'
|
|
37
37
|
import { isPlanModeState, PLAN_MODE_CHANNEL } from './internal/plan-mode-state.js'
|
|
38
38
|
import { approvalRecheck, isProjectApprovedSilently } from './internal/project-approval.js'
|
|
39
|
-
import {
|
|
39
|
+
import { checkoutRoot, gitRoot } from './internal/project-root.js'
|
|
40
40
|
import { readSettingsChain } from './internal/settings-chain.js'
|
|
41
41
|
import { watchSettingsFiles } from './internal/settings-watch.js'
|
|
42
42
|
import { readActiveStyleName, settingsFiles } from './output-styles.js'
|
|
@@ -283,7 +283,7 @@ export default function statusLine(pi: ExtensionAPI) {
|
|
|
283
283
|
// directory. added_dirs comes from the --add-dir flag pi-code registers.
|
|
284
284
|
workspace: {
|
|
285
285
|
current_dir: ctx.cwd,
|
|
286
|
-
project_dir:
|
|
286
|
+
project_dir: checkoutRoot(ctx.cwd),
|
|
287
287
|
git_worktree: isGitWorktree(ctx.cwd),
|
|
288
288
|
added_dirs: addedDirs(),
|
|
289
289
|
},
|
|
@@ -14,7 +14,7 @@ import { claudeConfigDir } from '../internal/config-dir.js'
|
|
|
14
14
|
import { parseClaudeFrontmatter } from '../internal/frontmatter.js'
|
|
15
15
|
import { findModel } from '../internal/model-lookup.js'
|
|
16
16
|
import { installedPlugins, pluginComponentPath } from '../internal/plugins.js'
|
|
17
|
-
import { ancestorDirs, findNearestDir } from '../internal/project-root.js'
|
|
17
|
+
import { ancestorDirs, findNearestDir, sameLocation } from '../internal/project-root.js'
|
|
18
18
|
import { errorMessage } from '../internal/values.js'
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -371,7 +371,12 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
371
371
|
// Claude scans every .claude/agents between cwd and the repository root, the
|
|
372
372
|
// definition closest to cwd winning a name clash; root-first load order makes
|
|
373
373
|
// the nearer directory's entry overwrite in the map below.
|
|
374
|
-
|
|
374
|
+
// The user's own directory is not a project one: at $HOME, or under a dotfiles repo
|
|
375
|
+
// rooted there, the walk reaches ~/.claude/agents and would gate the user's agents as
|
|
376
|
+
// repo-controlled.
|
|
377
|
+
const projectClaudeDirs = ancestorDirs(cwd, path.join('.claude', 'agents'))
|
|
378
|
+
.filter((dir) => !sameLocation(dir, claudeUserDir))
|
|
379
|
+
.reverse()
|
|
375
380
|
|
|
376
381
|
// Plugins load after builtins and before the user's own dirs, so a user agent
|
|
377
382
|
// wins a name clash with a plugin's, and ~/.pi/agent/agents wins over ~/.claude.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.73",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, subagents, and goals",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|