@phi-code-admin/phi-code 0.78.0 ā 0.79.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/CHANGELOG.md +37 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/extensions/phi/commit.ts +253 -0
- package/extensions/phi/memory.ts +129 -2
- package/extensions/phi/orchestrator.ts +101 -10
- package/package.json +1 -1
- /package/extensions/phi/{orchestrator-helpers.ts ā providers/orchestrator-helpers.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.79.0] - 2026-06-13
|
|
4
|
+
|
|
5
|
+
Marathon increment 2: resumable orchestration, productivity, deterministic recall.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`/plan --resume`.** A crashed or aborted orchestration can be resumed from a
|
|
10
|
+
tiny checkpoint (`.phi/plans/orchestration-state.json`) that records the run's
|
|
11
|
+
position. Resume rebuilds the phases with the original timestamp so the existing
|
|
12
|
+
`.phi/plans/*.md` handoff files are reused. A `BLOCKED` verdict now leaves the
|
|
13
|
+
checkpoint in place so you can fix the blocker and `/plan --resume`.
|
|
14
|
+
- **`/commit` command.** Packaged, fully deterministic (no LLM): stages or commits
|
|
15
|
+
the working tree with a `[phi] ...` message derived from the staged files and the
|
|
16
|
+
last assistant turn, with a safety protocol (refuses staged `.env` / `*.pem` /
|
|
17
|
+
secrets, never `--amend`, single shell-free `git commit` call). Stages all only
|
|
18
|
+
with `/commit --all`.
|
|
19
|
+
- **Deterministic memory recall.** A compact one-line-per-note memory index
|
|
20
|
+
(filename + frontmatter summary, capped at 40 entries / ~2k chars) is injected
|
|
21
|
+
every turn and returned by `memory_read`, so the model always knows what facts
|
|
22
|
+
exist instead of relying on it to call `memory_search`.
|
|
23
|
+
- **Effort-calibrated REVIEW.** The review agent now scales its depth to the diff
|
|
24
|
+
size (small diff: one high-confidence pass, <=4 findings; large diff: all angles
|
|
25
|
+
+ verification), staying within the phase time limit.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Startup warning regression (0.78.0).** The orchestrator helper module lived at
|
|
30
|
+
the top level of `extensions/phi/`, where extension auto-discovery tried to load
|
|
31
|
+
it as an extension and logged "does not export a valid factory function". Moved
|
|
32
|
+
it into `providers/` (a non-discovered subdir) where the other phi helper modules
|
|
33
|
+
live. No behaviour change.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Bash tool description now asks for an active-voice command description for the
|
|
38
|
+
TUI / logs.
|
|
39
|
+
|
|
3
40
|
## [0.78.0] - 2026-06-13
|
|
4
41
|
|
|
5
42
|
Orchestration robustness + quality pass, derived from an analysis of Claude Code's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAY5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAoD,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAExG,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,wBAAwB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,wBAAwB,CA8ExF;AA4DD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,CACL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QACR,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACxB,KACG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAgE1F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;AAO5E,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAKD,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;CACrC,CAAC;AAwGF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CAqLjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { spawn } from \"child_process\";\nimport type { AgentTool } from \"phi-code-agent\";\nimport { Container, Text, truncateToWidth } from \"phi-code-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { waitForChildProcess } from \"../../utils/child-process.js\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { OutputAccumulator } from \"./output-accumulator.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.js\";\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface DestructiveCommandResult {\n\tblocked: boolean;\n\treason?: string;\n}\n\n/**\n * Deterministic, local (zero API call) safety gate for destructive bash commands.\n *\n * Pure function: given a raw command string (and optionally the workspace cwd),\n * returns whether the command should be blocked and a human readable reason.\n *\n * This is intentionally conservative: a false negative (a dangerous command slips\n * through) is preferred over a false positive (a safe command is blocked). Only\n * patterns that are very clearly destructive are matched. The gate is meant to be\n * applied only during autonomous orchestration, never in normal interactive use.\n */\nexport function isDestructiveCommand(cmd: string, cwd?: string): DestructiveCommandResult {\n\tif (typeof cmd !== \"string\" || cmd.length === 0) {\n\t\treturn { blocked: false };\n\t}\n\t// Normalize for matching: collapse runs of whitespace to single spaces.\n\t// The original command is still used for path extraction below.\n\tconst normalized = cmd.replace(/\\s+/g, \" \").trim();\n\tconst lower = normalized.toLowerCase();\n\n\t// Fork bombs (classic shell fork bomb and common variants).\n\t// Compare against a whitespace-stripped form to catch spacing variants.\n\tconst dense = normalized.replace(/\\s+/g, \"\");\n\tif (/\\(\\)\\{:?\\|:?&?\\};:/.test(dense) || /\\w+\\(\\)\\{\\w+\\|\\w+&\\};/.test(dense)) {\n\t\treturn { blocked: true, reason: \"Fork bomb pattern detected.\" };\n\t}\n\n\t// Pipe a remote download straight into a shell interpreter (curl|sh, wget|sh, etc.).\n\tif (/\\b(?:curl|wget)\\b[^|]*\\|\\s*(?:sudo\\s+)?(?:ba|z|da)?sh\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Piping a remote download directly into a shell is blocked.\" };\n\t}\n\n\t// Dangerous permission bypass flag.\n\tif (/--dangerously-skip-permissions\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Use of --dangerously-skip-permissions is blocked.\" };\n\t}\n\n\t// Filesystem creation (mkfs) and raw disk writes (dd of=/dev/...).\n\tif (/\\bmkfs(?:\\.\\w+)?\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Filesystem creation (mkfs) is blocked.\" };\n\t}\n\tif (/\\bdd\\b[^\\n]*\\bof=\\/dev\\//.test(lower)) {\n\t\treturn { blocked: true, reason: \"Raw disk write (dd of=/dev/...) is blocked.\" };\n\t}\n\n\t// SQL: dropping a database.\n\tif (/\\bdrop\\s+database\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"DROP DATABASE is blocked.\" };\n\t}\n\n\t// Git: force push, pushing to main/master, hard reset, aggressive clean.\n\tif (/\\bgit\\s+push\\b/.test(lower)) {\n\t\tif (/\\bgit\\s+push\\b[^\\n]*(?:--force-with-lease\\b|--force\\b|\\s-f\\b)/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git push --force / --force-with-lease is blocked.\" };\n\t\t}\n\t\tif (\n\t\t\t/\\bgit\\s+push\\b[^\\n]*\\borigin\\b[^\\n]*\\b(?:main|master)\\b/.test(lower) ||\n\t\t\t/\\bgit\\s+push\\b\\s+(?:main|master)\\b/.test(lower)\n\t\t) {\n\t\t\treturn { blocked: true, reason: \"git push to main/master is blocked.\" };\n\t\t}\n\t}\n\tif (/\\bgit\\s+reset\\b[^\\n]*--hard\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"git reset --hard is blocked.\" };\n\t}\n\tif (/\\bgit\\s+clean\\b/.test(lower)) {\n\t\t// Match a single combined flag (e.g. -fdx, -xfd) containing f, d and x.\n\t\tconst cleanFlag = lower.match(/\\bgit\\s+clean\\b[^\\n]*?(-[a-z]+)/);\n\t\tif (cleanFlag && /f/.test(cleanFlag[1]) && /d/.test(cleanFlag[1]) && /x/.test(cleanFlag[1])) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t\t// Match separate flags (e.g. -f -d -x).\n\t\tif (/(?:^|\\s)-[a-z]*f/.test(lower) && /(?:^|\\s)-[a-z]*d/.test(lower) && /(?:^|\\s)-[a-z]*x/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t}\n\n\t// rm -rf targeting paths outside the workspace cwd.\n\t// Split on common command separators and inspect each rm segment that\n\t// carries both recursive and force flags.\n\tconst segments = normalized.split(/&&|\\|\\||[;&|]/);\n\tfor (const segment of segments) {\n\t\tif (!/\\brm\\b/.test(segment)) continue;\n\t\tif (!hasRecursiveForceFlags(segment)) continue;\n\t\tconst verdict = inspectRmTargets(segment, cwd);\n\t\tif (verdict.blocked) return verdict;\n\t}\n\n\treturn { blocked: false };\n}\n\n/**\n * Return true when an rm segment carries both a recursive flag (-r / -R) and a\n * force flag (-f), either combined (-rf, -fr) or as separate tokens.\n */\nfunction hasRecursiveForceFlags(segment: string): boolean {\n\tconst flagTokens = segment.split(/\\s+/).filter((t) => t.startsWith(\"-\") && !t.startsWith(\"--\"));\n\tlet recursive = false;\n\tlet force = false;\n\tfor (const flag of flagTokens) {\n\t\tif (/[rR]/.test(flag)) recursive = true;\n\t\tif (/f/.test(flag)) force = true;\n\t}\n\tif (/--recursive\\b/.test(segment)) recursive = true;\n\tif (/--force\\b/.test(segment)) force = true;\n\treturn recursive && force;\n}\n\n/**\n * Inspect the path arguments of an `rm -rf` segment and block targets that\n * clearly escape the workspace cwd (root, home, parent traversal, absolute\n * paths outside cwd, or broad wildcards). Relative paths that stay inside the\n * workspace are allowed.\n */\nfunction inspectRmTargets(rmSegment: string, cwd?: string): DestructiveCommandResult {\n\t// Strip the leading `rm` and its flag tokens, keep the operands.\n\tconst withoutRm = rmSegment.replace(/^.*?\\brm\\b/i, \"\");\n\tconst tokens = withoutRm.split(/\\s+/).filter((t) => t.length > 0 && !t.startsWith(\"-\"));\n\tfor (const rawToken of tokens) {\n\t\t// Unquote a leading/trailing quote pair for inspection.\n\t\tconst token = rawToken.replace(/^['\"]/, \"\").replace(/['\"]$/, \"\");\n\t\tif (token.length === 0) continue;\n\t\t// Root or near-root absolute targets.\n\t\tif (token === \"/\" || /^\\/\\*?$/.test(token)) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the filesystem root is blocked.\" };\n\t\t}\n\t\t// Home directory targets.\n\t\tif (token === \"~\" || token.startsWith(\"~/\") || token === \"$HOME\" || token.startsWith(\"$HOME/\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the home directory is blocked.\" };\n\t\t}\n\t\t// Parent-directory traversal escapes the workspace.\n\t\tif (token === \"..\" || token.startsWith(\"../\") || token.includes(\"/../\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf with parent-directory traversal (..) is blocked.\" };\n\t\t}\n\t\t// Absolute path: allow only if it stays inside the workspace cwd.\n\t\tif (token.startsWith(\"/\")) {\n\t\t\tconst target = token.replace(/\\*+$/, \"\");\n\t\t\tif (cwd) {\n\t\t\t\tconst normalizedCwd = cwd.replace(/[\\\\/]+$/, \"\");\n\t\t\t\tif (target === normalizedCwd || target.startsWith(`${normalizedCwd}/`)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting an absolute path outside the workspace is blocked.\" };\n\t\t}\n\t}\n\treturn { blocked: false };\n}\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst { shell, args } = getShellConfig(options?.shellPath);\n\t\t\t\tif (!existsSync(cwd)) {\n\t\t\t\t\treject(new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\t\tcwd,\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t\t// Avoid spawning a visible console window on Windows for the shell process.\n\t\t\t\t\twindowsHide: true,\n\t\t\t\t});\n\t\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\t\tlet timedOut = false;\n\t\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeout * 1000);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t};\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\twaitForChildProcess(child)\n\t\t\t\t\t.then((code) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\treturn theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tconst output = getTextOutput(result as any, showImages).trim();\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\t// Deterministic local safety gate: only active during autonomous\n\t\t\t// orchestration. In normal interactive use nothing is blocked.\n\t\t\tif ((globalThis as any).__phiOrchestrationActive === true) {\n\t\t\t\tconst verdict = isDestructiveCommand(command, cwd);\n\t\t\t\tif (verdict.blocked) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Command blocked by safety gate: ${verdict.reason ?? \"destructive command\"}\\n` +\n\t\t\t\t\t\t\t\"This guard is active only during autonomous orchestration.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAY5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAoD,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAExG,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,wBAAwB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,wBAAwB,CA8ExF;AA4DD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,CACL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QACR,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACxB,KACG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAgE1F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;AAO5E,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAKD,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;CACrC,CAAC;AAwGF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CAqLjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { spawn } from \"child_process\";\nimport type { AgentTool } from \"phi-code-agent\";\nimport { Container, Text, truncateToWidth } from \"phi-code-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { waitForChildProcess } from \"../../utils/child-process.js\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { OutputAccumulator } from \"./output-accumulator.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.js\";\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface DestructiveCommandResult {\n\tblocked: boolean;\n\treason?: string;\n}\n\n/**\n * Deterministic, local (zero API call) safety gate for destructive bash commands.\n *\n * Pure function: given a raw command string (and optionally the workspace cwd),\n * returns whether the command should be blocked and a human readable reason.\n *\n * This is intentionally conservative: a false negative (a dangerous command slips\n * through) is preferred over a false positive (a safe command is blocked). Only\n * patterns that are very clearly destructive are matched. The gate is meant to be\n * applied only during autonomous orchestration, never in normal interactive use.\n */\nexport function isDestructiveCommand(cmd: string, cwd?: string): DestructiveCommandResult {\n\tif (typeof cmd !== \"string\" || cmd.length === 0) {\n\t\treturn { blocked: false };\n\t}\n\t// Normalize for matching: collapse runs of whitespace to single spaces.\n\t// The original command is still used for path extraction below.\n\tconst normalized = cmd.replace(/\\s+/g, \" \").trim();\n\tconst lower = normalized.toLowerCase();\n\n\t// Fork bombs (classic shell fork bomb and common variants).\n\t// Compare against a whitespace-stripped form to catch spacing variants.\n\tconst dense = normalized.replace(/\\s+/g, \"\");\n\tif (/\\(\\)\\{:?\\|:?&?\\};:/.test(dense) || /\\w+\\(\\)\\{\\w+\\|\\w+&\\};/.test(dense)) {\n\t\treturn { blocked: true, reason: \"Fork bomb pattern detected.\" };\n\t}\n\n\t// Pipe a remote download straight into a shell interpreter (curl|sh, wget|sh, etc.).\n\tif (/\\b(?:curl|wget)\\b[^|]*\\|\\s*(?:sudo\\s+)?(?:ba|z|da)?sh\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Piping a remote download directly into a shell is blocked.\" };\n\t}\n\n\t// Dangerous permission bypass flag.\n\tif (/--dangerously-skip-permissions\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Use of --dangerously-skip-permissions is blocked.\" };\n\t}\n\n\t// Filesystem creation (mkfs) and raw disk writes (dd of=/dev/...).\n\tif (/\\bmkfs(?:\\.\\w+)?\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Filesystem creation (mkfs) is blocked.\" };\n\t}\n\tif (/\\bdd\\b[^\\n]*\\bof=\\/dev\\//.test(lower)) {\n\t\treturn { blocked: true, reason: \"Raw disk write (dd of=/dev/...) is blocked.\" };\n\t}\n\n\t// SQL: dropping a database.\n\tif (/\\bdrop\\s+database\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"DROP DATABASE is blocked.\" };\n\t}\n\n\t// Git: force push, pushing to main/master, hard reset, aggressive clean.\n\tif (/\\bgit\\s+push\\b/.test(lower)) {\n\t\tif (/\\bgit\\s+push\\b[^\\n]*(?:--force-with-lease\\b|--force\\b|\\s-f\\b)/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git push --force / --force-with-lease is blocked.\" };\n\t\t}\n\t\tif (\n\t\t\t/\\bgit\\s+push\\b[^\\n]*\\borigin\\b[^\\n]*\\b(?:main|master)\\b/.test(lower) ||\n\t\t\t/\\bgit\\s+push\\b\\s+(?:main|master)\\b/.test(lower)\n\t\t) {\n\t\t\treturn { blocked: true, reason: \"git push to main/master is blocked.\" };\n\t\t}\n\t}\n\tif (/\\bgit\\s+reset\\b[^\\n]*--hard\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"git reset --hard is blocked.\" };\n\t}\n\tif (/\\bgit\\s+clean\\b/.test(lower)) {\n\t\t// Match a single combined flag (e.g. -fdx, -xfd) containing f, d and x.\n\t\tconst cleanFlag = lower.match(/\\bgit\\s+clean\\b[^\\n]*?(-[a-z]+)/);\n\t\tif (cleanFlag && /f/.test(cleanFlag[1]) && /d/.test(cleanFlag[1]) && /x/.test(cleanFlag[1])) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t\t// Match separate flags (e.g. -f -d -x).\n\t\tif (/(?:^|\\s)-[a-z]*f/.test(lower) && /(?:^|\\s)-[a-z]*d/.test(lower) && /(?:^|\\s)-[a-z]*x/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t}\n\n\t// rm -rf targeting paths outside the workspace cwd.\n\t// Split on common command separators and inspect each rm segment that\n\t// carries both recursive and force flags.\n\tconst segments = normalized.split(/&&|\\|\\||[;&|]/);\n\tfor (const segment of segments) {\n\t\tif (!/\\brm\\b/.test(segment)) continue;\n\t\tif (!hasRecursiveForceFlags(segment)) continue;\n\t\tconst verdict = inspectRmTargets(segment, cwd);\n\t\tif (verdict.blocked) return verdict;\n\t}\n\n\treturn { blocked: false };\n}\n\n/**\n * Return true when an rm segment carries both a recursive flag (-r / -R) and a\n * force flag (-f), either combined (-rf, -fr) or as separate tokens.\n */\nfunction hasRecursiveForceFlags(segment: string): boolean {\n\tconst flagTokens = segment.split(/\\s+/).filter((t) => t.startsWith(\"-\") && !t.startsWith(\"--\"));\n\tlet recursive = false;\n\tlet force = false;\n\tfor (const flag of flagTokens) {\n\t\tif (/[rR]/.test(flag)) recursive = true;\n\t\tif (/f/.test(flag)) force = true;\n\t}\n\tif (/--recursive\\b/.test(segment)) recursive = true;\n\tif (/--force\\b/.test(segment)) force = true;\n\treturn recursive && force;\n}\n\n/**\n * Inspect the path arguments of an `rm -rf` segment and block targets that\n * clearly escape the workspace cwd (root, home, parent traversal, absolute\n * paths outside cwd, or broad wildcards). Relative paths that stay inside the\n * workspace are allowed.\n */\nfunction inspectRmTargets(rmSegment: string, cwd?: string): DestructiveCommandResult {\n\t// Strip the leading `rm` and its flag tokens, keep the operands.\n\tconst withoutRm = rmSegment.replace(/^.*?\\brm\\b/i, \"\");\n\tconst tokens = withoutRm.split(/\\s+/).filter((t) => t.length > 0 && !t.startsWith(\"-\"));\n\tfor (const rawToken of tokens) {\n\t\t// Unquote a leading/trailing quote pair for inspection.\n\t\tconst token = rawToken.replace(/^['\"]/, \"\").replace(/['\"]$/, \"\");\n\t\tif (token.length === 0) continue;\n\t\t// Root or near-root absolute targets.\n\t\tif (token === \"/\" || /^\\/\\*?$/.test(token)) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the filesystem root is blocked.\" };\n\t\t}\n\t\t// Home directory targets.\n\t\tif (token === \"~\" || token.startsWith(\"~/\") || token === \"$HOME\" || token.startsWith(\"$HOME/\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the home directory is blocked.\" };\n\t\t}\n\t\t// Parent-directory traversal escapes the workspace.\n\t\tif (token === \"..\" || token.startsWith(\"../\") || token.includes(\"/../\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf with parent-directory traversal (..) is blocked.\" };\n\t\t}\n\t\t// Absolute path: allow only if it stays inside the workspace cwd.\n\t\tif (token.startsWith(\"/\")) {\n\t\t\tconst target = token.replace(/\\*+$/, \"\");\n\t\t\tif (cwd) {\n\t\t\t\tconst normalizedCwd = cwd.replace(/[\\\\/]+$/, \"\");\n\t\t\t\tif (target === normalizedCwd || target.startsWith(`${normalizedCwd}/`)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting an absolute path outside the workspace is blocked.\" };\n\t\t}\n\t}\n\treturn { blocked: false };\n}\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst { shell, args } = getShellConfig(options?.shellPath);\n\t\t\t\tif (!existsSync(cwd)) {\n\t\t\t\t\treject(new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\t\tcwd,\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t\t// Avoid spawning a visible console window on Windows for the shell process.\n\t\t\t\t\twindowsHide: true,\n\t\t\t\t});\n\t\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\t\tlet timedOut = false;\n\t\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeout * 1000);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t};\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\twaitForChildProcess(child)\n\t\t\t\t\t.then((code) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\treturn theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tconst output = getTextOutput(result as any, showImages).trim();\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds. When the command is shown in the TUI or logs, describe it in a concise active voice imperative (\"List files\", \"Run the build\", \"Install dependencies\"), not \"Lists...\" or \"This command...\".`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\t// Deterministic local safety gate: only active during autonomous\n\t\t\t// orchestration. In normal interactive use nothing is blocked.\n\t\t\tif ((globalThis as any).__phiOrchestrationActive === true) {\n\t\t\t\tconst verdict = isDestructiveCommand(command, cwd);\n\t\t\t\tif (verdict.blocked) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Command blocked by safety gate: ${verdict.reason ?? \"destructive command\"}\\n` +\n\t\t\t\t\t\t\t\"This guard is active only during autonomous orchestration.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -330,7 +330,7 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
330
330
|
return {
|
|
331
331
|
name: "bash",
|
|
332
332
|
label: "bash",
|
|
333
|
-
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
|
333
|
+
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds. When the command is shown in the TUI or logs, describe it in a concise active voice imperative ("List files", "Run the build", "Install dependencies"), not "Lists..." or "This command...".`,
|
|
334
334
|
promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
|
|
335
335
|
parameters: bashSchema,
|
|
336
336
|
async execute(_toolCallId, { command, timeout }, signal, onUpdate, _ctx) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.js","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { spawn } from \"child_process\";\nimport type { AgentTool } from \"phi-code-agent\";\nimport { Container, Text, truncateToWidth } from \"phi-code-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { waitForChildProcess } from \"../../utils/child-process.js\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { OutputAccumulator } from \"./output-accumulator.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.js\";\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface DestructiveCommandResult {\n\tblocked: boolean;\n\treason?: string;\n}\n\n/**\n * Deterministic, local (zero API call) safety gate for destructive bash commands.\n *\n * Pure function: given a raw command string (and optionally the workspace cwd),\n * returns whether the command should be blocked and a human readable reason.\n *\n * This is intentionally conservative: a false negative (a dangerous command slips\n * through) is preferred over a false positive (a safe command is blocked). Only\n * patterns that are very clearly destructive are matched. The gate is meant to be\n * applied only during autonomous orchestration, never in normal interactive use.\n */\nexport function isDestructiveCommand(cmd: string, cwd?: string): DestructiveCommandResult {\n\tif (typeof cmd !== \"string\" || cmd.length === 0) {\n\t\treturn { blocked: false };\n\t}\n\t// Normalize for matching: collapse runs of whitespace to single spaces.\n\t// The original command is still used for path extraction below.\n\tconst normalized = cmd.replace(/\\s+/g, \" \").trim();\n\tconst lower = normalized.toLowerCase();\n\n\t// Fork bombs (classic shell fork bomb and common variants).\n\t// Compare against a whitespace-stripped form to catch spacing variants.\n\tconst dense = normalized.replace(/\\s+/g, \"\");\n\tif (/\\(\\)\\{:?\\|:?&?\\};:/.test(dense) || /\\w+\\(\\)\\{\\w+\\|\\w+&\\};/.test(dense)) {\n\t\treturn { blocked: true, reason: \"Fork bomb pattern detected.\" };\n\t}\n\n\t// Pipe a remote download straight into a shell interpreter (curl|sh, wget|sh, etc.).\n\tif (/\\b(?:curl|wget)\\b[^|]*\\|\\s*(?:sudo\\s+)?(?:ba|z|da)?sh\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Piping a remote download directly into a shell is blocked.\" };\n\t}\n\n\t// Dangerous permission bypass flag.\n\tif (/--dangerously-skip-permissions\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Use of --dangerously-skip-permissions is blocked.\" };\n\t}\n\n\t// Filesystem creation (mkfs) and raw disk writes (dd of=/dev/...).\n\tif (/\\bmkfs(?:\\.\\w+)?\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Filesystem creation (mkfs) is blocked.\" };\n\t}\n\tif (/\\bdd\\b[^\\n]*\\bof=\\/dev\\//.test(lower)) {\n\t\treturn { blocked: true, reason: \"Raw disk write (dd of=/dev/...) is blocked.\" };\n\t}\n\n\t// SQL: dropping a database.\n\tif (/\\bdrop\\s+database\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"DROP DATABASE is blocked.\" };\n\t}\n\n\t// Git: force push, pushing to main/master, hard reset, aggressive clean.\n\tif (/\\bgit\\s+push\\b/.test(lower)) {\n\t\tif (/\\bgit\\s+push\\b[^\\n]*(?:--force-with-lease\\b|--force\\b|\\s-f\\b)/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git push --force / --force-with-lease is blocked.\" };\n\t\t}\n\t\tif (\n\t\t\t/\\bgit\\s+push\\b[^\\n]*\\borigin\\b[^\\n]*\\b(?:main|master)\\b/.test(lower) ||\n\t\t\t/\\bgit\\s+push\\b\\s+(?:main|master)\\b/.test(lower)\n\t\t) {\n\t\t\treturn { blocked: true, reason: \"git push to main/master is blocked.\" };\n\t\t}\n\t}\n\tif (/\\bgit\\s+reset\\b[^\\n]*--hard\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"git reset --hard is blocked.\" };\n\t}\n\tif (/\\bgit\\s+clean\\b/.test(lower)) {\n\t\t// Match a single combined flag (e.g. -fdx, -xfd) containing f, d and x.\n\t\tconst cleanFlag = lower.match(/\\bgit\\s+clean\\b[^\\n]*?(-[a-z]+)/);\n\t\tif (cleanFlag && /f/.test(cleanFlag[1]) && /d/.test(cleanFlag[1]) && /x/.test(cleanFlag[1])) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t\t// Match separate flags (e.g. -f -d -x).\n\t\tif (/(?:^|\\s)-[a-z]*f/.test(lower) && /(?:^|\\s)-[a-z]*d/.test(lower) && /(?:^|\\s)-[a-z]*x/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t}\n\n\t// rm -rf targeting paths outside the workspace cwd.\n\t// Split on common command separators and inspect each rm segment that\n\t// carries both recursive and force flags.\n\tconst segments = normalized.split(/&&|\\|\\||[;&|]/);\n\tfor (const segment of segments) {\n\t\tif (!/\\brm\\b/.test(segment)) continue;\n\t\tif (!hasRecursiveForceFlags(segment)) continue;\n\t\tconst verdict = inspectRmTargets(segment, cwd);\n\t\tif (verdict.blocked) return verdict;\n\t}\n\n\treturn { blocked: false };\n}\n\n/**\n * Return true when an rm segment carries both a recursive flag (-r / -R) and a\n * force flag (-f), either combined (-rf, -fr) or as separate tokens.\n */\nfunction hasRecursiveForceFlags(segment: string): boolean {\n\tconst flagTokens = segment.split(/\\s+/).filter((t) => t.startsWith(\"-\") && !t.startsWith(\"--\"));\n\tlet recursive = false;\n\tlet force = false;\n\tfor (const flag of flagTokens) {\n\t\tif (/[rR]/.test(flag)) recursive = true;\n\t\tif (/f/.test(flag)) force = true;\n\t}\n\tif (/--recursive\\b/.test(segment)) recursive = true;\n\tif (/--force\\b/.test(segment)) force = true;\n\treturn recursive && force;\n}\n\n/**\n * Inspect the path arguments of an `rm -rf` segment and block targets that\n * clearly escape the workspace cwd (root, home, parent traversal, absolute\n * paths outside cwd, or broad wildcards). Relative paths that stay inside the\n * workspace are allowed.\n */\nfunction inspectRmTargets(rmSegment: string, cwd?: string): DestructiveCommandResult {\n\t// Strip the leading `rm` and its flag tokens, keep the operands.\n\tconst withoutRm = rmSegment.replace(/^.*?\\brm\\b/i, \"\");\n\tconst tokens = withoutRm.split(/\\s+/).filter((t) => t.length > 0 && !t.startsWith(\"-\"));\n\tfor (const rawToken of tokens) {\n\t\t// Unquote a leading/trailing quote pair for inspection.\n\t\tconst token = rawToken.replace(/^['\"]/, \"\").replace(/['\"]$/, \"\");\n\t\tif (token.length === 0) continue;\n\t\t// Root or near-root absolute targets.\n\t\tif (token === \"/\" || /^\\/\\*?$/.test(token)) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the filesystem root is blocked.\" };\n\t\t}\n\t\t// Home directory targets.\n\t\tif (token === \"~\" || token.startsWith(\"~/\") || token === \"$HOME\" || token.startsWith(\"$HOME/\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the home directory is blocked.\" };\n\t\t}\n\t\t// Parent-directory traversal escapes the workspace.\n\t\tif (token === \"..\" || token.startsWith(\"../\") || token.includes(\"/../\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf with parent-directory traversal (..) is blocked.\" };\n\t\t}\n\t\t// Absolute path: allow only if it stays inside the workspace cwd.\n\t\tif (token.startsWith(\"/\")) {\n\t\t\tconst target = token.replace(/\\*+$/, \"\");\n\t\t\tif (cwd) {\n\t\t\t\tconst normalizedCwd = cwd.replace(/[\\\\/]+$/, \"\");\n\t\t\t\tif (target === normalizedCwd || target.startsWith(`${normalizedCwd}/`)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting an absolute path outside the workspace is blocked.\" };\n\t\t}\n\t}\n\treturn { blocked: false };\n}\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst { shell, args } = getShellConfig(options?.shellPath);\n\t\t\t\tif (!existsSync(cwd)) {\n\t\t\t\t\treject(new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\t\tcwd,\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t\t// Avoid spawning a visible console window on Windows for the shell process.\n\t\t\t\t\twindowsHide: true,\n\t\t\t\t});\n\t\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\t\tlet timedOut = false;\n\t\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeout * 1000);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t};\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\twaitForChildProcess(child)\n\t\t\t\t\t.then((code) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\treturn theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tconst output = getTextOutput(result as any, showImages).trim();\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\t// Deterministic local safety gate: only active during autonomous\n\t\t\t// orchestration. In normal interactive use nothing is blocked.\n\t\t\tif ((globalThis as any).__phiOrchestrationActive === true) {\n\t\t\t\tconst verdict = isDestructiveCommand(command, cwd);\n\t\t\t\tif (verdict.blocked) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Command blocked by safety gate: ${verdict.reason ?? \"destructive command\"}\\n` +\n\t\t\t\t\t\t\t\"This guard is active only during autonomous orchestration.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bash.js","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { spawn } from \"child_process\";\nimport type { AgentTool } from \"phi-code-agent\";\nimport { Container, Text, truncateToWidth } from \"phi-code-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { waitForChildProcess } from \"../../utils/child-process.js\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { OutputAccumulator } from \"./output-accumulator.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.js\";\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface DestructiveCommandResult {\n\tblocked: boolean;\n\treason?: string;\n}\n\n/**\n * Deterministic, local (zero API call) safety gate for destructive bash commands.\n *\n * Pure function: given a raw command string (and optionally the workspace cwd),\n * returns whether the command should be blocked and a human readable reason.\n *\n * This is intentionally conservative: a false negative (a dangerous command slips\n * through) is preferred over a false positive (a safe command is blocked). Only\n * patterns that are very clearly destructive are matched. The gate is meant to be\n * applied only during autonomous orchestration, never in normal interactive use.\n */\nexport function isDestructiveCommand(cmd: string, cwd?: string): DestructiveCommandResult {\n\tif (typeof cmd !== \"string\" || cmd.length === 0) {\n\t\treturn { blocked: false };\n\t}\n\t// Normalize for matching: collapse runs of whitespace to single spaces.\n\t// The original command is still used for path extraction below.\n\tconst normalized = cmd.replace(/\\s+/g, \" \").trim();\n\tconst lower = normalized.toLowerCase();\n\n\t// Fork bombs (classic shell fork bomb and common variants).\n\t// Compare against a whitespace-stripped form to catch spacing variants.\n\tconst dense = normalized.replace(/\\s+/g, \"\");\n\tif (/\\(\\)\\{:?\\|:?&?\\};:/.test(dense) || /\\w+\\(\\)\\{\\w+\\|\\w+&\\};/.test(dense)) {\n\t\treturn { blocked: true, reason: \"Fork bomb pattern detected.\" };\n\t}\n\n\t// Pipe a remote download straight into a shell interpreter (curl|sh, wget|sh, etc.).\n\tif (/\\b(?:curl|wget)\\b[^|]*\\|\\s*(?:sudo\\s+)?(?:ba|z|da)?sh\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Piping a remote download directly into a shell is blocked.\" };\n\t}\n\n\t// Dangerous permission bypass flag.\n\tif (/--dangerously-skip-permissions\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Use of --dangerously-skip-permissions is blocked.\" };\n\t}\n\n\t// Filesystem creation (mkfs) and raw disk writes (dd of=/dev/...).\n\tif (/\\bmkfs(?:\\.\\w+)?\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"Filesystem creation (mkfs) is blocked.\" };\n\t}\n\tif (/\\bdd\\b[^\\n]*\\bof=\\/dev\\//.test(lower)) {\n\t\treturn { blocked: true, reason: \"Raw disk write (dd of=/dev/...) is blocked.\" };\n\t}\n\n\t// SQL: dropping a database.\n\tif (/\\bdrop\\s+database\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"DROP DATABASE is blocked.\" };\n\t}\n\n\t// Git: force push, pushing to main/master, hard reset, aggressive clean.\n\tif (/\\bgit\\s+push\\b/.test(lower)) {\n\t\tif (/\\bgit\\s+push\\b[^\\n]*(?:--force-with-lease\\b|--force\\b|\\s-f\\b)/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git push --force / --force-with-lease is blocked.\" };\n\t\t}\n\t\tif (\n\t\t\t/\\bgit\\s+push\\b[^\\n]*\\borigin\\b[^\\n]*\\b(?:main|master)\\b/.test(lower) ||\n\t\t\t/\\bgit\\s+push\\b\\s+(?:main|master)\\b/.test(lower)\n\t\t) {\n\t\t\treturn { blocked: true, reason: \"git push to main/master is blocked.\" };\n\t\t}\n\t}\n\tif (/\\bgit\\s+reset\\b[^\\n]*--hard\\b/.test(lower)) {\n\t\treturn { blocked: true, reason: \"git reset --hard is blocked.\" };\n\t}\n\tif (/\\bgit\\s+clean\\b/.test(lower)) {\n\t\t// Match a single combined flag (e.g. -fdx, -xfd) containing f, d and x.\n\t\tconst cleanFlag = lower.match(/\\bgit\\s+clean\\b[^\\n]*?(-[a-z]+)/);\n\t\tif (cleanFlag && /f/.test(cleanFlag[1]) && /d/.test(cleanFlag[1]) && /x/.test(cleanFlag[1])) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t\t// Match separate flags (e.g. -f -d -x).\n\t\tif (/(?:^|\\s)-[a-z]*f/.test(lower) && /(?:^|\\s)-[a-z]*d/.test(lower) && /(?:^|\\s)-[a-z]*x/.test(lower)) {\n\t\t\treturn { blocked: true, reason: \"git clean -fdx is blocked.\" };\n\t\t}\n\t}\n\n\t// rm -rf targeting paths outside the workspace cwd.\n\t// Split on common command separators and inspect each rm segment that\n\t// carries both recursive and force flags.\n\tconst segments = normalized.split(/&&|\\|\\||[;&|]/);\n\tfor (const segment of segments) {\n\t\tif (!/\\brm\\b/.test(segment)) continue;\n\t\tif (!hasRecursiveForceFlags(segment)) continue;\n\t\tconst verdict = inspectRmTargets(segment, cwd);\n\t\tif (verdict.blocked) return verdict;\n\t}\n\n\treturn { blocked: false };\n}\n\n/**\n * Return true when an rm segment carries both a recursive flag (-r / -R) and a\n * force flag (-f), either combined (-rf, -fr) or as separate tokens.\n */\nfunction hasRecursiveForceFlags(segment: string): boolean {\n\tconst flagTokens = segment.split(/\\s+/).filter((t) => t.startsWith(\"-\") && !t.startsWith(\"--\"));\n\tlet recursive = false;\n\tlet force = false;\n\tfor (const flag of flagTokens) {\n\t\tif (/[rR]/.test(flag)) recursive = true;\n\t\tif (/f/.test(flag)) force = true;\n\t}\n\tif (/--recursive\\b/.test(segment)) recursive = true;\n\tif (/--force\\b/.test(segment)) force = true;\n\treturn recursive && force;\n}\n\n/**\n * Inspect the path arguments of an `rm -rf` segment and block targets that\n * clearly escape the workspace cwd (root, home, parent traversal, absolute\n * paths outside cwd, or broad wildcards). Relative paths that stay inside the\n * workspace are allowed.\n */\nfunction inspectRmTargets(rmSegment: string, cwd?: string): DestructiveCommandResult {\n\t// Strip the leading `rm` and its flag tokens, keep the operands.\n\tconst withoutRm = rmSegment.replace(/^.*?\\brm\\b/i, \"\");\n\tconst tokens = withoutRm.split(/\\s+/).filter((t) => t.length > 0 && !t.startsWith(\"-\"));\n\tfor (const rawToken of tokens) {\n\t\t// Unquote a leading/trailing quote pair for inspection.\n\t\tconst token = rawToken.replace(/^['\"]/, \"\").replace(/['\"]$/, \"\");\n\t\tif (token.length === 0) continue;\n\t\t// Root or near-root absolute targets.\n\t\tif (token === \"/\" || /^\\/\\*?$/.test(token)) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the filesystem root is blocked.\" };\n\t\t}\n\t\t// Home directory targets.\n\t\tif (token === \"~\" || token.startsWith(\"~/\") || token === \"$HOME\" || token.startsWith(\"$HOME/\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting the home directory is blocked.\" };\n\t\t}\n\t\t// Parent-directory traversal escapes the workspace.\n\t\tif (token === \"..\" || token.startsWith(\"../\") || token.includes(\"/../\")) {\n\t\t\treturn { blocked: true, reason: \"rm -rf with parent-directory traversal (..) is blocked.\" };\n\t\t}\n\t\t// Absolute path: allow only if it stays inside the workspace cwd.\n\t\tif (token.startsWith(\"/\")) {\n\t\t\tconst target = token.replace(/\\*+$/, \"\");\n\t\t\tif (cwd) {\n\t\t\t\tconst normalizedCwd = cwd.replace(/[\\\\/]+$/, \"\");\n\t\t\t\tif (target === normalizedCwd || target.startsWith(`${normalizedCwd}/`)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { blocked: true, reason: \"rm -rf targeting an absolute path outside the workspace is blocked.\" };\n\t\t}\n\t}\n\treturn { blocked: false };\n}\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tconst { shell, args } = getShellConfig(options?.shellPath);\n\t\t\t\tif (!existsSync(cwd)) {\n\t\t\t\t\treject(new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\t\tcwd,\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t\t// Avoid spawning a visible console window on Windows for the shell process.\n\t\t\t\t\twindowsHide: true,\n\t\t\t\t});\n\t\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\t\tlet timedOut = false;\n\t\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeout * 1000);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t};\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\twaitForChildProcess(child)\n\t\t\t\t\t.then((code) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\t\treject(new Error(`timeout:${timeout}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t});\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\treturn theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tconst output = getTextOutput(result as any, showImages).trim();\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds. When the command is shown in the TUI or logs, describe it in a concise active voice imperative (\"List files\", \"Run the build\", \"Install dependencies\"), not \"Lists...\" or \"This command...\".`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\t// Deterministic local safety gate: only active during autonomous\n\t\t\t// orchestration. In normal interactive use nothing is blocked.\n\t\t\tif ((globalThis as any).__phiOrchestrationActive === true) {\n\t\t\t\tconst verdict = isDestructiveCommand(command, cwd);\n\t\t\t\tif (verdict.blocked) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Command blocked by safety gate: ${verdict.reason ?? \"destructive command\"}\\n` +\n\t\t\t\t\t\t\t\"This guard is active only during autonomous orchestration.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Extension - /commit command for deterministic, LLM-free git commits.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by examples/extensions/auto-commit-on-exit.ts, but exposed as an
|
|
5
|
+
* explicit user command and hardened with a strict safety protocol. There is
|
|
6
|
+
* NO model call anywhere in this file: the commit message is derived purely
|
|
7
|
+
* from git state and the local session, so the result is fully reproducible
|
|
8
|
+
* and never depends on a flaky structured-output proxy.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* /commit : commit what is already staged (no auto-staging)
|
|
12
|
+
* /commit <message> : commit already-staged changes with that message
|
|
13
|
+
* /commit --all : stage everything (git add -A) then commit
|
|
14
|
+
* /commit --all <message> : stage everything then commit with that message
|
|
15
|
+
*
|
|
16
|
+
* Message format: always prefixed with "[phi] ". When the user passes a
|
|
17
|
+
* message it is used verbatim after the prefix. Otherwise the message is
|
|
18
|
+
* derived deterministically from the staged files (git diff --staged --stat /
|
|
19
|
+
* git status) plus the first line of the last assistant message when present.
|
|
20
|
+
*
|
|
21
|
+
* Safety protocol (strict):
|
|
22
|
+
* - Refuses to commit if a sensitive file is staged (.env, *.pem, *secret*,
|
|
23
|
+
* id_rsa). The user is warned and nothing is committed.
|
|
24
|
+
* - Never uses --amend.
|
|
25
|
+
* - Only stages with `git add -A` when --all is passed; otherwise it commits
|
|
26
|
+
* strictly what is already in the index.
|
|
27
|
+
* - The message is passed via a single pi.exec("git", ["commit", "-m", msg])
|
|
28
|
+
* call: no shell string interpolation, robust cross-OS.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "phi-code";
|
|
32
|
+
|
|
33
|
+
/** Max length of the derived subject line (without the "[phi] " prefix). */
|
|
34
|
+
const MAX_SUBJECT_LENGTH = 60;
|
|
35
|
+
|
|
36
|
+
/** Max number of file names listed in an auto-derived commit message. */
|
|
37
|
+
const MAX_LISTED_FILES = 3;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Patterns for files that must never be committed by this command.
|
|
41
|
+
* Matched against the path of each staged file (case-insensitive on basename).
|
|
42
|
+
*/
|
|
43
|
+
const SENSITIVE_PATTERNS: { label: string; test: (path: string) => boolean }[] = [
|
|
44
|
+
{
|
|
45
|
+
label: ".env file",
|
|
46
|
+
test: (p) => {
|
|
47
|
+
const base = basename(p).toLowerCase();
|
|
48
|
+
return base === ".env" || base.startsWith(".env.");
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{ label: "PEM/key file", test: (p) => p.toLowerCase().endsWith(".pem") },
|
|
52
|
+
{ label: "secret-named file", test: (p) => basename(p).toLowerCase().includes("secret") },
|
|
53
|
+
{ label: "SSH private key", test: (p) => basename(p).toLowerCase() === "id_rsa" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Return the last path segment, tolerating both / and \ separators. */
|
|
57
|
+
function basename(path: string): string {
|
|
58
|
+
const normalized = path.replace(/\\/g, "/");
|
|
59
|
+
const idx = normalized.lastIndexOf("/");
|
|
60
|
+
return idx === -1 ? normalized : normalized.slice(idx + 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse `git status --porcelain` output into staged file paths.
|
|
65
|
+
*
|
|
66
|
+
* Porcelain v1 lines are "XY <path>" where X is the index (staged) status.
|
|
67
|
+
* A file is considered staged when X is not a space and not "?" (untracked).
|
|
68
|
+
* Rename entries use "orig -> dest"; we keep the destination path.
|
|
69
|
+
*/
|
|
70
|
+
function parseStagedFiles(porcelain: string): string[] {
|
|
71
|
+
const files: string[] = [];
|
|
72
|
+
for (const rawLine of porcelain.split("\n")) {
|
|
73
|
+
const line = rawLine.replace(/\r$/, "");
|
|
74
|
+
if (line.length < 4) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const indexStatus = line[0];
|
|
78
|
+
if (indexStatus === " " || indexStatus === "?") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let pathPart = line.slice(3).trim();
|
|
82
|
+
const renameSep = pathPart.indexOf(" -> ");
|
|
83
|
+
if (renameSep !== -1) {
|
|
84
|
+
pathPart = pathPart.slice(renameSep + 4).trim();
|
|
85
|
+
}
|
|
86
|
+
// Porcelain quotes paths containing special chars; strip surrounding quotes.
|
|
87
|
+
if (pathPart.startsWith('"') && pathPart.endsWith('"')) {
|
|
88
|
+
pathPart = pathPart.slice(1, -1);
|
|
89
|
+
}
|
|
90
|
+
if (pathPart.length > 0) {
|
|
91
|
+
files.push(pathPart);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Find the first sensitive file among the staged paths, if any. */
|
|
98
|
+
function findSensitiveFile(stagedFiles: string[]): { path: string; label: string } | undefined {
|
|
99
|
+
for (const path of stagedFiles) {
|
|
100
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
101
|
+
if (pattern.test(path)) {
|
|
102
|
+
return { path, label: pattern.label };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Extract the first line of the last assistant message from the session. */
|
|
110
|
+
function lastAssistantFirstLine(ctx: ExtensionCommandContext): string {
|
|
111
|
+
const entries = ctx.sessionManager.getEntries();
|
|
112
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
113
|
+
const entry = entries[i];
|
|
114
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const content = entry.message.content;
|
|
118
|
+
let text = "";
|
|
119
|
+
if (typeof content === "string") {
|
|
120
|
+
text = content;
|
|
121
|
+
} else if (Array.isArray(content)) {
|
|
122
|
+
text = content
|
|
123
|
+
.filter((c): c is { type: "text"; text: string } => c?.type === "text" && typeof c.text === "string")
|
|
124
|
+
.map((c) => c.text)
|
|
125
|
+
.join("\n");
|
|
126
|
+
}
|
|
127
|
+
const firstLine = text.split("\n").find((l) => l.trim().length > 0);
|
|
128
|
+
return firstLine ? firstLine.trim() : "";
|
|
129
|
+
}
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Collapse whitespace and clamp to MAX_SUBJECT_LENGTH with an ellipsis. */
|
|
134
|
+
function clampSubject(text: string): string {
|
|
135
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
136
|
+
if (collapsed.length <= MAX_SUBJECT_LENGTH) {
|
|
137
|
+
return collapsed;
|
|
138
|
+
}
|
|
139
|
+
return `${collapsed.slice(0, MAX_SUBJECT_LENGTH - 3)}...`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build a deterministic subject derived from the staged files plus the first
|
|
144
|
+
* line of the last assistant message. No LLM involved.
|
|
145
|
+
*/
|
|
146
|
+
function deriveSubject(stagedFiles: string[], assistantHint: string): string {
|
|
147
|
+
const names = stagedFiles.map(basename);
|
|
148
|
+
let filesPart: string;
|
|
149
|
+
if (names.length === 0) {
|
|
150
|
+
filesPart = "changes";
|
|
151
|
+
} else if (names.length <= MAX_LISTED_FILES) {
|
|
152
|
+
filesPart = names.join(", ");
|
|
153
|
+
} else {
|
|
154
|
+
const shown = names.slice(0, MAX_LISTED_FILES).join(", ");
|
|
155
|
+
filesPart = `${shown} (+${names.length - MAX_LISTED_FILES} more)`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const hint = assistantHint.trim();
|
|
159
|
+
const subject = hint.length > 0 ? `update ${filesPart}: ${hint}` : `update ${filesPart}`;
|
|
160
|
+
return clampSubject(subject);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default function commitExtension(pi: ExtensionAPI) {
|
|
164
|
+
pi.registerCommand("commit", {
|
|
165
|
+
description: "Deterministically commit staged changes ([phi] prefix, no LLM). Use --all to stage everything first.",
|
|
166
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
167
|
+
try {
|
|
168
|
+
// 1. Parse args: extract the --all flag, keep the rest as the message.
|
|
169
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
170
|
+
const stageAll = tokens.includes("--all");
|
|
171
|
+
const userMessage = tokens.filter((t) => t !== "--all").join(" ").trim();
|
|
172
|
+
|
|
173
|
+
// 2. Confirm we are inside a git repo with changes.
|
|
174
|
+
const status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
175
|
+
if (status.code !== 0) {
|
|
176
|
+
ctx.ui.notify(
|
|
177
|
+
"Not a git repository (or git unavailable). Nothing to commit.",
|
|
178
|
+
"warning",
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (status.stdout.trim().length === 0) {
|
|
183
|
+
ctx.ui.notify("Working tree clean: nothing to commit.", "info");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 3. Optionally stage everything (only when explicitly requested).
|
|
188
|
+
if (stageAll) {
|
|
189
|
+
const add = await pi.exec("git", ["add", "-A"], { cwd: ctx.cwd });
|
|
190
|
+
if (add.code !== 0) {
|
|
191
|
+
ctx.ui.notify(`git add -A failed: ${add.stderr.trim() || "unknown error"}`, "error");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 4. Re-read status to know exactly what is staged after any add.
|
|
197
|
+
const afterStatus = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
198
|
+
const stagedFiles = parseStagedFiles(afterStatus.stdout);
|
|
199
|
+
if (stagedFiles.length === 0) {
|
|
200
|
+
ctx.ui.notify(
|
|
201
|
+
"No staged changes to commit. Stage files first, or run `/commit --all`.",
|
|
202
|
+
"warning",
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 5. Safety protocol: refuse if a sensitive file is staged.
|
|
208
|
+
const sensitive = findSensitiveFile(stagedFiles);
|
|
209
|
+
if (sensitive) {
|
|
210
|
+
ctx.ui.notify(
|
|
211
|
+
`Refusing to commit: sensitive ${sensitive.label} is staged (\`${sensitive.path}\`). ` +
|
|
212
|
+
`Unstage it with \`git restore --staged ${sensitive.path}\` before committing.`,
|
|
213
|
+
"error",
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 6. Build the deterministic message.
|
|
219
|
+
let subject: string;
|
|
220
|
+
if (userMessage.length > 0) {
|
|
221
|
+
subject = userMessage;
|
|
222
|
+
} else {
|
|
223
|
+
const hint = lastAssistantFirstLine(ctx);
|
|
224
|
+
subject = deriveSubject(stagedFiles, hint);
|
|
225
|
+
}
|
|
226
|
+
const message = `[phi] ${subject}`;
|
|
227
|
+
|
|
228
|
+
// 7. Commit via a single exec call (no shell, no --amend).
|
|
229
|
+
const commit = await pi.exec("git", ["commit", "-m", message], { cwd: ctx.cwd });
|
|
230
|
+
if (commit.code !== 0) {
|
|
231
|
+
ctx.ui.notify(
|
|
232
|
+
`git commit failed: ${commit.stderr.trim() || commit.stdout.trim() || "unknown error"}`,
|
|
233
|
+
"error",
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 8. Report the result with the short hash.
|
|
239
|
+
const rev = await pi.exec("git", ["rev-parse", "--short", "HEAD"], { cwd: ctx.cwd });
|
|
240
|
+
const shortHash = rev.code === 0 ? rev.stdout.trim() : "(unknown)";
|
|
241
|
+
ctx.ui.notify(
|
|
242
|
+
`Committed ${shortHash}: ${message} (${stagedFiles.length} file${stagedFiles.length === 1 ? "" : "s"}).`,
|
|
243
|
+
"info",
|
|
244
|
+
);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
ctx.ui.notify(
|
|
247
|
+
`/commit error: ${err instanceof Error ? err.message : String(err)}`,
|
|
248
|
+
"error",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
package/extensions/phi/memory.ts
CHANGED
|
@@ -64,6 +64,113 @@ function withFrontmatter(content: string, name: string): string {
|
|
|
64
64
|
return `---\nname: "${name}"\ndescription: "${description}"\n---\n\n${content}`;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Hard caps for the deterministic recall manifest (no LLM involved). */
|
|
68
|
+
const MANIFEST_MAX_ENTRIES = 40;
|
|
69
|
+
const MANIFEST_MAX_CHARS = 2000;
|
|
70
|
+
const MANIFEST_DESC_MAX = 100;
|
|
71
|
+
// Chars reserved for the trailing "⦠(N notes total ā¦)" overflow line so the
|
|
72
|
+
// final manifest still respects MANIFEST_MAX_CHARS once it is appended.
|
|
73
|
+
const MANIFEST_OVERFLOW_RESERVE = 96;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract a one-line summary for a single note file, deterministically.
|
|
77
|
+
*
|
|
78
|
+
* Prefers the YAML frontmatter `description:` field (written by
|
|
79
|
+
* withFrontmatter). Falls back to the first non-empty, non-frontmatter line.
|
|
80
|
+
* Returns a trimmed, single-line string truncated to MANIFEST_DESC_MAX chars.
|
|
81
|
+
*/
|
|
82
|
+
function summarizeNote(content: string): string {
|
|
83
|
+
const text = content.replace(/\r\n/g, "\n");
|
|
84
|
+
let body = text;
|
|
85
|
+
|
|
86
|
+
// If a frontmatter block is present, scan it for a description first.
|
|
87
|
+
if (text.startsWith("---\n")) {
|
|
88
|
+
const end = text.indexOf("\n---", 4);
|
|
89
|
+
if (end !== -1) {
|
|
90
|
+
const fm = text.slice(4, end);
|
|
91
|
+
for (const rawLine of fm.split("\n")) {
|
|
92
|
+
const m = rawLine.match(/^\s*description\s*:\s*(.+?)\s*$/);
|
|
93
|
+
if (m) {
|
|
94
|
+
// Strip surrounding quotes if present.
|
|
95
|
+
const value = m[1].replace(/^["']|["']$/g, "").trim();
|
|
96
|
+
if (value) return collapseLine(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// No description in frontmatter: summarize the body that follows it.
|
|
100
|
+
body = text.slice(end + 4);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const rawLine of body.split("\n")) {
|
|
105
|
+
const line = rawLine.replace(/^#+\s*/, "").trim();
|
|
106
|
+
if (line && line !== "---") {
|
|
107
|
+
return collapseLine(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Collapse internal whitespace to single spaces and truncate cleanly. */
|
|
114
|
+
function collapseLine(value: string): string {
|
|
115
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
116
|
+
return flat.length > MANIFEST_DESC_MAX ? `${flat.slice(0, MANIFEST_DESC_MAX - 1).trimEnd()}ā¦` : flat;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a deterministic one-line-per-file manifest of all memory notes.
|
|
121
|
+
*
|
|
122
|
+
* Purely local: lists note files and reads their frontmatter / first line.
|
|
123
|
+
* No LLM call, no vector search. Bounded to MANIFEST_MAX_ENTRIES entries and
|
|
124
|
+
* MANIFEST_MAX_CHARS characters so it stays cheap to inject every turn.
|
|
125
|
+
*
|
|
126
|
+
* Returns an empty string when there are no notes, so callers can skip
|
|
127
|
+
* injection entirely.
|
|
128
|
+
*/
|
|
129
|
+
function buildMemoryManifest(sigmaMemory: SigmaMemory): string {
|
|
130
|
+
let files: Array<{ name: string; size: number; date: string }>;
|
|
131
|
+
try {
|
|
132
|
+
files = sigmaMemory.notes.list();
|
|
133
|
+
} catch {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
if (files.length === 0) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
let total = 0;
|
|
142
|
+
let truncated = false;
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
if (lines.length >= MANIFEST_MAX_ENTRIES) {
|
|
146
|
+
truncated = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
let summary = "";
|
|
150
|
+
try {
|
|
151
|
+
summary = summarizeNote(sigmaMemory.notes.read(file.name));
|
|
152
|
+
} catch {
|
|
153
|
+
// Unreadable note: still list its name so the model knows it exists.
|
|
154
|
+
summary = "";
|
|
155
|
+
}
|
|
156
|
+
const entry = summary ? `- ${file.name}: ${summary}` : `- ${file.name}`;
|
|
157
|
+
if (total + entry.length + 1 > MANIFEST_MAX_CHARS - MANIFEST_OVERFLOW_RESERVE) {
|
|
158
|
+
truncated = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
lines.push(entry);
|
|
162
|
+
total += entry.length + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (lines.length === 0) {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
if (truncated || lines.length < files.length) {
|
|
169
|
+
lines.push(`- ⦠(${files.length} notes total; use memory_read/memory_search for full content)`);
|
|
170
|
+
}
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
67
174
|
export default function memoryExtension(pi: ExtensionAPI) {
|
|
68
175
|
// Initialize sigma-memory with embedded vector store
|
|
69
176
|
const sigmaMemory = new SigmaMemory();
|
|
@@ -244,8 +351,19 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
244
351
|
)
|
|
245
352
|
.join("\n");
|
|
246
353
|
|
|
354
|
+
// Lead with the deterministic recall manifest (file + summary)
|
|
355
|
+
// so a no-arg memory_read surfaces what each note contains, not
|
|
356
|
+
// just file names. Falls back gracefully when empty.
|
|
357
|
+
const manifest = buildMemoryManifest(sigmaMemory);
|
|
358
|
+
const manifestSection = manifest ? `## Memory index (summaries)\n\n${manifest}\n\n` : "";
|
|
359
|
+
|
|
247
360
|
return {
|
|
248
|
-
content: [
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: `${manifestSection}## Available memory files (${files.length})\n\n${fileList}`,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
249
367
|
details: { action: "list", fileCount: files.length },
|
|
250
368
|
};
|
|
251
369
|
}
|
|
@@ -529,10 +647,19 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
529
647
|
// Neutralize angle brackets so user content cannot close the
|
|
530
648
|
// <system-reminder> block early and inject trusted instructions.
|
|
531
649
|
const safe = truncated.replace(/[<>]/g, (c) => (c === "<" ? "<" : ">")).replace(/"/g, '\\"');
|
|
650
|
+
|
|
651
|
+
// Deterministic recall index: a one-line-per-file manifest of memory
|
|
652
|
+
// notes so the model ALWAYS sees which facts exist without having to
|
|
653
|
+
// guess and call memory_search blindly. Built locally (no LLM).
|
|
654
|
+
const manifest = buildMemoryManifest(sigmaMemory);
|
|
655
|
+
const manifestBlock = manifest
|
|
656
|
+
? `\nMEMORY INDEX (existing saved notes, read with \`memory_read <file>\`):\n${manifest}\n`
|
|
657
|
+
: "";
|
|
658
|
+
|
|
532
659
|
const reminder = `<system-reminder>
|
|
533
660
|
You are about to respond to a new user message:
|
|
534
661
|
"${safe}"
|
|
535
|
-
|
|
662
|
+
${manifestBlock}
|
|
536
663
|
REMINDER (project rule, applies every turn):
|
|
537
664
|
1. Call \`memory_search\` FIRST with keywords from the user's intent. Recent
|
|
538
665
|
project context, prior decisions, and saved learnings are accessible
|
|
@@ -20,14 +20,14 @@ import { Type } from "@sinclair/typebox";
|
|
|
20
20
|
import type { ExtensionAPI } from "phi-code";
|
|
21
21
|
import { writeFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import {
|
|
26
26
|
extractBlockingFindings,
|
|
27
27
|
extractHandoff,
|
|
28
28
|
isTransientError,
|
|
29
29
|
parsePhaseVerdict,
|
|
30
|
-
} from "./orchestrator-helpers.js";
|
|
30
|
+
} from "./providers/orchestrator-helpers.js";
|
|
31
31
|
|
|
32
32
|
// āāā Types āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
33
33
|
|
|
@@ -229,6 +229,9 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
229
229
|
// When true, switchModelForPhase resolves the fallback model first (used on retry
|
|
230
230
|
// to swap to a different model family rather than re-hit the one that just failed).
|
|
231
231
|
useFallback?: boolean;
|
|
232
|
+
// 0..4 for the five base phases (explore..review); undefined for synthetic
|
|
233
|
+
// phases (the review fix + re-review). Used to checkpoint resume position.
|
|
234
|
+
baseIndex?: number;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
let phaseQueue: OrchestratorPhase[] = [];
|
|
@@ -251,6 +254,8 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
251
254
|
let currentPhase: OrchestratorPhase | null = null;
|
|
252
255
|
// Timestamp shared by all phase report file names in the current run.
|
|
253
256
|
let currentRunTs = "";
|
|
257
|
+
// The original /plan description, kept so a checkpoint can be resumed.
|
|
258
|
+
let currentDescription = "";
|
|
254
259
|
// Hard cap: at most one REVIEW->fix->re-REVIEW cycle per run.
|
|
255
260
|
let reviewFixRounds = 0;
|
|
256
261
|
// Set true right before an INTERNAL ctx.abort() (phase timeout) so the
|
|
@@ -266,6 +271,41 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
266
271
|
return null;
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
// āāā Resume checkpoint āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
275
|
+
// A tiny JSON marker tracking POSITION in the 5 base phases, so a crashed /
|
|
276
|
+
// aborted orchestration can be resumed with `/plan --resume`. The handoff .md
|
|
277
|
+
// files hold the real content; this only records where to pick up.
|
|
278
|
+
function checkpointPath(): string {
|
|
279
|
+
return join(process.cwd(), ".phi", "plans", "orchestration-state.json");
|
|
280
|
+
}
|
|
281
|
+
function writeCheckpoint(nextBaseIndex: number): void {
|
|
282
|
+
try {
|
|
283
|
+
if (nextBaseIndex >= 5) { clearCheckpoint(); return; }
|
|
284
|
+
writeFileSync(
|
|
285
|
+
checkpointPath(),
|
|
286
|
+
JSON.stringify({ description: currentDescription, ts: currentRunTs, nextBaseIndex }, null, 2),
|
|
287
|
+
"utf-8",
|
|
288
|
+
);
|
|
289
|
+
} catch { /* best effort */ }
|
|
290
|
+
}
|
|
291
|
+
function readCheckpoint(): { description: string; ts: string; nextBaseIndex: number } | null {
|
|
292
|
+
try {
|
|
293
|
+
const p = checkpointPath();
|
|
294
|
+
if (!existsSync(p)) return null;
|
|
295
|
+
const c = JSON.parse(readFileSync(p, "utf-8"));
|
|
296
|
+
if (typeof c?.description === "string" && typeof c?.ts === "string" && typeof c?.nextBaseIndex === "number") {
|
|
297
|
+
return c;
|
|
298
|
+
}
|
|
299
|
+
} catch { /* ignore */ }
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
function clearCheckpoint(): void {
|
|
303
|
+
try {
|
|
304
|
+
const p = checkpointPath();
|
|
305
|
+
if (existsSync(p)) unlinkSync(p);
|
|
306
|
+
} catch { /* best effort */ }
|
|
307
|
+
}
|
|
308
|
+
|
|
269
309
|
/**
|
|
270
310
|
* Common rules appended to every phase instruction. Pure prompt (zero latency,
|
|
271
311
|
* robust to the proxy): honest reporting, autonomous operation, a canonical
|
|
@@ -321,7 +361,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
321
361
|
* Load routing config and build phase queue with model assignments + agent definitions.
|
|
322
362
|
* Each phase now reads outputs from previous phases and writes structured outputs.
|
|
323
363
|
*/
|
|
324
|
-
function buildPhases(description: string): OrchestratorPhase[] {
|
|
364
|
+
function buildPhases(description: string, tsOverride?: string): OrchestratorPhase[] {
|
|
325
365
|
const routingPath = join(homedir(), ".phi", "agent", "routing.json");
|
|
326
366
|
let routing: any = { routes: {}, default: { model: "default" } };
|
|
327
367
|
try {
|
|
@@ -342,15 +382,15 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
342
382
|
const test = getModel("test");
|
|
343
383
|
const review = getModel("review");
|
|
344
384
|
|
|
345
|
-
const ts = timestamp();
|
|
346
|
-
currentRunTs = ts; // shared by all phase report file names this run
|
|
385
|
+
const ts = tsOverride || timestamp();
|
|
386
|
+
currentRunTs = ts; // shared by all phase report file names this run (or resumed)
|
|
347
387
|
// Inject runtime info so agents can adapt to the host OS
|
|
348
388
|
const shellNote = process.platform === 'win32'
|
|
349
389
|
? `\nShell: bash (Git Bash), NOT cmd.exe. Always use Unix syntax: rm not del, test -f not if exist, / not \\\\`
|
|
350
390
|
: '';
|
|
351
391
|
const runtimeInfo = `\n\nRuntime: ${process.platform} (${process.arch})${shellNote}`;
|
|
352
392
|
|
|
353
|
-
|
|
393
|
+
const phases: OrchestratorPhase[] = [
|
|
354
394
|
{
|
|
355
395
|
key: "explore", label: "š Phase 1 ā EXPLORE", model: explore.preferred, fallback: explore.fallback,
|
|
356
396
|
agent: loadAgentDef("explore"),
|
|
@@ -573,7 +613,7 @@ After testing, use \`memory_write\` to save test results, bugs found, and lesson
|
|
|
573
613
|
|
|
574
614
|
**Project Request:** ${description}
|
|
575
615
|
|
|
576
|
-
**Step 1:** Get the diff to review. Run \`git diff\` (and \`git status\`) if it is a git repo; otherwise read the files listed in the progress/handoff reports. Skip test/fixture hunks.
|
|
616
|
+
**Step 1:** Get the diff to review. Run \`git diff\` and \`git diff --stat\` (and \`git status\`) if it is a git repo; otherwise read the files listed in the progress/handoff reports. Skip test/fixture hunks. **Scale your effort to the diff size:** a small diff (under ~50 changed lines) gets one focused precision pass, bias to high confidence, at most 4 findings; a large diff gets all three angles below plus the verification step. Never spend so long that you risk the phase time limit.
|
|
577
617
|
**Step 2:** Review in SEQUENTIAL ANGLES (do all in this one turn, no sub-agents). Collect raw candidates first, judge after:
|
|
578
618
|
- **Angle 1 - Correctness:** read the diff line by line. What behaviour did it change or remove? Off-by-one, null/undefined, wrong condition, broken invariant.
|
|
579
619
|
- **Angle 2 - Cross-file:** grep the callers and callees of changed symbols. Did a signature/return/contract change break a caller elsewhere?
|
|
@@ -615,6 +655,8 @@ Tag the note with relevant keywords for vector search.
|
|
|
615
655
|
- Save any new architectural decisions or patterns discovered` + runtimeInfo + COMMON_PHASE_RULES,
|
|
616
656
|
},
|
|
617
657
|
];
|
|
658
|
+
phases.forEach((p, i) => { p.baseIndex = i; });
|
|
659
|
+
return phases;
|
|
618
660
|
}
|
|
619
661
|
|
|
620
662
|
/**
|
|
@@ -752,6 +794,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
752
794
|
// All phases done ā clean up and notify
|
|
753
795
|
setOrchestrationActive(false);
|
|
754
796
|
phasePending = false;
|
|
797
|
+
clearCheckpoint();
|
|
755
798
|
deactivateAgent();
|
|
756
799
|
// Restore the user's model so /plan does not overwrite their /model choice.
|
|
757
800
|
if (originalModel) {
|
|
@@ -1027,6 +1070,12 @@ Tag the note with relevant keywords for vector search.
|
|
|
1027
1070
|
}
|
|
1028
1071
|
} catch { /* verdict logic is best-effort */ }
|
|
1029
1072
|
|
|
1073
|
+
// Checkpoint resume position after each base phase (synthetic fix/re-review
|
|
1074
|
+
// phases have no baseIndex and do not advance it).
|
|
1075
|
+
if (currentPhase && typeof currentPhase.baseIndex === "number") {
|
|
1076
|
+
writeCheckpoint(currentPhase.baseIndex + 1);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1030
1079
|
// Phase complete ā chain to next
|
|
1031
1080
|
completedPhases++;
|
|
1032
1081
|
phasePending = false;
|
|
@@ -1038,10 +1087,51 @@ Tag the note with relevant keywords for vector search.
|
|
|
1038
1087
|
pi.registerCommand("plan", {
|
|
1039
1088
|
description: "Plan AND execute a project ā 5 phases, each with its own model from routing.json",
|
|
1040
1089
|
handler: async (args, ctx) => {
|
|
1041
|
-
const
|
|
1090
|
+
const rawArgs = args.trim();
|
|
1091
|
+
|
|
1092
|
+
// Resume a crashed / aborted orchestration from its checkpoint.
|
|
1093
|
+
if (/^(--resume|resume)\b/i.test(rawArgs)) {
|
|
1094
|
+
const cp = readCheckpoint();
|
|
1095
|
+
if (!cp) {
|
|
1096
|
+
ctx.ui.notify("No orchestration checkpoint to resume. Start one with `/plan <description>`.", "warning");
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
await ensurePlansDir();
|
|
1100
|
+
currentDescription = cp.description;
|
|
1101
|
+
// Reuse the saved timestamp so the rebuilt phases point at the same
|
|
1102
|
+
// .phi/plans/*.md handoff files that the earlier run produced.
|
|
1103
|
+
const rphases = buildPhases(cp.description, cp.ts);
|
|
1104
|
+
const remaining = rphases.slice(Math.max(0, Math.min(5, cp.nextBaseIndex)));
|
|
1105
|
+
if (remaining.length === 0) {
|
|
1106
|
+
clearCheckpoint();
|
|
1107
|
+
ctx.ui.notify("Checkpoint already complete ā nothing to resume.", "info");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
setOrchestrationActive(true);
|
|
1111
|
+
phasePending = true;
|
|
1112
|
+
originalModel = ctx.model || null;
|
|
1113
|
+
savedTools = pi.getActiveTools();
|
|
1114
|
+
completedPhases = cp.nextBaseIndex;
|
|
1115
|
+
skippedPhases = 0;
|
|
1116
|
+
reviewFixRounds = 0;
|
|
1117
|
+
internalAbort = false;
|
|
1118
|
+
phaseStartTime = Date.now();
|
|
1119
|
+
const firstResume = remaining[0];
|
|
1120
|
+
currentPhase = firstResume;
|
|
1121
|
+
phaseQueue = remaining.slice(1);
|
|
1122
|
+
ctx.ui.notify(`š **Resuming orchestration** at ${firstResume.label} (${remaining.length} phase(s) left)\n`, "info");
|
|
1123
|
+
const { modelId, warning } = await switchModelForPhase(firstResume, ctx);
|
|
1124
|
+
activateAgent(firstResume, ctx);
|
|
1125
|
+
ctx.ui.notify(`${firstResume.label} ā \`${modelId}\``, "info");
|
|
1126
|
+
if (warning) ctx.ui.notify(`\nā ļø ${warning}`, "warning");
|
|
1127
|
+
setTimeout(() => pi.sendUserMessage(firstResume.instruction, { deliverAs: "followUp" }), 200);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const description = rawArgs;
|
|
1042
1132
|
|
|
1043
1133
|
if (!description) {
|
|
1044
|
-
ctx.ui.notify(`**Usage:** \`/plan <project description>\`
|
|
1134
|
+
ctx.ui.notify(`**Usage:** \`/plan <project description>\` (or \`/plan --resume\` to continue a checkpointed run)
|
|
1045
1135
|
|
|
1046
1136
|
**Examples:**
|
|
1047
1137
|
/plan Build a REST API for user authentication with JWT
|
|
@@ -1060,6 +1150,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
1060
1150
|
await writeFile(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
|
|
1061
1151
|
|
|
1062
1152
|
// Build phases with model assignments + agent definitions
|
|
1153
|
+
currentDescription = description;
|
|
1063
1154
|
const phases = buildPhases(description);
|
|
1064
1155
|
phaseQueue = phases.slice(1); // Queue phases 2-5
|
|
1065
1156
|
setOrchestrationActive(true);
|
|
@@ -1072,9 +1163,9 @@ Tag the note with relevant keywords for vector search.
|
|
|
1072
1163
|
completedPhases = 0;
|
|
1073
1164
|
skippedPhases = 0;
|
|
1074
1165
|
reviewFixRounds = 0;
|
|
1075
|
-
currentPhase = null;
|
|
1076
1166
|
internalAbort = false;
|
|
1077
1167
|
const firstPhase = phases[0];
|
|
1168
|
+
currentPhase = firstPhase;
|
|
1078
1169
|
|
|
1079
1170
|
ctx.ui.notify(`š **Orchestrator started** ā 5 phases with model routing + agent roles\n`, "info");
|
|
1080
1171
|
|
package/package.json
CHANGED
|
File without changes
|