@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2

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.
Files changed (278) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +18 -3
  2. package/dist/agent/agent-definition-registry.js +47 -18
  3. package/dist/agent/agent-definition.d.ts +17 -0
  4. package/dist/agent/agent-definition.js +22 -1
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +114 -19
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +208 -14
  68. package/dist/engine/engine.js +754 -169
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +31 -6
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +4 -0
  171. package/dist/prompt/composer.js +28 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +33 -0
  220. package/dist/settings/manager.js +94 -1
  221. package/dist/settings/personalization.d.ts +19 -0
  222. package/dist/settings/personalization.js +7 -0
  223. package/dist/settings/schema.d.ts +1142 -126
  224. package/dist/settings/schema.js +119 -10
  225. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  226. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  227. package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
  228. package/dist/tool-system/builtin/agent-registry.js +8 -4
  229. package/dist/tool-system/builtin/agent.d.ts +29 -10
  230. package/dist/tool-system/builtin/agent.js +108 -52
  231. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  232. package/dist/tool-system/builtin/arena.js +6 -7
  233. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  234. package/dist/tool-system/builtin/complete-goal.js +45 -0
  235. package/dist/tool-system/builtin/cron.js +53 -9
  236. package/dist/tool-system/builtin/edit.d.ts +2 -1
  237. package/dist/tool-system/builtin/edit.js +6 -1
  238. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  239. package/dist/tool-system/builtin/generate-image.js +146 -0
  240. package/dist/tool-system/builtin/glob.js +7 -0
  241. package/dist/tool-system/builtin/grep.js +6 -0
  242. package/dist/tool-system/builtin/index.d.ts +18 -1
  243. package/dist/tool-system/builtin/index.js +62 -1
  244. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  245. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  246. package/dist/tool-system/builtin/read.d.ts +2 -1
  247. package/dist/tool-system/builtin/read.js +9 -1
  248. package/dist/tool-system/builtin/sleep.js +9 -3
  249. package/dist/tool-system/builtin/task.js +4 -1
  250. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  251. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  252. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  253. package/dist/tool-system/builtin/view-image.js +96 -0
  254. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  255. package/dist/tool-system/builtin/web-search.js +36 -3
  256. package/dist/tool-system/builtin/write.d.ts +2 -1
  257. package/dist/tool-system/builtin/write.js +7 -1
  258. package/dist/tool-system/context.d.ts +12 -0
  259. package/dist/tool-system/executor.d.ts +0 -1
  260. package/dist/tool-system/executor.js +34 -40
  261. package/dist/tool-system/investigation-guard.d.ts +4 -1
  262. package/dist/tool-system/investigation-guard.js +19 -3
  263. package/dist/tool-system/mcp-manager.d.ts +36 -12
  264. package/dist/tool-system/mcp-manager.js +173 -10
  265. package/dist/tool-system/path-policy.d.ts +94 -0
  266. package/dist/tool-system/path-policy.js +279 -0
  267. package/dist/tool-system/permission.js +43 -50
  268. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  269. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  270. package/dist/tool-system/registry.js +12 -1
  271. package/dist/tool-system/validation.d.ts +10 -0
  272. package/dist/tool-system/validation.js +16 -3
  273. package/dist/types.d.ts +90 -15
  274. package/dist/utils/format.js +4 -2
  275. package/dist/utils/lockfile.js +9 -2
  276. package/dist/utils/theme.d.ts +6 -0
  277. package/dist/utils/theme.js +4 -2
  278. package/package.json +2 -1
@@ -0,0 +1,13 @@
1
+ export interface GitLogEntry {
2
+ hash: string;
3
+ message: string;
4
+ author: string;
5
+ date: string;
6
+ }
7
+ /**
8
+ * Parse `git log --format=%H|%s|%an|%ci` output into entries. Tolerates
9
+ * malformed/short lines (missing `|` separators) by defaulting absent fields
10
+ * to "" rather than destructuring `undefined` and crashing on `.slice`
11
+ * (review-2026-05-30). Blank lines are skipped.
12
+ */
13
+ export declare function parseGitLog(raw: string): GitLogEntry[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Parse `git log --format=%H|%s|%an|%ci` output into entries. Tolerates
3
+ * malformed/short lines (missing `|` separators) by defaulting absent fields
4
+ * to "" rather than destructuring `undefined` and crashing on `.slice`
5
+ * (review-2026-05-30). Blank lines are skipped.
6
+ */
7
+ export function parseGitLog(raw) {
8
+ return raw
9
+ .split("\n")
10
+ .filter((line) => line.trim() !== "")
11
+ .map((line) => {
12
+ const parts = line.split("|");
13
+ const hash = parts[0] ?? "";
14
+ return {
15
+ hash: hash.slice(0, 8),
16
+ message: parts[1] ?? "",
17
+ author: parts[2] ?? "",
18
+ date: parts[3] ?? "",
19
+ };
20
+ });
21
+ }
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Git utility functions for the git workflow commands.
3
+ *
4
+ * Every call goes through execFileSync with an argv array — no command
5
+ * strings, no shell interpolation. Even arguments that look "safe" (already
6
+ * quoted, hard-coded) are passed as separate argv tokens so a future caller
7
+ * can't accidentally widen the attack surface by interpolating user input
8
+ * into the string form.
3
9
  */
10
+ import { type GitLogEntry } from "./parse-log.js";
4
11
  export interface GitStatusEntry {
5
12
  status: string;
6
13
  path: string;
7
14
  }
8
- export interface GitLogEntry {
9
- hash: string;
10
- message: string;
11
- author: string;
12
- date: string;
13
- }
15
+ export type { GitLogEntry };
14
16
  export declare function isGitRepo(cwd: string): boolean;
15
17
  export declare function getCurrentBranch(cwd: string): string;
16
18
  export declare function getGitStatus(cwd: string): GitStatusEntry[];
package/dist/git/utils.js CHANGED
@@ -1,10 +1,25 @@
1
1
  /**
2
2
  * Git utility functions for the git workflow commands.
3
+ *
4
+ * Every call goes through execFileSync with an argv array — no command
5
+ * strings, no shell interpolation. Even arguments that look "safe" (already
6
+ * quoted, hard-coded) are passed as separate argv tokens so a future caller
7
+ * can't accidentally widen the attack surface by interpolating user input
8
+ * into the string form.
3
9
  */
4
- import { execSync } from "node:child_process";
10
+ import { execFileSync } from "node:child_process";
11
+ import { parseGitLog } from "./parse-log.js";
12
+ /** Run git with an argv array and return its trimmed stdout. */
13
+ function git(cwd, args, timeoutMs = 10000) {
14
+ return execFileSync("git", args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
15
+ }
16
+ /** Run gh with an argv array and return its trimmed stdout. */
17
+ function gh(cwd, args, timeoutMs = 10000) {
18
+ return execFileSync("gh", args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
19
+ }
5
20
  export function isGitRepo(cwd) {
6
21
  try {
7
- execSync("git rev-parse --is-inside-work-tree", { cwd, encoding: "utf-8", timeout: 5000 });
22
+ git(cwd, ["rev-parse", "--is-inside-work-tree"], 5000);
8
23
  return true;
9
24
  }
10
25
  catch {
@@ -12,10 +27,10 @@ export function isGitRepo(cwd) {
12
27
  }
13
28
  }
14
29
  export function getCurrentBranch(cwd) {
15
- return execSync("git branch --show-current", { cwd, encoding: "utf-8", timeout: 5000 }).trim();
30
+ return git(cwd, ["branch", "--show-current"], 5000);
16
31
  }
17
32
  export function getGitStatus(cwd) {
18
- const raw = execSync("git status --porcelain", { cwd, encoding: "utf-8", timeout: 10000 }).trim();
33
+ const raw = git(cwd, ["status", "--porcelain"], 10000);
19
34
  if (!raw)
20
35
  return [];
21
36
  return raw.split("\n").map((line) => ({
@@ -24,51 +39,58 @@ export function getGitStatus(cwd) {
24
39
  }));
25
40
  }
26
41
  export function getGitDiff(cwd, opts) {
27
- const args = ["git", "diff", "--no-color"];
42
+ const args = ["diff", "--no-color"];
28
43
  if (opts?.staged)
29
44
  args.push("--staged");
45
+ // `--` terminates option parsing so a file path starting with `-` can't
46
+ // be re-interpreted as a flag.
30
47
  if (opts?.file)
31
48
  args.push("--", opts.file);
32
- return execSync(args.join(" "), { cwd, encoding: "utf-8", timeout: 30000 }).trim();
49
+ return git(cwd, args, 30000);
33
50
  }
34
51
  export function getGitDiffStat(cwd, opts) {
35
- const args = ["git", "diff", "--stat", "--no-color"];
52
+ const args = ["diff", "--stat", "--no-color"];
36
53
  if (opts?.staged)
37
54
  args.push("--staged");
38
55
  if (opts?.file)
39
56
  args.push("--", opts.file);
40
- return execSync(args.join(" "), { cwd, encoding: "utf-8", timeout: 10000 }).trim();
57
+ return git(cwd, args, 10000);
41
58
  }
42
59
  export function getGitLog(cwd, n = 10) {
43
- const raw = execSync(`git log --oneline --format="%H|%s|%an|%ci" -${n}`, { cwd, encoding: "utf-8", timeout: 10000 }).trim();
60
+ // `n` is numeric coerce/validate to keep the argv clean even if a caller
61
+ // hands us a string.
62
+ const count = Number.isFinite(n) && n > 0 ? Math.floor(n) : 10;
63
+ const raw = git(cwd, ["log", "--oneline", "--format=%H|%s|%an|%ci", `-${count}`], 10000);
44
64
  if (!raw)
45
65
  return [];
46
- return raw.split("\n").map((line) => {
47
- const [hash, message, author, date] = line.split("|");
48
- return { hash: hash.slice(0, 8), message, author, date };
49
- });
66
+ return parseGitLog(raw);
50
67
  }
51
68
  export function getRemoteUrl(cwd) {
52
69
  try {
53
- return execSync("git remote get-url origin", { cwd, encoding: "utf-8", timeout: 5000 }).trim();
70
+ return git(cwd, ["remote", "get-url", "origin"], 5000);
54
71
  }
55
72
  catch {
56
73
  return undefined;
57
74
  }
58
75
  }
59
76
  export function gitAdd(cwd, files = ["."]) {
60
- const fileArgs = files.map((f) => `"${f}"`).join(" ");
61
- execSync(`git add ${fileArgs}`, { cwd, timeout: 10000 });
77
+ // `--` ensures a path starting with `-` cannot be parsed as a flag.
78
+ // Each file is its own argv token, so spaces / quotes / non-ASCII pass
79
+ // through verbatim with no shell parsing.
80
+ execFileSync("git", ["add", "--", ...files], { cwd, timeout: 10000 });
62
81
  }
63
82
  export function gitCommit(cwd, message) {
64
- return execSync(`git commit -m ${JSON.stringify(message)}`, {
83
+ // Pre-fix this used `JSON.stringify(message)` which only happened to be
84
+ // safe because JSON.stringify covers most shell metacharacters — but it's
85
+ // not real escaping. The argv form is.
86
+ return execFileSync("git", ["commit", "-m", message], {
65
87
  cwd,
66
88
  encoding: "utf-8",
67
89
  timeout: 30000,
68
90
  }).trim();
69
91
  }
70
92
  export function gitListBranches(cwd) {
71
- const raw = execSync("git branch --no-color", { cwd, encoding: "utf-8", timeout: 5000 }).trim();
93
+ const raw = git(cwd, ["branch", "--no-color"], 5000);
72
94
  if (!raw)
73
95
  return [];
74
96
  return raw.split("\n").map((line) => ({
@@ -77,12 +99,22 @@ export function gitListBranches(cwd) {
77
99
  }));
78
100
  }
79
101
  export function gitCheckout(cwd, branch, create = false) {
80
- const flag = create ? "-b" : "";
81
- execSync(`git checkout ${flag} ${branch}`, { cwd, timeout: 10000 });
102
+ // argv form defeats shell injection. We additionally reject branch names
103
+ // that start with `-` so a value like `--orphan` can't slip through git's
104
+ // own option parser (the trailing `--` trick doesn't help on checkout:
105
+ // git interprets it as the option's value, not as an option terminator).
106
+ if (typeof branch !== "string" || branch.length === 0) {
107
+ throw new Error("branch must be a non-empty string");
108
+ }
109
+ if (branch.startsWith("-")) {
110
+ throw new Error(`refusing branch name that starts with '-': ${branch}`);
111
+ }
112
+ const args = create ? ["checkout", "-b", branch] : ["checkout", branch];
113
+ execFileSync("git", args, { cwd, timeout: 10000 });
82
114
  }
83
115
  export function ghAvailable() {
84
116
  try {
85
- execSync("gh --version", { encoding: "utf-8", timeout: 5000 });
117
+ execFileSync("gh", ["--version"], { encoding: "utf-8", timeout: 5000 });
86
118
  return true;
87
119
  }
88
120
  catch {
@@ -90,9 +122,8 @@ export function ghAvailable() {
90
122
  }
91
123
  }
92
124
  export function ghPrComments(cwd, prUrl) {
93
- return execSync(`gh pr view ${prUrl} --comments --json comments -q ".comments[].body"`, {
94
- cwd,
95
- encoding: "utf-8",
96
- timeout: 30000,
97
- }).trim();
125
+ // Pre-fix `gh pr view ${prUrl}` interpolated the URL directly. argv form
126
+ // means even a value like "$(rm -rf ~)" is sent to gh as a literal
127
+ // positional argument (which gh will then reject as a bad PR URL).
128
+ return gh(cwd, ["pr", "view", prUrl, "--comments", "--json", "comments", "-q", ".comments[].body"], 30000);
98
129
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Git worktree management — create/remove isolated worktrees for agents.
3
3
  */
4
- import { execSync } from "node:child_process";
4
+ import { execFileSync } from "node:child_process";
5
5
  import { existsSync, symlinkSync, lstatSync } from "node:fs";
6
- import { join, resolve } from "node:path";
6
+ import { join, resolve, dirname } from "node:path";
7
7
  /**
8
8
  * Validate worktree slug to prevent path traversal attacks.
9
9
  */
@@ -19,7 +19,7 @@ export function validateWorktreeSlug(slug) {
19
19
  * Find the canonical git root (resolves worktree → main repo).
20
20
  */
21
21
  export function findGitRoot(cwd) {
22
- return execSync("git rev-parse --show-toplevel", { cwd, encoding: "utf-8", timeout: 5000 }).trim();
22
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", timeout: 5000 }).trim();
23
23
  }
24
24
  /**
25
25
  * Create an isolated git worktree for an agent session.
@@ -32,7 +32,7 @@ export function createWorktree(cwd, slug, sessionId) {
32
32
  // Get current branch for later reference
33
33
  let originalBranch;
34
34
  try {
35
- originalBranch = execSync("git branch --show-current", {
35
+ originalBranch = execFileSync("git", ["branch", "--show-current"], {
36
36
  cwd: gitRoot,
37
37
  encoding: "utf-8",
38
38
  timeout: 5000,
@@ -41,8 +41,10 @@ export function createWorktree(cwd, slug, sessionId) {
41
41
  catch {
42
42
  // Detached HEAD
43
43
  }
44
- // Create the worktree
45
- execSync(`git worktree add -b "${branchName}" "${worktreePath}"`, {
44
+ // Create the worktree. Pre-fix this used a quoted command string; the argv
45
+ // form keeps branchName/worktreePath as literal positional arguments even
46
+ // if a future caller bypasses validateWorktreeSlug.
47
+ execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath], {
46
48
  cwd: gitRoot,
47
49
  timeout: 30000,
48
50
  });
@@ -63,25 +65,38 @@ export function createWorktree(cwd, slug, sessionId) {
63
65
  */
64
66
  export function removeWorktree(worktreePath, removeBranch = false) {
65
67
  try {
66
- const gitRoot = findGitRoot(worktreePath);
67
- execSync(`git worktree remove "${worktreePath}" --force`, {
68
- cwd: gitRoot,
69
- timeout: 30000,
70
- });
68
+ // The MAIN repo root, not the worktree's own toplevel. `git rev-parse
69
+ // --show-toplevel` from inside a worktree returns the worktree path, which
70
+ // is about to be deleted; the branch-delete must run from the main repo,
71
+ // which outlives the worktree. Derive it from the common git dir.
72
+ const commonDir = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: worktreePath, encoding: "utf-8", timeout: 5000 }).trim();
73
+ const mainRoot = dirname(commonDir); // <main>/.git → <main>
74
+ // Capture the worktree's branch BEFORE removing the worktree — afterwards
75
+ // the directory is gone and `git branch --show-current` in it would fail
76
+ // (silently, leaving removeBranch a no-op).
77
+ let branch = "";
71
78
  if (removeBranch) {
72
- // Extract branch name from worktree
73
79
  try {
74
- const branch = execSync("git branch --show-current", {
80
+ branch = execFileSync("git", ["branch", "--show-current"], {
75
81
  cwd: worktreePath,
76
82
  encoding: "utf-8",
77
83
  timeout: 5000,
78
84
  }).trim();
79
- if (branch.startsWith("worktree/")) {
80
- execSync(`git branch -D "${branch}"`, { cwd: gitRoot, timeout: 10000 });
81
- }
82
85
  }
83
86
  catch {
84
- // Branch might already be removed
87
+ // Detached HEAD or unreadable — nothing to delete.
88
+ }
89
+ }
90
+ execFileSync("git", ["worktree", "remove", worktreePath, "--force"], {
91
+ cwd: mainRoot,
92
+ timeout: 30000,
93
+ });
94
+ if (removeBranch && branch.startsWith("worktree/")) {
95
+ try {
96
+ execFileSync("git", ["branch", "-D", branch], { cwd: mainRoot, timeout: 10000 });
97
+ }
98
+ catch {
99
+ // Branch might already be removed.
85
100
  }
86
101
  }
87
102
  }
@@ -93,7 +108,7 @@ export function removeWorktree(worktreePath, removeBranch = false) {
93
108
  * List active worktrees.
94
109
  */
95
110
  export function listWorktrees(cwd) {
96
- const raw = execSync("git worktree list --porcelain", {
111
+ const raw = execFileSync("git", ["worktree", "list", "--porcelain"], {
97
112
  cwd,
98
113
  encoding: "utf-8",
99
114
  timeout: 10000,
@@ -23,6 +23,21 @@
23
23
  * may return `messages` to inject a
24
24
  * per-turn reminder appended to the
25
25
  * conversation right before the model call.
26
+ * - on_stop (turn-loop.ts) — fires when the model
27
+ * returns no tool calls and the loop is
28
+ * about to return reason "completed". A
29
+ * handler returning `continueSession: true`
30
+ * (with `messages`) BLOCKS termination:
31
+ * the messages are injected as a
32
+ * <system-reminder> and the loop runs
33
+ * another turn instead of stopping. This
34
+ * is the seam Goal mode uses (see
35
+ * goal-stop-hook.ts). Bounded by a
36
+ * consecutive-block cap (maxStopBlocks)
37
+ * and maxTurns. ctx.data carries `goal`,
38
+ * `finalText`, `turnCount`. Distinct from
39
+ * HookResult.stop, which controls the hook
40
+ * CHAIN, not agent termination.
26
41
  * - pre_tool_use / post_tool_use (executor.ts) — pre_tool_use honors
27
42
  * `decision: "deny"` to short-circuit
28
43
  * the call (executor.ts:131).
@@ -72,7 +87,7 @@
72
87
  * ContextManager; current implementation only knows
73
88
  * after-the-fact (use post_compact instead).
74
89
  */
75
- export type HookEventName = "on_agent_start" | "on_agent_end" | "on_turn_start" | "on_turn_end" | "on_tool_start" | "on_tool_end" | "on_permission_check" | "on_session_start" | "on_session_end" | "pre_tool_use" | "post_tool_use" | "user_prompt_submit" | "pre_compact" | "post_compact" | "file_changed" | "notification";
90
+ export type HookEventName = "on_agent_start" | "on_agent_end" | "on_turn_start" | "on_turn_end" | "on_stop" | "on_tool_start" | "on_tool_end" | "on_permission_check" | "on_session_start" | "on_session_end" | "pre_tool_use" | "post_tool_use" | "user_prompt_submit" | "pre_compact" | "post_compact" | "file_changed" | "notification";
76
91
  export interface HookContext {
77
92
  eventName: HookEventName;
78
93
  data: Record<string, unknown>;
@@ -82,6 +97,13 @@ export interface HookContext {
82
97
  export interface HookResult {
83
98
  /** If true, stop the hook chain */
84
99
  stop?: boolean;
100
+ /**
101
+ * For on_stop: if true, BLOCK agent termination — the loop injects this
102
+ * result's `messages` and runs another turn instead of returning
103
+ * "completed". Distinct from `stop` (which only short-circuits the hook
104
+ * chain). Ignored for non-on_stop events. Bounded by maxStopBlocks.
105
+ */
106
+ continueSession?: boolean;
85
107
  /** Modified data to pass along */
86
108
  data?: Record<string, unknown>;
87
109
  /** Additional messages to inject */
@@ -0,0 +1,30 @@
1
+ import type { HookHandler } from "./registry.js";
2
+ import type { LLMResponse } from "../types.js";
3
+ import { type GoalConfig } from "../engine/goal.js";
4
+ /** Narrow LLM surface the judge needs — just a one-shot completion. */
5
+ export interface GoalJudgeLLM {
6
+ createMessage(options: {
7
+ systemPrompt: string;
8
+ messages: {
9
+ role: "user" | "assistant" | "system";
10
+ content: string;
11
+ }[];
12
+ stream?: boolean;
13
+ maxTokens?: number;
14
+ recordUsage?: boolean;
15
+ signal?: AbortSignal;
16
+ }): Promise<LLMResponse>;
17
+ }
18
+ interface GoalLogger {
19
+ info: (msg: string, data?: Record<string, unknown>) => void;
20
+ warn: (msg: string, data?: Record<string, unknown>) => void;
21
+ error: (msg: string, data?: Record<string, unknown>) => void;
22
+ }
23
+ export interface GoalStopHookOptions {
24
+ llm: GoalJudgeLLM;
25
+ log: GoalLogger;
26
+ /** Override the goal instead of reading ctx.data.goal (mainly for tests). */
27
+ goal?: string | GoalConfig;
28
+ }
29
+ export declare function createGoalStopHook(opts: GoalStopHookOptions): HookHandler;
30
+ export {};
@@ -0,0 +1,102 @@
1
+ import { normalizeGoal } from "../engine/goal.js";
2
+ const JUDGE_SYSTEM = "你是一个目标完成度裁判。给定一个目标和 agent 最近的输出,判断目标是否已经" +
3
+ "完全达成。只返回一个 JSON 对象,形如 " +
4
+ '{"met": true|false, "gaps": "若未达成,简述还差什么;达成则空串"}。' +
5
+ "不要输出任何额外文字。宁可严格:只有确信目标已完全完成时才返回 met:true。";
6
+ /** Pull the first balanced JSON object out of possibly-prose text. */
7
+ function extractJson(text) {
8
+ const start = text.indexOf("{");
9
+ const end = text.lastIndexOf("}");
10
+ if (start === -1 || end === -1 || end <= start)
11
+ return null;
12
+ const slice = text.slice(start, end + 1);
13
+ try {
14
+ const parsed = JSON.parse(slice);
15
+ if (!parsed || typeof parsed !== "object")
16
+ return null;
17
+ const p = parsed;
18
+ if (typeof p.met !== "boolean")
19
+ return null;
20
+ const gaps = typeof p.gaps === "string" ? p.gaps : "";
21
+ return { met: p.met, gaps };
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function createGoalStopHook(opts) {
28
+ const { llm, log } = opts;
29
+ return async (ctx) => {
30
+ // Accept string or GoalConfig from either the override or ctx.data.goal.
31
+ const g = normalizeGoal(opts.goal ?? ctx.data.goal);
32
+ // No goal → not Goal mode → allow stop.
33
+ if (!g)
34
+ return {};
35
+ const goal = g.objective;
36
+ const finalText = typeof ctx.data.finalText === "string" ? ctx.data.finalText : "";
37
+ let verdict = null;
38
+ try {
39
+ const resp = await llm.createMessage({
40
+ systemPrompt: JUDGE_SYSTEM,
41
+ messages: [
42
+ {
43
+ role: "user",
44
+ content: `目标:\n${goal}\n\n` +
45
+ `agent 最近的输出:\n${finalText || "(无文本输出)"}\n\n` +
46
+ "目标完全达成了吗?按要求只返回 JSON。",
47
+ },
48
+ ],
49
+ stream: false,
50
+ maxTokens: 400,
51
+ // Auxiliary sub-call — keep it out of the session cost/turn stats.
52
+ recordUsage: false,
53
+ });
54
+ verdict = extractJson(resp.text ?? "");
55
+ }
56
+ catch (err) {
57
+ log.warn("goal_stop.judge_failed", {
58
+ cat: "goal",
59
+ error: err.message,
60
+ });
61
+ // P0: do NOT silently allow stop on judge failure — in unattended runs
62
+ // that means the goal silently fails. Nudge to continue instead; the
63
+ // run-scoped budget guardrail (turn-loop) is the real safety backstop
64
+ // that prevents an unsatisfiable goal from looping forever.
65
+ return {
66
+ continueSession: true,
67
+ messages: [
68
+ "继续 —— 目标完成度无法判定,请继续推进直到明确完成(或调用 complete_goal 声明完成)。",
69
+ ],
70
+ };
71
+ }
72
+ if (!verdict) {
73
+ log.warn("goal_stop.unparseable", { cat: "goal" });
74
+ // P0: same as the throw path — unparseable judge output must not be
75
+ // treated as "done". Continue instead of silently allowing the stop.
76
+ return {
77
+ continueSession: true,
78
+ messages: [
79
+ "继续 —— 目标完成度无法判定,请继续推进直到明确完成(或调用 complete_goal 声明完成)。",
80
+ ],
81
+ };
82
+ }
83
+ if (verdict.met) {
84
+ log.info("goal_stop.met", { cat: "goal" });
85
+ // Surface the verdict so the loop can emit a goal_progress(met) event.
86
+ return { data: { goalVerdict: { met: true, gaps: "" } } };
87
+ }
88
+ log.info("goal_stop.not_met", { cat: "goal", gaps: verdict.gaps });
89
+ const gaps = verdict.gaps.trim();
90
+ return {
91
+ continueSession: true,
92
+ messages: [
93
+ gaps
94
+ ? `继续 —— 目标尚未达成。还差:${gaps}`
95
+ : "继续 —— 目标尚未达成,请接着完成它。",
96
+ ],
97
+ // Structured verdict for the UI — the loop emits goal_progress(not_met)
98
+ // with this `gaps` instead of re-running the judge.
99
+ data: { goalVerdict: { met: false, gaps } },
100
+ };
101
+ };
102
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared output-handling primitives for hook runners.
3
+ *
4
+ * Two runners spawn child processes that produce HookResult JSON:
5
+ * - hooks/shell-runner.ts (user-configured shell hooks from settings)
6
+ * - plugins/pluginCommandHook.ts (hooks declared by installed plugins)
7
+ *
8
+ * Both need the same defences against:
9
+ * 1. Runaway output (a chatty / malicious handler eating engine RAM).
10
+ * 2. Schema drift (unknown decision values, typo'd top-level keys,
11
+ * non-string messages — silent no-ops are worse than rejections).
12
+ *
13
+ * Keeping the byte cap and validator in one module guarantees the two
14
+ * runners can't accidentally diverge on what counts as "valid".
15
+ */
16
+ import type { HookResult } from "./events.js";
17
+ /**
18
+ * Cap hook output to avoid runaway logging / OOM. 1 MiB is more than enough
19
+ * for any legitimate HookResult (which is typically < 1 KiB), while still
20
+ * absorbing chatty handlers without truncating their decision/messages
21
+ * payload. Past the cap callers kill the child and treat the hook as failed.
22
+ */
23
+ export declare const MAX_HOOK_OUTPUT_BYTES: number;
24
+ /**
25
+ * Permissive runtime check that a parsed object matches HookResult.
26
+ * Rejects unknown decision values, non-string/array fields, and unknown
27
+ * top-level keys (signalling a typo or hostile/garbage payload). Returns
28
+ * a normalized HookResult on success or null on failure — caller treats
29
+ * null the same as "no result, drop it".
30
+ *
31
+ * Exported so a unit test can pin the contract without spawning a shell.
32
+ */
33
+ export declare function validateHookResult(parsed: unknown): HookResult | null;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared output-handling primitives for hook runners.
3
+ *
4
+ * Two runners spawn child processes that produce HookResult JSON:
5
+ * - hooks/shell-runner.ts (user-configured shell hooks from settings)
6
+ * - plugins/pluginCommandHook.ts (hooks declared by installed plugins)
7
+ *
8
+ * Both need the same defences against:
9
+ * 1. Runaway output (a chatty / malicious handler eating engine RAM).
10
+ * 2. Schema drift (unknown decision values, typo'd top-level keys,
11
+ * non-string messages — silent no-ops are worse than rejections).
12
+ *
13
+ * Keeping the byte cap and validator in one module guarantees the two
14
+ * runners can't accidentally diverge on what counts as "valid".
15
+ */
16
+ /**
17
+ * Cap hook output to avoid runaway logging / OOM. 1 MiB is more than enough
18
+ * for any legitimate HookResult (which is typically < 1 KiB), while still
19
+ * absorbing chatty handlers without truncating their decision/messages
20
+ * payload. Past the cap callers kill the child and treat the hook as failed.
21
+ */
22
+ export const MAX_HOOK_OUTPUT_BYTES = 1024 * 1024;
23
+ /**
24
+ * Permissive runtime check that a parsed object matches HookResult.
25
+ * Rejects unknown decision values, non-string/array fields, and unknown
26
+ * top-level keys (signalling a typo or hostile/garbage payload). Returns
27
+ * a normalized HookResult on success or null on failure — caller treats
28
+ * null the same as "no result, drop it".
29
+ *
30
+ * Exported so a unit test can pin the contract without spawning a shell.
31
+ */
32
+ export function validateHookResult(parsed) {
33
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
34
+ return null;
35
+ const p = parsed;
36
+ const known = new Set([
37
+ "stop",
38
+ "continueSession",
39
+ "data",
40
+ "messages",
41
+ "decision",
42
+ "updatedInput",
43
+ "additionalContext",
44
+ "updatedPrompt",
45
+ ]);
46
+ for (const k of Object.keys(p)) {
47
+ if (!known.has(k))
48
+ return null;
49
+ }
50
+ if ("stop" in p && typeof p.stop !== "boolean")
51
+ return null;
52
+ if ("continueSession" in p && typeof p.continueSession !== "boolean")
53
+ return null;
54
+ if ("data" in p) {
55
+ if (!p.data || typeof p.data !== "object" || Array.isArray(p.data))
56
+ return null;
57
+ }
58
+ if ("messages" in p) {
59
+ if (!Array.isArray(p.messages))
60
+ return null;
61
+ for (const m of p.messages) {
62
+ if (typeof m !== "string")
63
+ return null;
64
+ }
65
+ }
66
+ if ("decision" in p) {
67
+ if (p.decision !== "allow" && p.decision !== "deny" && p.decision !== "ask")
68
+ return null;
69
+ }
70
+ if ("updatedInput" in p) {
71
+ if (!p.updatedInput || typeof p.updatedInput !== "object" || Array.isArray(p.updatedInput))
72
+ return null;
73
+ }
74
+ if ("additionalContext" in p && typeof p.additionalContext !== "string")
75
+ return null;
76
+ if ("updatedPrompt" in p && typeof p.updatedPrompt !== "string")
77
+ return null;
78
+ return p;
79
+ }
@@ -6,6 +6,13 @@ export type HookHandler = (ctx: HookContext) => HookResult | Promise<HookResult>
6
6
  export declare class HookRegistry {
7
7
  private hooks;
8
8
  register(eventName: HookEventName, handler: HookHandler, priority?: number, name?: string): void;
9
+ /**
10
+ * Remove a previously-registered handler by identity. Used for handlers
11
+ * with a run-scoped lifetime (e.g. the per-run GoalStopHook) so they don't
12
+ * leak across subsequent runs on the same long-lived registry. No-op if
13
+ * the handler isn't registered for that event.
14
+ */
15
+ unregister(eventName: HookEventName, handler: HookHandler): void;
9
16
  emit(eventName: HookEventName, data?: Record<string, unknown>): Promise<HookResult>;
10
17
  hasHooks(eventName: HookEventName): boolean;
11
18
  clear(eventName?: HookEventName): void;