@gajae-code/coding-agent 0.11.2 → 0.11.4

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 (101) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/cli/launch-disposition.d.ts +23 -0
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +85 -10
  7. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  8. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  9. package/dist/types/main.d.ts +1 -0
  10. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  11. package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
  12. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  13. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  14. package/dist/types/sdk/broker/broker.d.ts +1 -0
  15. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  16. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  17. package/dist/types/sdk/broker/internal.d.ts +3 -0
  18. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  19. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  20. package/dist/types/sdk/bus/index.d.ts +2 -0
  21. package/dist/types/sdk/session.d.ts +2 -0
  22. package/dist/types/tools/ask.d.ts +39 -12
  23. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  24. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  25. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  26. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  27. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  28. package/dist/types/tools/path-utils.d.ts +9 -14
  29. package/dist/types/tools/read.d.ts +2 -0
  30. package/dist/types/tools/render-utils.d.ts +1 -8
  31. package/dist/types/tools/resource-gc.d.ts +14 -0
  32. package/dist/types/tools/subagent.d.ts +1 -0
  33. package/dist/types/utils/file-mentions.d.ts +3 -3
  34. package/dist/types/utils/image-loading.d.ts +23 -0
  35. package/dist/types/utils/image-resize.d.ts +2 -0
  36. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  37. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  38. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  39. package/dist/types/web/scrapers/types.d.ts +1 -5
  40. package/package.json +24 -8
  41. package/scripts/run-threat-model-check.ts +60 -0
  42. package/src/cli/launch-disposition.ts +42 -0
  43. package/src/cli/worktree-cli.ts +9 -287
  44. package/src/cli.ts +10 -9
  45. package/src/commands/gc.ts +11 -1
  46. package/src/commands/sdk.ts +3 -2
  47. package/src/commands/worktree.ts +10 -53
  48. package/src/config/model-profiles.ts +15 -15
  49. package/src/config/settings-schema.ts +55 -10
  50. package/src/eval/js/shared/runtime.ts +1 -1
  51. package/src/gjc-runtime/gc-render.ts +19 -1
  52. package/src/gjc-runtime/gc-runtime.ts +68 -3
  53. package/src/gjc-runtime/state-argv.ts +292 -0
  54. package/src/gjc-runtime/state-runtime.ts +51 -212
  55. package/src/main.ts +99 -22
  56. package/src/modes/components/custom-editor.ts +103 -32
  57. package/src/modes/components/custom-provider-wizard.ts +26 -1
  58. package/src/modes/components/session-observer-overlay.ts +10 -23
  59. package/src/modes/components/tool-transcript-format.ts +72 -0
  60. package/src/modes/components/transcript-viewer-overlay.ts +42 -2
  61. package/src/modes/controllers/input-controller.ts +98 -37
  62. package/src/modes/controllers/selector-controller.ts +1 -1
  63. package/src/modes/interactive-mode.ts +13 -2
  64. package/src/modes/prompt-action-autocomplete.ts +7 -0
  65. package/src/sdk/broker/broker.ts +139 -32
  66. package/src/sdk/broker/discovery.ts +125 -0
  67. package/src/sdk/broker/ensure.ts +103 -19
  68. package/src/sdk/broker/internal.ts +10 -0
  69. package/src/sdk/broker/session-index.ts +228 -15
  70. package/src/sdk/broker/state-version.ts +5 -2
  71. package/src/sdk/bus/index.ts +4 -1
  72. package/src/sdk/bus/telegram-daemon.ts +50 -10
  73. package/src/sdk/host/host.ts +13 -1
  74. package/src/sdk/session.ts +4 -1
  75. package/src/session/internal/managed-session-scope.ts +15 -1
  76. package/src/tools/ask.ts +216 -41
  77. package/src/tools/bash-allowed-prefixes.ts +79 -29
  78. package/src/tools/browser/launch.ts +3 -1
  79. package/src/tools/browser/readable.ts +1 -1
  80. package/src/tools/browser/screenshot-format.ts +34 -0
  81. package/src/tools/browser/tab-protocol.ts +3 -0
  82. package/src/tools/browser/tab-supervisor.ts +125 -122
  83. package/src/tools/browser/tab-worker-entry.ts +33 -14
  84. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  85. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  86. package/src/tools/browser/tab-worker.ts +7 -5
  87. package/src/tools/output-meta.ts +6 -2
  88. package/src/tools/path-utils.ts +42 -35
  89. package/src/tools/read.ts +189 -64
  90. package/src/tools/render-utils.ts +1 -27
  91. package/src/tools/resource-gc.ts +123 -34
  92. package/src/tools/subagent.ts +43 -2
  93. package/src/utils/file-mentions.ts +4 -4
  94. package/src/utils/image-loading.ts +85 -34
  95. package/src/utils/image-resize.ts +16 -9
  96. package/src/utils/pasted-image-loading.ts +514 -0
  97. package/src/utils/pasted-image-path.ts +223 -98
  98. package/src/web/scrapers/html-to-markdown.ts +68 -0
  99. package/src/web/scrapers/readthedocs.ts +1 -1
  100. package/src/web/scrapers/types.ts +1 -68
  101. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -42,6 +42,7 @@ import {
42
42
  SessionResolutionError,
43
43
  writeSessionActivityMarker,
44
44
  } from "./session-resolution";
45
+ import { classifyStateArgv, firstStateFlagValue, type StateAction, type StateArgvClassification } from "./state-argv";
45
46
  import { renderStateGraph, type StateGraphFormat } from "./state-graph";
46
47
  import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
47
48
  import {
@@ -72,7 +73,7 @@ import {
72
73
  writeGuardedWorkflowEnvelopeAtomic,
73
74
  } from "./state-writer";
74
75
  import { assertSafePathComponent, CommandError, flagValue, hasFlag, isPlainObject } from "./workflow-cli-common";
75
- import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
76
+ import { getSkillManifest, isKnownWorkflowState, isValidTransition } from "./workflow-manifest";
76
77
 
77
78
  /**
78
79
  * Native implementation of the `gjc state read|write|clear` command surface.
@@ -99,136 +100,9 @@ class StateCommandError extends CommandError {
99
100
  }
100
101
 
101
102
  const GRAPH_FORMATS = new Set(["ascii", "mermaid", "dot"]);
102
- const FLAGS_WITH_VALUES = new Set([
103
- "--input",
104
- "--mode",
105
- "--session-id",
106
- "--thread-id",
107
- "--turn-id",
108
- "--to",
109
- "--skill",
110
- "--format",
111
- "--older-than",
112
- "--status",
113
- "--fields",
114
- "--since",
115
- "--limit",
116
- ]);
117
- const ACTION_NAMES = new Set([
118
- "read",
119
- "write",
120
- "clear",
121
- "contract",
122
- "handoff",
123
- "graph",
124
- "prune",
125
- "gc",
126
- "migrate",
127
- "status",
128
- "doctor",
129
- ]);
130
- const BOOLEAN_FLAGS = new Set([
131
- "--json",
132
- "--replace",
133
- "--hard",
134
- "--dry-run",
135
- "--migrate",
136
- "--compact",
137
- "--history",
138
- "--force",
139
- ]);
140
- const VERB_SPECIFIC_FLAGS = new Set([
141
- "--skill",
142
- "--format",
143
- "--older-than",
144
- "--status",
145
- "--fields",
146
- "--since",
147
- "--limit",
148
- "--history",
149
- ]);
150
-
151
- function flagName(arg: string): string | undefined {
152
- if (!arg.startsWith("--")) return undefined;
153
- const equalsIndex = arg.indexOf("=");
154
- return equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
155
- }
156
-
157
- function manifestFlagNames(action: ParsedInvocation["action"], positionalSkill: string | undefined): Set<string> {
158
- const names = new Set<string>();
159
- const skills =
160
- positionalSkill && KNOWN_MODES.includes(positionalSkill)
161
- ? [positionalSkill as CanonicalGjcWorkflowSkill]
162
- : CANONICAL_GJC_WORKFLOW_SKILLS;
163
- for (const skill of skills) {
164
- for (const arg of typedArgsFor(skill, action)) names.add(`--${arg.name}`);
165
- }
166
- return names;
167
- }
168
-
169
- function assertKnownFlags(args: readonly string[], parsed: ParsedInvocation): void {
170
- const manifestFlags = manifestFlagNames(parsed.action, parsed.positionalSkill);
171
- for (const arg of args) {
172
- const flag = flagName(arg);
173
- if (!flag) continue;
174
- if (
175
- FLAGS_WITH_VALUES.has(flag) ||
176
- BOOLEAN_FLAGS.has(flag) ||
177
- VERB_SPECIFIC_FLAGS.has(flag) ||
178
- manifestFlags.has(flag)
179
- ) {
180
- continue;
181
- }
182
- throw new StateCommandError(2, `unknown gjc state flag: ${flag}`);
183
- }
184
- }
185
-
186
- interface ParsedInvocation {
187
- action:
188
- | "read"
189
- | "write"
190
- | "clear"
191
- | "contract"
192
- | "handoff"
193
- | "graph"
194
- | "prune"
195
- | "gc"
196
- | "migrate"
197
- | "status"
198
- | "doctor";
199
- positionalSkill?: string;
200
- }
201
-
202
- function parsePositionalArgs(args: readonly string[]): ParsedInvocation {
203
- let skipNext = false;
204
- const positional: string[] = [];
205
- for (const arg of args) {
206
- if (skipNext) {
207
- skipNext = false;
208
- continue;
209
- }
210
- if (FLAGS_WITH_VALUES.has(arg)) {
211
- skipNext = true;
212
- continue;
213
- }
214
- if (!arg.startsWith("-")) positional.push(arg);
215
- }
216
- // Documented argv shapes:
217
- // gjc state read|write|clear|contract ...
218
- // gjc state <skill> read|write|contract ...
219
- const first = positional[0];
220
- const second = positional[1];
221
- if (first && ACTION_NAMES.has(first)) {
222
- return { action: first as ParsedInvocation["action"], positionalSkill: second };
223
- }
224
- if (first && second && ACTION_NAMES.has(second)) {
225
- return { action: second as ParsedInvocation["action"], positionalSkill: first };
226
- }
227
- // `gjc state <skill>` alone defaults to read for that skill.
228
- if (first && !second) {
229
- return { action: "read", positionalSkill: first };
230
- }
231
- return { action: "read" };
103
+ function assertKnownFlags(classification: StateArgvClassification): void {
104
+ const [unknownFlag] = classification.unknownFlags;
105
+ if (unknownFlag) throw new StateCommandError(2, `unknown gjc state flag: ${unknownFlag}`);
232
106
  }
233
107
 
234
108
  function isKnownMode(mode: string): mode is CanonicalGjcWorkflowSkill {
@@ -279,19 +153,16 @@ interface ResolvedSelectors {
279
153
  // `clear` resolves like a read (explicit -> payload -> env -> latest-activity marker)
280
154
  // per the spec: read/status/clear may fall back to the most-recent session. Commands
281
155
  // that create or mutate new state roots still require an explicit/env session id.
282
- const WRITE_SESSION_ACTIONS = new Set<ParsedInvocation["action"]>(["write", "handoff", "prune", "migrate"]);
156
+ const WRITE_SESSION_ACTIONS = new Set<StateAction>(["write", "handoff", "prune", "migrate"]);
283
157
 
284
- async function resolveSelectors(
285
- args: readonly string[],
286
- cwd: string,
287
- positionalSkill: string | undefined,
288
- action: ParsedInvocation["action"],
289
- ): Promise<ResolvedSelectors> {
290
- const payload = await readInputJson(flagValue(args, "--input"), cwd);
158
+ async function resolveSelectors(args: readonly string[], cwd: string, action: StateAction): Promise<ResolvedSelectors> {
159
+ const classification = classifyStateArgv(args);
160
+ const payload = await readInputJson(firstStateFlagValue(classification, "--input"), cwd);
291
161
 
162
+ const [modeCandidate, positionalCandidate] = classification.runtimeSelectorCandidates;
292
163
  const candidates: Array<string | undefined> = [
293
- flagValue(args, "--mode")?.trim() || undefined,
294
- positionalSkill?.trim() || undefined,
164
+ modeCandidate?.value,
165
+ positionalCandidate?.value,
295
166
  typeof payload?.mode === "string" ? (payload.mode as string).trim() || undefined : undefined,
296
167
  typeof payload?.skill === "string" ? (payload.skill as string).trim() || undefined : undefined,
297
168
  ];
@@ -1232,12 +1103,8 @@ export async function readWorkflowStateJson(
1232
1103
  return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
1233
1104
  }
1234
1105
 
1235
- async function handleRead(
1236
- args: readonly string[],
1237
- cwd: string,
1238
- positionalSkill: string | undefined,
1239
- ): Promise<StateCommandResult> {
1240
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1106
+ async function handleRead(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1107
+ const selectors = await resolveSelectors(args, cwd, "read");
1241
1108
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1242
1109
  const fields = parseFieldsFlag(args);
1243
1110
  if (mode) {
@@ -1276,12 +1143,8 @@ async function handleRead(
1276
1143
  return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
1277
1144
  }
1278
1145
 
1279
- async function handleStatus(
1280
- args: readonly string[],
1281
- cwd: string,
1282
- positionalSkill: string | undefined,
1283
- ): Promise<StateCommandResult> {
1284
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1146
+ async function handleStatus(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1147
+ const selectors = await resolveSelectors(args, cwd, "read");
1285
1148
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1286
1149
  if (!mode) {
1287
1150
  throw new StateCommandError(
@@ -1303,12 +1166,8 @@ async function handleStatus(
1303
1166
  };
1304
1167
  }
1305
1168
 
1306
- async function handleWrite(
1307
- args: readonly string[],
1308
- cwd: string,
1309
- positionalSkill: string | undefined,
1310
- ): Promise<StateCommandResult> {
1311
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
1169
+ async function handleWrite(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1170
+ const selectors = await resolveSelectors(args, cwd, "write");
1312
1171
  const { gjcSessionId: sessionId, threadId, turnId, payload } = selectors;
1313
1172
  if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
1314
1173
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
@@ -1465,12 +1324,8 @@ async function handleWrite(
1465
1324
  );
1466
1325
  }
1467
1326
 
1468
- async function handleClear(
1469
- args: readonly string[],
1470
- cwd: string,
1471
- positionalSkill: string | undefined,
1472
- ): Promise<StateCommandResult> {
1473
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
1327
+ async function handleClear(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1328
+ const selectors = await resolveSelectors(args, cwd, "clear");
1474
1329
  const { gjcSessionId: sessionId, threadId, turnId } = selectors;
1475
1330
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1476
1331
  if (!mode)
@@ -1631,12 +1486,8 @@ async function assertDeepInterviewHandoffReady(state: Record<string, unknown>):
1631
1486
  * the phase remains in `skill-active-state.json` until a chain call (or
1632
1487
  * explicit `clear`) demotes it.
1633
1488
  */
1634
- async function handleHandoffUnlocked(
1635
- args: readonly string[],
1636
- cwd: string,
1637
- positionalSkill: string | undefined,
1638
- ): Promise<StateCommandResult> {
1639
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
1489
+ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1490
+ const selectors = await resolveSelectors(args, cwd, "handoff");
1640
1491
  const { gjcSessionId: sessionId, threadId, turnId } = selectors;
1641
1492
  const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1642
1493
  if (!caller) {
@@ -1944,23 +1795,22 @@ async function handleHandoffUnlocked(
1944
1795
  };
1945
1796
  }
1946
1797
 
1947
- async function handleHandoff(
1948
- args: readonly string[],
1949
- cwd: string,
1950
- positionalSkill: string | undefined,
1951
- ): Promise<StateCommandResult> {
1952
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
1953
- return withWorkflowStateLock(path.relative(cwd, activeStateFile(cwd, selectors.gjcSessionId)), async () =>
1954
- handleHandoffUnlocked(args, cwd, positionalSkill),
1955
- );
1798
+ async function handleHandoff(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1799
+ const selectors = await resolveSelectors(args, cwd, "handoff");
1800
+ // Serialize concurrent handoffs on a dedicated sentinel lock, NOT on the
1801
+ // derived `skill-active-state.json` cache. The inner transaction
1802
+ // (applyHandoffToActiveState / syncSkillActiveState -> rebuildActiveSnapshot)
1803
+ // re-locks that cache file, and `withFileLock` is not reentrant: holding the
1804
+ // active-state lock here made the inner rebuild self-contend and fail after
1805
+ // all retries whenever `cwd === process.cwd()` (the real CLI case). Pass
1806
+ // `{ cwd }` so the sentinel resolves against the handoff cwd rather than
1807
+ // `process.cwd()`.
1808
+ const handoffLock = path.join(sessionStateDir(cwd, selectors.gjcSessionId), "handoff");
1809
+ return withWorkflowStateLock(handoffLock, async () => handleHandoffUnlocked(args, cwd), { cwd });
1956
1810
  }
1957
1811
 
1958
- async function handleContract(
1959
- args: readonly string[],
1960
- cwd: string,
1961
- positionalSkill: string | undefined,
1962
- ): Promise<StateCommandResult> {
1963
- const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
1812
+ async function handleContract(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1813
+ const { mode } = await resolveSelectors(args, cwd, "read");
1964
1814
  if (!mode) {
1965
1815
  throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
1966
1816
  }
@@ -2188,12 +2038,8 @@ async function handleGraph(
2188
2038
  };
2189
2039
  }
2190
2040
 
2191
- async function handlePrune(
2192
- args: readonly string[],
2193
- cwd: string,
2194
- positionalSkill: string | undefined,
2195
- ): Promise<StateCommandResult> {
2196
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
2041
+ async function handlePrune(args: readonly string[], cwd: string): Promise<StateCommandResult> {
2042
+ const selectors = await resolveSelectors(args, cwd, "prune");
2197
2043
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
2198
2044
  if (!mode) {
2199
2045
  throw new StateCommandError(
@@ -2258,12 +2104,8 @@ async function handleGc(
2258
2104
  return { status: 0, stdout: `${JSON.stringify(summary, null, 2)}\n` };
2259
2105
  }
2260
2106
 
2261
- async function handleMigrate(
2262
- args: readonly string[],
2263
- cwd: string,
2264
- positionalSkill: string | undefined,
2265
- ): Promise<StateCommandResult> {
2266
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
2107
+ async function handleMigrate(args: readonly string[], cwd: string): Promise<StateCommandResult> {
2108
+ const selectors = await resolveSelectors(args, cwd, "migrate");
2267
2109
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
2268
2110
  if (!mode) {
2269
2111
  throw new StateCommandError(
@@ -2294,34 +2136,31 @@ async function handleMigrate(
2294
2136
 
2295
2137
  export async function runNativeStateCommand(args: string[], cwd = process.cwd()): Promise<StateCommandResult> {
2296
2138
  try {
2297
- const parsed = parsePositionalArgs(args);
2298
- assertKnownFlags(args, parsed);
2299
- switch (parsed.action) {
2139
+ const parsed = classifyStateArgv(args);
2140
+ assertKnownFlags(parsed);
2141
+ switch (parsed.effectiveAction) {
2300
2142
  case "read":
2301
- if (hasFlag(args, "--migrate")) return await handleMigrate(args, cwd, parsed.positionalSkill);
2302
- return await handleRead(args, cwd, parsed.positionalSkill);
2143
+ return await handleRead(args, cwd);
2303
2144
  case "write":
2304
- return await handleWrite(args, cwd, parsed.positionalSkill);
2145
+ return await handleWrite(args, cwd);
2305
2146
  case "clear":
2306
- return await handleClear(args, cwd, parsed.positionalSkill);
2147
+ return await handleClear(args, cwd);
2307
2148
  case "contract":
2308
- return await handleContract(args, cwd, parsed.positionalSkill);
2149
+ return await handleContract(args, cwd);
2309
2150
  case "status":
2310
- return await handleStatus(args, cwd, parsed.positionalSkill);
2151
+ return await handleStatus(args, cwd);
2311
2152
  case "doctor":
2312
2153
  return await handleDoctor(args, cwd, parsed.positionalSkill);
2313
2154
  case "handoff":
2314
- return await handleHandoff(args, cwd, parsed.positionalSkill);
2155
+ return await handleHandoff(args, cwd);
2315
2156
  case "graph":
2316
2157
  return await handleGraph(args, cwd, parsed.positionalSkill);
2317
2158
  case "prune":
2318
- return await handlePrune(args, cwd, parsed.positionalSkill);
2159
+ return await handlePrune(args, cwd);
2319
2160
  case "gc":
2320
2161
  return await handleGc(args, cwd, parsed.positionalSkill);
2321
2162
  case "migrate":
2322
- return await handleMigrate(args, cwd, parsed.positionalSkill);
2323
- default:
2324
- return { status: 2, stderr: `Unknown gjc state command: ${parsed.action}\n` };
2163
+ return await handleMigrate(args, cwd);
2325
2164
  }
2326
2165
  } catch (error) {
2327
2166
  if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
package/src/main.ts CHANGED
@@ -23,6 +23,7 @@ import chalk from "chalk";
23
23
  import type { Args } from "./cli/args";
24
24
  import { processFileArguments } from "./cli/file-processor";
25
25
  import { buildInitialMessage } from "./cli/initial-message";
26
+ import { resolveLaunchDisposition } from "./cli/launch-disposition";
26
27
  import { runListModelsCommand } from "./cli/list-models";
27
28
  import { selectSession } from "./cli/session-picker";
28
29
  import { findConfigFile } from "./config";
@@ -282,7 +283,7 @@ export function resolveAcpStartupOptions(
282
283
  }
283
284
 
284
285
  async function readPipedInput(): Promise<string | undefined> {
285
- if (process.stdin.isTTY !== false) return undefined;
286
+ if (process.stdin.isTTY === true) return undefined;
286
287
  try {
287
288
  const text = await Bun.stdin.text();
288
289
  if (text.trim().length === 0) return undefined;
@@ -292,6 +293,30 @@ async function readPipedInput(): Promise<string | undefined> {
292
293
  }
293
294
  }
294
295
 
296
+ async function readPipedInputIfReady(): Promise<string | undefined> {
297
+ if (process.stdin.isTTY === true) return undefined;
298
+
299
+ const ready = Promise.withResolvers<void>();
300
+ const onReadable = () => ready.resolve();
301
+ const onEnd = () => ready.resolve();
302
+ const onError = () => ready.resolve();
303
+ process.stdin.once("readable", onReadable);
304
+ process.stdin.once("end", onEnd);
305
+ process.stdin.once("error", onError);
306
+ await Promise.race([ready.promise, new Promise<void>(resolve => setImmediate(resolve))]);
307
+ process.stdin.removeListener("readable", onReadable);
308
+ process.stdin.removeListener("end", onEnd);
309
+ process.stdin.removeListener("error", onError);
310
+
311
+ const chunks: Buffer[] = [];
312
+ for (let chunk = process.stdin.read(); chunk !== null; chunk = process.stdin.read()) {
313
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
314
+ }
315
+ if (chunks.length === 0) return undefined;
316
+ const text = Buffer.concat(chunks).toString("utf8");
317
+ return text.trim() ? text : undefined;
318
+ }
319
+
295
320
  export interface InteractiveModeNotify {
296
321
  kind: "warn" | "error" | "info";
297
322
  message: string;
@@ -1014,6 +1039,7 @@ export interface RunRootCommandDependencies {
1014
1039
  startupUpdate?: { check: () => Promise<string | undefined> };
1015
1040
  initTheme?: typeof initTheme;
1016
1041
  readPipedInput?: typeof readPipedInput;
1042
+ stdinIsTTY?: boolean;
1017
1043
  runStartupCredentialAutoImportIfNeeded?: typeof runStartupCredentialAutoImportIfNeeded;
1018
1044
  getChangelogForDisplay?: typeof getChangelogForDisplay;
1019
1045
  createInteractiveMode?: CreateInteractiveMode;
@@ -1164,8 +1190,16 @@ export async function runRootCommand(
1164
1190
  if (parsedArgs.noTitle || parsedArgs.mode === "acp") {
1165
1191
  Bun.env.PI_NO_TITLE = "1";
1166
1192
  }
1193
+ const hasPreparedInput = parsedArgs.messages.length > 0 || parsedArgs.fileArgs.length > 0;
1167
1194
  const { pipedInput, fileText, fileImages } = await logger.time("prepareInitialMessage", async () => {
1168
- const pipedInput = await (deps.readPipedInput ?? readPipedInput)();
1195
+ const pipedInput =
1196
+ parsedArgs.mode === "acp"
1197
+ ? undefined
1198
+ : deps.readPipedInput
1199
+ ? await deps.readPipedInput()
1200
+ : hasPreparedInput
1201
+ ? await readPipedInputIfReady()
1202
+ : await readPipedInput();
1169
1203
  if (parsedArgs.fileArgs.length === 0) {
1170
1204
  return { pipedInput, fileText: undefined, fileImages: undefined };
1171
1205
  }
@@ -1180,14 +1214,25 @@ export async function runRootCommand(
1180
1214
  fileImages,
1181
1215
  stdinContent: pipedInput,
1182
1216
  });
1183
- const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
1217
+ const disposition = resolveLaunchDisposition({
1218
+ stdinIsTTY: deps.stdinIsTTY ?? process.stdin.isTTY,
1219
+ pipedInput,
1220
+ hasPreparedInput,
1221
+ print: Boolean(parsedArgs.print),
1222
+ mode: parsedArgs.mode,
1223
+ });
1224
+ if (disposition.nonInteractiveError) {
1225
+ process.stderr.write(`${chalk.red(disposition.nonInteractiveError)}\n`);
1226
+ process.exit(1);
1227
+ }
1228
+ const autoPrint = disposition.autoPrint;
1184
1229
  const startupUpdateRoute = classifyStartupUpdateRoute(parsedArgs, autoPrint);
1185
1230
  const startupUpdate = new StartupUpdateOrchestrator(
1186
1231
  startupUpdateRoute,
1187
1232
  () => settingsInstance.get("startup.checkUpdate"),
1188
1233
  deps.startupUpdate?.check ?? (() => checkForNewVersion(VERSION)),
1189
1234
  );
1190
- const isInteractive = startupUpdateRoute === "interactive";
1235
+ const isInteractive = disposition.isInteractive;
1191
1236
  const mode = parsedArgs.mode || "text";
1192
1237
 
1193
1238
  // Initialize discovery system with settings for provider persistence
@@ -1319,6 +1364,7 @@ export async function runRootCommand(
1319
1364
  sessionOptions.modelRegistry = modelRegistry;
1320
1365
  sessionOptions.hasUI = isInteractive;
1321
1366
  sessionOptions.notificationHostModeSupported = isInteractive;
1367
+ sessionOptions.sdkHostModeSupported = isInteractive;
1322
1368
  sessionOptions.settings = settingsInstance;
1323
1369
  const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
1324
1370
 
@@ -1351,6 +1397,30 @@ export async function runRootCommand(
1351
1397
  applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, sessionOptions.model);
1352
1398
  }
1353
1399
 
1400
+ if (
1401
+ !deps.suppressProcessExit &&
1402
+ deps.createAgentSession === undefined &&
1403
+ parsedArgs.noSession === true &&
1404
+ !parsedArgs.continue &&
1405
+ parsedArgs.resume === undefined &&
1406
+ parsedArgs.fork === undefined &&
1407
+ !isInteractive &&
1408
+ mode !== "acp" &&
1409
+ !hasRootStartupProfile &&
1410
+ !sessionOptions.model &&
1411
+ !sessionOptions.modelPattern &&
1412
+ modelRegistry.getAvailable().length === 0
1413
+ ) {
1414
+ process.stderr.write(`${chalk.red(`No models available. ${formatModelOnboardingGuidance()}`)}\n`);
1415
+ process.stderr.write(
1416
+ `${chalk.yellow(`\nAdvanced manual config remains available at ${ModelsConfigFile.path()}`)}\n`,
1417
+ );
1418
+ process.exitCode = 1;
1419
+ authStorage.close();
1420
+ stopThemeWatcher();
1421
+ await postmortem.cleanup();
1422
+ return;
1423
+ }
1354
1424
  const createAgentSessionImpl = deps.createAgentSession ?? createAgentSession;
1355
1425
  const createSession: CreateSessionForMain = async (options, context): Promise<CreateAgentSessionResult> => {
1356
1426
  const result = await logger.time("createAgentSession", createAgentSessionImpl, options);
@@ -1365,10 +1435,16 @@ export async function runRootCommand(
1365
1435
  };
1366
1436
 
1367
1437
  if (mode === "acp") {
1368
- await (deps.runAcpMode ?? (await import("./modes/acp")).runAcpMode)({
1369
- agentDir: settingsInstance.getAgentDir(),
1370
- ...(acpStartupOptions ? { startupOptions: acpStartupOptions } : {}),
1371
- });
1438
+ try {
1439
+ await (deps.runAcpMode ?? (await import("./modes/acp")).runAcpMode)({
1440
+ agentDir: settingsInstance.getAgentDir(),
1441
+ ...(acpStartupOptions ? { startupOptions: acpStartupOptions } : {}),
1442
+ });
1443
+ } finally {
1444
+ authStorage.close();
1445
+ stopThemeWatcher();
1446
+ if (!deps.suppressProcessExit) await postmortem.cleanup();
1447
+ }
1372
1448
  } else {
1373
1449
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } = await createSession(
1374
1450
  sessionOptions,
@@ -1503,20 +1579,21 @@ export async function runRootCommand(
1503
1579
  }
1504
1580
  } else {
1505
1581
  const runPrint = deps.runPrintMode ?? (await import("./modes/print-mode")).runPrintMode;
1506
- await runPrint(session, {
1507
- mode,
1508
- messages: parsedArgs.messages,
1509
- initialMessage,
1510
- initialImages,
1511
- suppressProcessExit: deps.suppressProcessExit,
1512
- });
1513
- if ($env.PI_TIMING) {
1514
- logger.printTimings();
1515
- }
1516
- stopThemeWatcher();
1517
- if (!deps.suppressProcessExit) {
1518
- const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
1519
- await postmortem.quit(exitCode);
1582
+ try {
1583
+ await runPrint(session, {
1584
+ mode,
1585
+ messages: parsedArgs.messages,
1586
+ initialMessage,
1587
+ initialImages,
1588
+ suppressProcessExit: deps.suppressProcessExit,
1589
+ });
1590
+ if ($env.PI_TIMING) {
1591
+ logger.printTimings();
1592
+ }
1593
+ } finally {
1594
+ stopThemeWatcher();
1595
+ authStorage.close();
1596
+ if (!deps.suppressProcessExit) await postmortem.cleanup();
1520
1597
  }
1521
1598
  }
1522
1599
  }