@gajae-code/coding-agent 0.16.4 → 0.16.6

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 (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
@@ -33,8 +33,6 @@ import { AgentDirSessionLifecycleClient } from "../../sdk/lifecycle/broker-clien
33
33
  import { sessionListPageFromResponse, traverseSessionList } from "../../sdk/session-list";
34
34
  import { PROVIDER_EXTENDS, PROVIDER_KEY, resolvePaseoHome } from "./setup-deps";
35
35
 
36
- export { resolvePaseoHome } from "./setup-deps";
37
-
38
36
  /** Cheap liveness probe of the daemon socket. Only gates the expensive CLI spawn. */
39
37
  const DAEMON_PROBE_TIMEOUT_MS = 750;
40
38
  /** Bound on the broker `session.list` traversal that proves our own liveness. */
@@ -87,10 +85,17 @@ export interface PaseoAnnounceDependencies {
87
85
  readonly providerKey: string;
88
86
  readonly cwd: string;
89
87
  readonly sessionId: string;
88
+ readonly daemonTarget: PaseoDaemonTarget;
89
+ readonly daemonHost: string;
90
90
  readonly signal?: AbortSignal;
91
91
  }): Promise<PaseoAnnounceOutcome>;
92
92
  }
93
93
 
94
+ interface ResolvedDaemonSelection {
95
+ readonly target: PaseoDaemonTarget;
96
+ readonly host: string;
97
+ }
98
+
94
99
  function asRecord(value: unknown): Record<string, unknown> | undefined {
95
100
  return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
96
101
  }
@@ -161,43 +166,39 @@ export function parseDaemonListen(raw: string): PaseoDaemonTarget | undefined {
161
166
  export async function resolveDaemonTarget(
162
167
  config: unknown,
163
168
  deps: Pick<PaseoAnnounceDependencies, "env" | "paseoHome" | "readJson">,
164
- ): Promise<PaseoDaemonTarget> {
165
- return (await resolveDaemonTargets(config, deps))[0] ?? DEFAULT_DAEMON_TARGET;
169
+ ): Promise<PaseoDaemonTarget | undefined> {
170
+ return (await resolveDaemonSelection(config, deps))?.target;
166
171
  }
167
172
 
168
- async function resolveDaemonTargets(
173
+ async function resolveDaemonSelection(
169
174
  config: unknown,
170
175
  deps: Pick<PaseoAnnounceDependencies, "env" | "paseoHome" | "readJson">,
171
- ): Promise<PaseoDaemonTarget[]> {
176
+ ): Promise<ResolvedDaemonSelection | undefined> {
172
177
  const explicitHost = deps.env.PASEO_HOST?.trim();
173
178
  if (explicitHost) {
174
- const explicitTarget = parseDaemonListen(explicitHost);
175
- return explicitTarget ? [explicitTarget] : [];
179
+ const target = parseDaemonListen(explicitHost);
180
+ return target ? { target, host: explicitHost } : undefined;
181
+ }
182
+ const explicitListen = deps.env.PASEO_LISTEN?.trim();
183
+ if (explicitListen) {
184
+ const target = parseDaemonListen(explicitListen);
185
+ return target ? { target, host: explicitListen } : undefined;
176
186
  }
177
187
 
178
188
  const pidFile = asRecord(await deps.readJson(path.join(deps.paseoHome, "paseo.pid")));
179
189
  const pidListen = typeof pidFile?.listen === "string" ? pidFile.listen : pidFile?.sockPath;
180
190
  const pidTarget = typeof pidListen === "string" ? parseDaemonListen(pidListen) : undefined;
191
+ if (pidTarget?.kind === "ipc") return { target: pidTarget, host: pidListen as string };
181
192
 
182
193
  const parsed = asRecord(config);
183
194
  const configured = asRecord(parsed?.daemon)?.listen ?? parsed?.listen;
184
- const configuredRaw = deps.env.PASEO_LISTEN?.trim() || (typeof configured === "string" ? configured : undefined);
185
- const configuredTarget = configuredRaw ? parseDaemonListen(configuredRaw) : undefined;
186
- const configuredIpc = configuredTarget?.kind === "ipc" ? configuredTarget : undefined;
187
- const pidIpc = pidTarget?.kind === "ipc" ? pidTarget : undefined;
188
- const configuredTcp =
189
- configuredTarget?.kind === "tcp" && !(configuredTarget.host === "127.0.0.1" && configuredTarget.port === 6767)
190
- ? configuredTarget
191
- : undefined;
192
- const directIpc = deps.env.PASEO_LISTEN?.trim();
193
- const directIpcTarget = directIpc ? parseDaemonListen(directIpc) : undefined;
194
- const targets = [
195
- directIpcTarget?.kind === "ipc" ? directIpcTarget : undefined,
196
- pidIpc,
197
- configuredIpc,
198
- configuredTcp,
199
- ].filter((target): target is PaseoDaemonTarget => target !== undefined);
200
- return targets.length > 0 ? [targets[0]!] : [DEFAULT_DAEMON_TARGET];
195
+ if (typeof configured !== "string") return { target: DEFAULT_DAEMON_TARGET, host: "localhost:6767" };
196
+ const configuredTarget = parseDaemonListen(configured);
197
+ if (!configuredTarget) return { target: DEFAULT_DAEMON_TARGET, host: "localhost:6767" };
198
+ if (configured.trim() === "127.0.0.1:6767") {
199
+ return { target: DEFAULT_DAEMON_TARGET, host: "localhost:6767" };
200
+ }
201
+ return { target: configuredTarget, host: configured };
201
202
  }
202
203
 
203
204
  /**
@@ -243,24 +244,26 @@ export async function announceSessionToPaseo(
243
244
  const cli = deps.resolveCli();
244
245
  if (!cli) return { kind: "skipped", reason: "cli-missing" };
245
246
 
246
- const targets = await resolveDaemonTargets(config, deps);
247
- if (deps.env.PASEO_HOST?.trim() && targets.length === 0)
247
+ const selection = await resolveDaemonSelection(config, deps);
248
+ if ((deps.env.PASEO_HOST?.trim() || deps.env.PASEO_LISTEN?.trim()) && selection === undefined)
248
249
  return { kind: "skipped", reason: "unsupported-daemon-target" };
249
- let reachable = false;
250
- for (const target of targets) {
251
- if (await deps.probeDaemon(target)) {
252
- reachable = true;
253
- break;
254
- }
255
- }
256
- if (!reachable) return { kind: "skipped", reason: "daemon-unreachable" };
250
+ if (!selection || !(await deps.probeDaemon(selection.target)))
251
+ return { kind: "skipped", reason: "daemon-unreachable" };
257
252
  if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
258
253
 
259
254
  // Importing a session the broker does not yet report as live would make ACP
260
255
  // `session/load` resume a copy instead of attaching to the running host.
261
256
  if (!(await deps.isSessionLive(input.sessionId, input.cwd))) return { kind: "skipped", reason: "session-not-live" };
262
257
 
263
- return await deps.runImport({ cli, providerKey, cwd: input.cwd, sessionId: input.sessionId, signal });
258
+ return await deps.runImport({
259
+ cli,
260
+ providerKey,
261
+ cwd: input.cwd,
262
+ sessionId: input.sessionId,
263
+ daemonTarget: selection.target,
264
+ daemonHost: selection.host,
265
+ signal,
266
+ });
264
267
  }
265
268
 
266
269
  /** Only a daemon that is not up yet, or a registration that has not landed yet, is worth retrying. */
@@ -369,17 +372,13 @@ function isSessionLive(agentDir: string): (sessionId: string, cwd: string) => Pr
369
372
  response => sessionListPageFromResponse(response),
370
373
  );
371
374
  for (const { sessions } of pages) {
372
- if (sessionListContainsLiveSessionForTests(sessions, sessionId, canonicalCwd)) return true;
375
+ if (sessionListContainsLiveSession(sessions, sessionId, canonicalCwd)) return true;
373
376
  }
374
377
  return false;
375
378
  };
376
379
  }
377
380
 
378
- export function sessionListContainsLiveSessionForTests(
379
- sessions: readonly unknown[],
380
- sessionId: string,
381
- cwd: string,
382
- ): boolean {
381
+ export function sessionListContainsLiveSession(sessions: readonly unknown[], sessionId: string, cwd: string): boolean {
383
382
  for (const value of sessions) {
384
383
  const session = asRecord(value);
385
384
  if (session?.sessionId !== sessionId || session.live !== true) continue;
@@ -403,6 +402,8 @@ function runImport(env: NodeJS.ProcessEnv) {
403
402
  readonly providerKey: string;
404
403
  readonly cwd: string;
405
404
  readonly sessionId: string;
405
+ readonly daemonTarget: PaseoDaemonTarget;
406
+ readonly daemonHost: string;
406
407
  readonly signal?: AbortSignal;
407
408
  }): Promise<PaseoAnnounceOutcome> => {
408
409
  const timeoutController = new AbortController();
@@ -413,7 +414,12 @@ function runImport(env: NodeJS.ProcessEnv) {
413
414
  try {
414
415
  const child = Bun.spawn(
415
416
  [input.cli, "import", "--provider", input.providerKey, "--cwd", input.cwd, input.sessionId],
416
- { stdout: "pipe", stderr: "pipe", signal, env },
417
+ {
418
+ stdout: "ignore",
419
+ stderr: "pipe",
420
+ signal,
421
+ env: { ...env, PASEO_HOST: input.daemonHost },
422
+ },
417
423
  );
418
424
  const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
419
425
  if (input.signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
@@ -87,7 +87,7 @@ export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env, home: str
87
87
  export function createDefaultPaseoPaths(
88
88
  agentDir: string = getAgentDir(),
89
89
  home: string = os.homedir(),
90
- env: NodeJS.ProcessEnv = {},
90
+ env: NodeJS.ProcessEnv = process.env,
91
91
  ): PaseoPaths {
92
92
  const paseoHome = resolvePaseoHome(env, home);
93
93
  return {
@@ -2,3 +2,15 @@
2
2
  export const CANONICAL_GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "autoresearch"] as const;
3
3
 
4
4
  export type CanonicalGjcWorkflowSkill = (typeof CANONICAL_GJC_WORKFLOW_SKILLS)[number];
5
+
6
+ const CANONICAL_GJC_WORKFLOW_SKILL_SET = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
7
+
8
+ /** Case-insensitive, Win32-path-equivalent key used for skill collision checks. */
9
+ export function getSkillFilesystemIdentity(name: string): string {
10
+ return name.replace(/[. ]+$/u, "").toLowerCase();
11
+ }
12
+
13
+ /** Whether a name resolves to a bundled workflow under portable filesystem semantics. */
14
+ export function isCanonicalGjcWorkflowSkillFilesystemAlias(name: string): boolean {
15
+ return CANONICAL_GJC_WORKFLOW_SKILL_SET.has(getSkillFilesystemIdentity(name));
16
+ }
@@ -1,12 +1,14 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, test, vi } from "bun:test";
2
+ import * as nodeFs from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
5
+ import type { LoadContext } from "../capability/types";
4
6
  import { getEmbeddedDefaultGjcSkills } from "../defaults/gjc-defaults";
5
7
  import { buildSkillPromptMessage } from "../extensibility/skills";
6
8
  import { SKILL_FRONTMATTER_SCAN_BYTES, SKILL_FRONTMATTER_SCAN_TOTAL_BYTES, scanSkillDescriptorsFromDir } from "./index";
7
9
 
8
- function makeContext(): any {
9
- return { cwd: process.cwd(), home: process.env.HOME ?? process.cwd(), repoRoot: null };
10
+ function makeContext(root: string): LoadContext {
11
+ return { cwd: root, home: root, repoRoot: null };
10
12
  }
11
13
 
12
14
  describe("skill descriptors", () => {
@@ -22,22 +24,31 @@ describe("skill descriptors", () => {
22
24
  `---\nname: bounded\ndescription: bounded scan\n---\n${body}`,
23
25
  );
24
26
 
25
- const originalFile = Bun.file;
26
- const sliceEnds: number[] = [];
27
- (Bun as any).file = (filePath: string) => {
28
- const file = originalFile(filePath);
29
- return new Proxy(file, {
30
- get(target, property, receiver) {
31
- if (property !== "slice") return Reflect.get(target, property, receiver);
32
- return (start?: number, end?: number) => {
33
- sliceEnds.push(end ?? -1);
34
- return target.slice(start, end);
35
- };
36
- },
37
- });
38
- };
27
+ // Discovery reads the validated descriptor rather than reopening with Bun.file.
28
+ // Observe only the handles this scan opens: a FileHandle prototype spy is
29
+ // process-wide, so unrelated reads from concurrently running tests
30
+ // interleave and make global counts flaky.
31
+ const reads: Array<unknown[]> = [];
32
+ const probe = await fs.open(path.join(skillDir, "SKILL.md"), "r");
33
+ const fileHandlePrototype = Object.getPrototypeOf(probe) as fs.FileHandle;
34
+ await probe.close();
35
+ const originalOpen = nodeFs.promises.open;
36
+ type OpenArgs = Parameters<typeof nodeFs.promises.open>;
37
+ const openSpy = vi.spyOn(nodeFs.promises, "open").mockImplementation((async (...args: OpenArgs) => {
38
+ const handle = await originalOpen(...args);
39
+ const skillPath = args[0];
40
+ if (typeof skillPath === "string" && skillPath.endsWith("SKILL.md")) {
41
+ const originalRead = handle.read.bind(handle);
42
+ handle.read = (async (...readArgs: never[]) => {
43
+ reads.push(readArgs as unknown[]);
44
+ return originalRead(...readArgs);
45
+ }) as typeof handle.read;
46
+ }
47
+ return handle;
48
+ }) as typeof nodeFs.promises.open);
49
+ const readFileSpy = vi.spyOn(fileHandlePrototype, "readFile");
39
50
  try {
40
- const result = await scanSkillDescriptorsFromDir(makeContext(), {
51
+ const result = await scanSkillDescriptorsFromDir(makeContext(root), {
41
52
  dir: root,
42
53
  providerId: "test",
43
54
  level: "project",
@@ -45,9 +56,18 @@ describe("skill descriptors", () => {
45
56
  expect(result.items).toHaveLength(1);
46
57
  expect(Object.hasOwn(result.items[0]?.metadata ?? {}, "content")).toBe(false);
47
58
  expect(JSON.stringify(result.items[0]?.metadata)).not.toContain(bodyMarker);
48
- expect(sliceEnds).toContain(SKILL_FRONTMATTER_SCAN_BYTES);
59
+ const boundedReads = reads.filter(
60
+ call => call[1] === 0 && call[2] === SKILL_FRONTMATTER_SCAN_BYTES && call[3] === 0,
61
+ );
62
+ expect(boundedReads).toHaveLength(1);
63
+ const oversizedReads = reads.filter(
64
+ call => typeof call[2] === "number" && call[2] > SKILL_FRONTMATTER_SCAN_BYTES,
65
+ );
66
+ expect(oversizedReads).toHaveLength(0);
67
+ expect(readFileSpy).not.toHaveBeenCalled();
49
68
  } finally {
50
- (Bun as any).file = originalFile;
69
+ openSpy.mockRestore();
70
+ readFileSpy.mockRestore();
51
71
  }
52
72
  } finally {
53
73
  await fs.rm(root, { recursive: true, force: true });
@@ -63,7 +83,7 @@ describe("skill descriptors", () => {
63
83
  path.join(skillDir, "SKILL.md"),
64
84
  `---\nname: unterminated\ndescription: no closing delimiter\n${"x".repeat(SKILL_FRONTMATTER_SCAN_TOTAL_BYTES * 32)}`,
65
85
  );
66
- const result = await scanSkillDescriptorsFromDir(makeContext(), {
86
+ const result = await scanSkillDescriptorsFromDir(makeContext(root), {
67
87
  dir: root,
68
88
  providerId: "test",
69
89
  level: "project",
@@ -235,6 +235,10 @@ export async function resolvePromptInput(input: string | undefined, description:
235
235
  export interface LoadContextFilesOptions {
236
236
  /** Working directory to start walking up from. Default: getProjectDir() */
237
237
  cwd?: string;
238
+ /** Agent directory backing native user-scope context and system prompt files. */
239
+ agentDir?: string;
240
+ /** Resolver-owned classification for `agentDir`; preserve it across HOME/config refreshes. */
241
+ profileAuthority?: "default" | "custom";
238
242
  }
239
243
 
240
244
  function dedupeExactContextFiles(
@@ -265,7 +269,11 @@ export async function loadProjectContextFilesResult(
265
269
  options: LoadContextFilesOptions = {},
266
270
  ): Promise<ProjectContextFilesResult> {
267
271
  const resolvedCwd = options.cwd ?? getProjectDir();
268
- const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
272
+ const result = await loadCapability(contextFileCapability.id, {
273
+ cwd: resolvedCwd,
274
+ agentDir: options.agentDir,
275
+ profileAuthority: options.profileAuthority,
276
+ });
269
277
  const items = result.items as ContextFile[];
270
278
 
271
279
  // Native user-global context applies everywhere and is least specific, so it
@@ -311,7 +319,11 @@ export async function loadProjectContextFiles(
311
319
  export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
312
320
  const resolvedCwd = options.cwd ?? getProjectDir();
313
321
 
314
- const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
322
+ const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, {
323
+ cwd: resolvedCwd,
324
+ agentDir: options.agentDir,
325
+ profileAuthority: options.profileAuthority,
326
+ });
315
327
 
316
328
  if (result.items.length === 0) return null;
317
329
 
@@ -374,6 +386,10 @@ export interface BuildSystemPromptOptions {
374
386
  skillsSettings?: SkillsSettings;
375
387
  /** Working directory. Default: getProjectDir() */
376
388
  cwd?: string;
389
+ /** Agent directory backing native user-scope prompt discovery. */
390
+ agentDir?: string;
391
+ /** Resolver-owned classification for `agentDir`; preserve it across HOME/config refreshes. */
392
+ profileAuthority?: "default" | "custom";
377
393
  /** Pre-loaded context files (skips discovery if provided). */
378
394
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
379
395
  /** Skills provided directly to system prompt construction. */
@@ -603,10 +619,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
603
619
 
604
620
  const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
605
621
  cwd: resolvedCwd,
622
+ agentDir: options.agentDir,
623
+ profileAuthority: options.profileAuthority,
606
624
  });
607
625
  const contextFilesPromise = providedContextFiles
608
626
  ? Promise.resolve({ contextFiles: providedContextFiles, warnings: [] })
609
- : logger.time("loadProjectContextFiles", loadProjectContextFilesResult, { cwd: resolvedCwd });
627
+ : logger.time("loadProjectContextFiles", loadProjectContextFilesResult, {
628
+ cwd: resolvedCwd,
629
+ agentDir: options.agentDir,
630
+ profileAuthority: options.profileAuthority,
631
+ });
610
632
  const workspaceTreePromise =
611
633
  providedWorkspaceTree !== undefined
612
634
  ? Promise.resolve(providedWorkspaceTree)
@@ -281,6 +281,9 @@ export interface ExecutorOptions {
281
281
  authStorage?: AuthStorage;
282
282
  modelRegistry?: ModelRegistry;
283
283
  settings?: Settings;
284
+ /** Parent session's selected agent directory and immutable resolver-owned authority. */
285
+ agentDir?: string;
286
+ profileAuthority?: "default" | "custom";
284
287
  /** Parent session's registry; shared by child sessions for IRC routing and roster visibility. */
285
288
  agentRegistry?: AgentRegistry;
286
289
  /**
@@ -1952,6 +1955,8 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1952
1955
  const { session } = await awaitAbortable(
1953
1956
  canonicalSdk.createAgentSession({
1954
1957
  cwd: worktree ?? cwd,
1958
+ agentDir: options.agentDir,
1959
+ profileAuthority: options.profileAuthority,
1955
1960
  authStorage,
1956
1961
  modelRegistry,
1957
1962
  settings: subagentSettings,
package/src/task/index.ts CHANGED
@@ -2337,6 +2337,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2337
2337
  modelRegistry: this.session.modelRegistry,
2338
2338
  agentRegistry: this.session.agentRegistry,
2339
2339
  settings: this.session.settings,
2340
+ agentDir: this.session.getSessionAgentDir?.() ?? this.session.settings.getAgentDir(),
2341
+ profileAuthority: this.session.profileAuthority,
2340
2342
  inheritedServiceTier: this.session.serviceTier,
2341
2343
  contextFiles,
2342
2344
  skills: availableSkills,
@@ -2421,6 +2423,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2421
2423
  modelRegistry: this.session.modelRegistry,
2422
2424
  agentRegistry: this.session.agentRegistry,
2423
2425
  settings: this.session.settings,
2426
+ agentDir: this.session.getSessionAgentDir?.() ?? this.session.settings.getAgentDir(),
2427
+ profileAuthority: this.session.profileAuthority,
2424
2428
  inheritedServiceTier: this.session.serviceTier,
2425
2429
  contextFiles,
2426
2430
  skills: availableSkills,
package/src/tools/bash.ts CHANGED
@@ -103,6 +103,7 @@ const BASH_ERROR_MAX_BYTES = 4096;
103
103
  const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
104
104
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
105
105
  const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
106
+ const MASTER_OWNER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID";
106
107
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
107
108
  const ACP_RELEASE_TIMEOUT_MS = 1_000;
108
109
  const READ_ONLY_BASH_ENV: Record<string, string> = {
@@ -186,8 +187,9 @@ function artifactReferenceIsReachable(text: string, result: BashResult | BashInt
186
187
  );
187
188
  }
188
189
 
189
- function suffixPrefixOverlap(source: string, target: string): number {
190
+ export function suffixPrefixOverlap(source: string, target: string): number {
190
191
  if (source.length === 0 || target.length === 0) return 0;
192
+ if (target.startsWith(source)) return source.length;
191
193
  const prefix = new Uint32Array(target.length);
192
194
  for (let index = 1; index < target.length; index += 1) {
193
195
  let length = prefix[index - 1] ?? 0;
@@ -244,10 +246,17 @@ function failureStatusCause(
244
246
  if (result.cancelled) return leadingStatus ?? lastStatus ?? "Command cancelled";
245
247
  if (isInteractiveResult(result) && result.timedOut) return lastStatus ?? "Command timed out";
246
248
  if (result.exitCode === undefined) return "Command failed: missing exit status";
247
- if (result.exitCode !== 0) return `Command exited with code ${result.exitCode}`;
249
+ if (result.exitCode !== 0) return nonZeroExitCause(result);
248
250
  return undefined;
249
251
  }
250
252
 
253
+ function nonZeroExitCause(result: BashResult | BashInteractiveResult): string {
254
+ if ("signal" in result && result.signal) {
255
+ return `Command terminated by ${result.signal} (exit code ${result.exitCode})`;
256
+ }
257
+ return `Command exited with code ${result.exitCode}`;
258
+ }
259
+
251
260
  function removeTrailingFailureCause(text: string, cause: string | undefined): string {
252
261
  if (!cause) return text;
253
262
  for (const suffix of [`\n\n${cause}`, `\n${cause}`, cause]) {
@@ -717,7 +726,12 @@ export function masterCommandEnvOverrides(
717
726
  directMasterSpawn: boolean,
718
727
  ): Record<string, string> {
719
728
  if (directMasterSpawn) return {};
720
- return Object.fromEntries(Object.entries(env ?? {}).filter(([key]) => key !== MASTER_CAPABILITY_ENV));
729
+ // The master capability is single-use spawn authority and the owner id is
730
+ // spawn-lineage identity: neither is ambient state for ordinary Bash or a
731
+ // chained/pipelined descendant (issue #5374).
732
+ return Object.fromEntries(
733
+ Object.entries(env ?? {}).filter(([key]) => key !== MASTER_CAPABILITY_ENV && key !== MASTER_OWNER_SESSION_ENV),
734
+ );
721
735
  }
722
736
 
723
737
  function escapeBashEnvValueForDisplay(value: string): string {
@@ -896,9 +910,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
896
910
  throw new ToolError(formatBashFailureMessage(result, `${outputText}\n\nCommand failed: missing exit status`));
897
911
  }
898
912
  if (result.exitCode !== 0) {
899
- throw new ToolError(
900
- formatBashFailureMessage(result, `${outputText}\n\nCommand exited with code ${result.exitCode}`),
901
- );
913
+ const cause = nonZeroExitCause(result);
914
+ throw new ToolError(formatBashFailureMessage(result, `${outputText}\n\n${cause}`));
902
915
  }
903
916
  return outputText;
904
917
  }
@@ -1438,7 +1451,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1438
1451
  // command that contained shell syntax into a privileged one.
1439
1452
  const directMasterSpawn = isStrictDirectSdkSpawnCommand(rawCommand) && isStrictDirectSdkSpawnCommand(command);
1440
1453
  const masterCapability = directMasterSpawn ? this.session.getMasterBashCapability?.() : undefined;
1441
- const masterOwnerSessionId = directMasterSpawn ? this.session.getMasterOwnerSessionId?.() : undefined;
1454
+ // Master ownership is lineage identity, not session identity: it travels
1455
+ // under its own variable on every bash env so a master-owned child can
1456
+ // still read its OWN id from GJC_SESSION_ID (issue #5374).
1457
+ const masterOwnerSessionId = this.session.getMasterOwnerSessionId?.();
1442
1458
  const commandEnvOverrides = masterCommandEnvOverrides(expandedEnv, directMasterSpawn);
1443
1459
  const resolvedEnv = {
1444
1460
  ...buildGjcRuntimeSessionEnv({
@@ -1451,7 +1467,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1451
1467
  : {}),
1452
1468
  ...commandEnvOverrides,
1453
1469
  ...(masterCapability ? { [MASTER_CAPABILITY_ENV]: masterCapability } : {}),
1454
- ...(masterOwnerSessionId ? { GJC_MASTER_OWNER_SESSION_ID: masterOwnerSessionId } : {}),
1470
+ ...(masterOwnerSessionId ? { [MASTER_OWNER_SESSION_ENV]: masterOwnerSessionId } : {}),
1455
1471
  ...(this.session.bashRestrictionProfile === "read-only" ? READ_ONLY_BASH_ENV : {}),
1456
1472
  ...(allowedPrefixes && allowedPrefixes.length > 0 ? { [GJC_RESTRICTED_ROLE_AGENT_BASH_ENV]: "1" } : {}),
1457
1473
  };
@@ -1503,8 +1519,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1503
1519
  resolvedEnv: Record<string, string> | undefined,
1504
1520
  ): Promise<BashResult> {
1505
1521
  const masterCapability = resolvedEnv?.[MASTER_CAPABILITY_ENV];
1522
+ const ownerSessionId = resolvedEnv?.[MASTER_OWNER_SESSION_ENV];
1506
1523
  const sessionAgentDir = resolvedEnv?.GJC_CODING_AGENT_DIR;
1507
- if (masterCapability === undefined || sessionAgentDir === undefined)
1524
+ if (masterCapability === undefined || ownerSessionId === undefined || sessionAgentDir === undefined)
1508
1525
  throw new ToolError("Master spawn authority context is unavailable.");
1509
1526
  const args = parseDirectSdkSpawnArgs(command);
1510
1527
  args.agentDir = sessionAgentDir;
@@ -1512,8 +1529,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1512
1529
  const request = runSdkSpawn(args, {
1513
1530
  env: {
1514
1531
  ...process.env,
1515
- GJC_MASTER_CAPABILITY: masterCapability,
1516
- GJC_SESSION_ID: resolvedEnv?.GJC_MASTER_OWNER_SESSION_ID ?? this.session.getSessionId?.() ?? undefined,
1532
+ [MASTER_CAPABILITY_ENV]: masterCapability,
1533
+ [MASTER_OWNER_SESSION_ENV]: ownerSessionId,
1517
1534
  GJC_CODING_AGENT_DIR: sessionAgentDir,
1518
1535
  },
1519
1536
  });
package/src/tools/gh.ts CHANGED
@@ -2738,7 +2738,14 @@ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<
2738
2738
  }
2739
2739
  }
2740
2740
 
2741
- const doFetch = () => fetchIssueViewFresh(options.cwd, repo, identifier, includeComments, options.signal);
2741
+ const doFetch = (signal?: AbortSignal) =>
2742
+ fetchIssueViewFresh(
2743
+ options.cwd,
2744
+ repo,
2745
+ identifier,
2746
+ includeComments,
2747
+ signal && options.signal ? AbortSignal.any([signal, options.signal]) : (signal ?? options.signal),
2748
+ );
2742
2749
 
2743
2750
  if (!repo || cacheNumber === undefined) {
2744
2751
  const fresh = await doFetch();
@@ -2752,6 +2759,7 @@ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<
2752
2759
  includeComments,
2753
2760
  settings: options.settings,
2754
2761
  authKey,
2762
+ signal: options.signal,
2755
2763
  fetchFresh: doFetch,
2756
2764
  });
2757
2765
  return {
@@ -2771,7 +2779,14 @@ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<
2771
2779
  export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLookupResult<GhPrViewData>> {
2772
2780
  const includeComments = options.includeComments ?? true;
2773
2781
  const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
2774
- const doFetch = () => fetchPrViewFresh(options.cwd, options.repo, options.number, includeComments, options.signal);
2782
+ const doFetch = (signal?: AbortSignal) =>
2783
+ fetchPrViewFresh(
2784
+ options.cwd,
2785
+ options.repo,
2786
+ options.number,
2787
+ includeComments,
2788
+ signal && options.signal ? AbortSignal.any([signal, options.signal]) : (signal ?? options.signal),
2789
+ );
2775
2790
  const lookup = await getOrFetchView<GhPrViewData>({
2776
2791
  repo: options.repo,
2777
2792
  kind: "pr",
@@ -2779,6 +2794,7 @@ export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLo
2779
2794
  includeComments,
2780
2795
  settings: options.settings,
2781
2796
  authKey,
2797
+ signal: options.signal,
2782
2798
  fetchFresh: doFetch,
2783
2799
  });
2784
2800
  return {
@@ -3121,7 +3137,13 @@ async function fetchPrDiffFresh(
3121
3137
  */
3122
3138
  export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<ViewLookupResult<PrDiffPayload>> {
3123
3139
  const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
3124
- const doFetch = () => fetchPrDiffFresh(options.cwd, options.repo, options.number, options.signal);
3140
+ const doFetch = (signal?: AbortSignal) =>
3141
+ fetchPrDiffFresh(
3142
+ options.cwd,
3143
+ options.repo,
3144
+ options.number,
3145
+ signal && options.signal ? AbortSignal.any([signal, options.signal]) : (signal ?? options.signal),
3146
+ );
3125
3147
  const lookup = await getOrFetchView<PrDiffPayload>({
3126
3148
  repo: options.repo,
3127
3149
  kind: "pr-diff",
@@ -3129,6 +3151,7 @@ export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<Vi
3129
3151
  includeComments: false,
3130
3152
  settings: options.settings,
3131
3153
  authKey,
3154
+ signal: options.signal,
3132
3155
  fetchFresh: doFetch,
3133
3156
  });
3134
3157
  return {