@gajae-code/coding-agent 0.12.8 → 0.12.11

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/sdk.d.ts +23 -0
  4. package/dist/types/discovery/agents-md.d.ts +11 -1
  5. package/dist/types/modes/types.d.ts +6 -0
  6. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
  7. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  8. package/dist/types/sdk/bus/notification-service.d.ts +23 -0
  9. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -3
  10. package/dist/types/session/internal/managed-session-storage.d.ts +20 -2
  11. package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
  12. package/dist/types/session/session-manager.d.ts +17 -0
  13. package/dist/types/session/session-storage.d.ts +1 -0
  14. package/dist/types/system-prompt.d.ts +12 -1
  15. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  16. package/dist/types/tools/computer.d.ts +35 -2
  17. package/dist/types/tools/todo-write.d.ts +2 -0
  18. package/package.json +8 -8
  19. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  20. package/src/cli/notify-cli.ts +12 -6
  21. package/src/commands/sdk.ts +54 -0
  22. package/src/config/model-profiles.ts +8 -0
  23. package/src/config/model-registry.ts +4 -1
  24. package/src/discovery/agents-md.ts +76 -19
  25. package/src/gjc-runtime/psmux-detect.ts +12 -16
  26. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  27. package/src/internal-urls/docs-index.generated.ts +6 -5
  28. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  29. package/src/modes/controllers/selector-controller.ts +61 -43
  30. package/src/modes/print-mode.ts +3 -0
  31. package/src/modes/types.ts +6 -0
  32. package/src/prompts/system/system-prompt.md +1 -0
  33. package/src/prompts/tools/computer.md +6 -1
  34. package/src/sdk/bus/chat-daemon-control.ts +5 -3
  35. package/src/sdk/bus/daemon-paths.ts +2 -0
  36. package/src/sdk/bus/notification-service.ts +140 -7
  37. package/src/sdk/bus/telegram-daemon-contract.ts +4 -3
  38. package/src/sdk/session.ts +24 -7
  39. package/src/session/agent-session.ts +176 -34
  40. package/src/session/internal/managed-session-storage.ts +280 -248
  41. package/src/session/internal/native-publish-outcome.ts +12 -2
  42. package/src/session/session-manager.ts +140 -6
  43. package/src/session/session-storage.ts +3 -0
  44. package/src/slash-commands/builtin-registry.ts +35 -11
  45. package/src/system-prompt.ts +28 -15
  46. package/src/tools/bash.ts +3 -1
  47. package/src/tools/composer-bash-policy.ts +21 -5
  48. package/src/tools/computer.ts +322 -45
  49. package/src/tools/todo-write.ts +29 -11
  50. package/src/utils/command-args.ts +19 -30
  51. package/src/utils/lang-from-path.ts +2 -4
@@ -16,6 +16,8 @@ export type NativePublishOperation = "direct_rename" | "retained_file" | "retain
16
16
 
17
17
  export type NativePublishPrimitive =
18
18
  | "renameat2_noreplace"
19
+ | "linkat_noreplace"
20
+ | "mkdirat_renameat_noreplace"
19
21
  | "renameatx_np_excl"
20
22
  | "windows_rename_noreplace"
21
23
  | "unsupported"
@@ -24,6 +26,7 @@ export type NativePublishPhase =
24
26
  | "preflight"
25
27
  | "file_sync"
26
28
  | "rename"
29
+ | "source_unlink"
27
30
  | "source_parent_sync"
28
31
  | "destination_parent_sync"
29
32
  | "terminal_identity"
@@ -83,6 +86,8 @@ const reasons = new Set<NativePublishReason>([
83
86
  ]);
84
87
  const primitives = new Set<NativePublishPrimitive>([
85
88
  "renameat2_noreplace",
89
+ "linkat_noreplace",
90
+ "mkdirat_renameat_noreplace",
86
91
  "renameatx_np_excl",
87
92
  "windows_rename_noreplace",
88
93
  "unsupported",
@@ -92,6 +97,7 @@ const phases = new Set<NativePublishPhase>([
92
97
  "preflight",
93
98
  "file_sync",
94
99
  "rename",
100
+ "source_unlink",
95
101
  "source_parent_sync",
96
102
  "destination_parent_sync",
97
103
  "terminal_identity",
@@ -204,7 +210,7 @@ function legalOutcome(outcome: NativePublishOutcome): boolean {
204
210
  ((outcome.reason === "durability_not_provable" &&
205
211
  ["file_sync", "source_parent_sync", "destination_parent_sync"].includes(outcome.phase)) ||
206
212
  (outcome.reason === "identity_violation" && outcome.phase === "terminal_identity") ||
207
- (outcome.reason === "io_failure" && outcome.phase === "terminal_identity"))
213
+ (outcome.reason === "io_failure" && ["source_unlink", "terminal_identity"].includes(outcome.phase)))
208
214
  );
209
215
  }
210
216
 
@@ -247,8 +253,12 @@ export function classifyNativePublishOutcome(
247
253
  const outcome = value as unknown as NativePublishOutcome;
248
254
  if (!legalOutcome(outcome)) return malformed;
249
255
  if (operation === "direct_rename") return outcome;
256
+ const retainedPrimitiveValid =
257
+ operation === "retained_file"
258
+ ? outcome.primitive === "renameat2_noreplace" || outcome.primitive === "linkat_noreplace"
259
+ : outcome.primitive === "renameat2_noreplace" || outcome.primitive === "mkdirat_renameat_noreplace";
250
260
  if (
251
- outcome.primitive !== "renameat2_noreplace" ||
261
+ !retainedPrimitiveValid ||
252
262
  (outcome.ok && (!outcome.identity || outcome.durabilityState !== "proven" || outcome.phase !== "complete"))
253
263
  )
254
264
  return malformed;
@@ -899,6 +899,7 @@ export interface ResumeSessionIdentity {
899
899
  size: number;
900
900
  mtimeMs: number;
901
901
  mtimeNs: bigint;
902
+ ctimeNs?: bigint;
902
903
  sha256: string;
903
904
  }
904
905
 
@@ -918,6 +919,21 @@ export interface ResumeTailError {
918
919
  }
919
920
 
920
921
  export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
922
+ export type SessionAppendPersistenceFailurePhase = "current_append" | "prior_failure";
923
+
924
+ export class SessionAppendPersistenceError extends Error {
925
+ readonly phase: SessionAppendPersistenceFailurePhase;
926
+ readonly entryId: string;
927
+ readonly persistenceError: Error;
928
+
929
+ constructor(phase: SessionAppendPersistenceFailurePhase, entryId: string, persistenceError: Error) {
930
+ super(persistenceError.message, { cause: persistenceError });
931
+ this.name = "SessionAppendPersistenceError";
932
+ this.phase = phase;
933
+ this.entryId = entryId;
934
+ this.persistenceError = persistenceError;
935
+ }
936
+ }
921
937
 
922
938
  export class SessionManagedStorageError extends Error {
923
939
  readonly code = "managed_storage_unsupported";
@@ -2161,6 +2177,7 @@ function sameResumeIdentity(left: ResumeSessionIdentity, right: ResumeSessionIde
2161
2177
  left.size === right.size &&
2162
2178
  left.mtimeMs === right.mtimeMs &&
2163
2179
  left.mtimeNs === right.mtimeNs &&
2180
+ (left.ctimeNs === undefined || right.ctimeNs === undefined || left.ctimeNs === right.ctimeNs) &&
2164
2181
  left.sha256 === right.sha256
2165
2182
  );
2166
2183
  }
@@ -2174,7 +2191,8 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
2174
2191
  left.nlink === right.nlink &&
2175
2192
  left.size === right.size &&
2176
2193
  left.mtimeMs === right.mtimeMs &&
2177
- left.mtimeNs === right.mtimeNs
2194
+ left.mtimeNs === right.mtimeNs &&
2195
+ left.ctimeNs === right.ctimeNs
2178
2196
  );
2179
2197
  }
2180
2198
 
@@ -2372,6 +2390,7 @@ function inspectResumeSessionFile(
2372
2390
  size: snapshot.size,
2373
2391
  mtimeMs: snapshot.mtimeMs,
2374
2392
  mtimeNs: snapshot.mtimeNs,
2393
+ ctimeNs: snapshot.ctimeNs,
2375
2394
  sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
2376
2395
  };
2377
2396
  return { identity, content: bytes, entries, context, migrationApplied };
@@ -2380,6 +2399,46 @@ function inspectResumeSessionFile(
2380
2399
  }
2381
2400
  }
2382
2401
 
2402
+ function fsyncResumeSessionIdentity(expected: ResumeSessionIdentity): void {
2403
+ const fd = fs.openSync(expected.canonicalPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2404
+ try {
2405
+ const before = fs.fstatSync(fd, { bigint: true });
2406
+ if (
2407
+ before.dev !== expected.dev ||
2408
+ before.ino !== expected.ino ||
2409
+ (expected.nlink !== undefined && before.nlink !== expected.nlink) ||
2410
+ Number(before.size) !== expected.size ||
2411
+ before.mtimeNs !== expected.mtimeNs ||
2412
+ (expected.ctimeNs !== undefined && before.ctimeNs !== expected.ctimeNs)
2413
+ ) {
2414
+ throw new Error("session_persistence_recovery_identity_mismatch");
2415
+ }
2416
+ fs.fsyncSync(fd);
2417
+ const after = fs.fstatSync(fd, { bigint: true });
2418
+ const named = fs.lstatSync(expected.canonicalPath, { bigint: true });
2419
+ if (
2420
+ after.dev !== before.dev ||
2421
+ after.ino !== before.ino ||
2422
+ after.nlink !== before.nlink ||
2423
+ after.size !== before.size ||
2424
+ after.mtimeNs !== before.mtimeNs ||
2425
+ after.ctimeNs !== before.ctimeNs ||
2426
+ named.dev !== after.dev ||
2427
+ named.ino !== after.ino ||
2428
+ named.nlink !== after.nlink
2429
+ ) {
2430
+ throw new Error("session_persistence_recovery_identity_mismatch");
2431
+ }
2432
+ } finally {
2433
+ fs.closeSync(fd);
2434
+ }
2435
+ const parentFd = fs.openSync(path.dirname(expected.canonicalPath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
2436
+ try {
2437
+ fs.fsyncSync(parentFd);
2438
+ } finally {
2439
+ fs.closeSync(parentFd);
2440
+ }
2441
+ }
2383
2442
  /** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
2384
2443
  function revalidateResumeSessionIdentity(
2385
2444
  filePath: string,
@@ -2410,6 +2469,7 @@ function revalidateResumeSessionIdentity(
2410
2469
  size: snapshot.stat.size,
2411
2470
  mtimeMs: snapshot.stat.mtimeMs,
2412
2471
  mtimeNs: snapshot.stat.mtimeNs,
2472
+ ctimeNs: snapshot.stat.ctimeNs,
2413
2473
  sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
2414
2474
  };
2415
2475
  return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
@@ -4751,7 +4811,9 @@ export class SessionManager {
4751
4811
  #recoveryPromotionTranscriptPath: string | undefined;
4752
4812
  #memoryGuardParticipantIngressToken: symbol | undefined;
4753
4813
  #fileEntries: FileEntry[] = [];
4754
- #pendingStrictAdoption: { canonicalPath: string; identity: ResumeSessionIdentity } | undefined;
4814
+ #pendingStrictAdoption:
4815
+ | { canonicalPath: string; identity: ResumeSessionIdentity; inspection?: ResumeInspectionSnapshot }
4816
+ | undefined;
4755
4817
  #byId: Map<string, SessionEntry> = new Map();
4756
4818
  #labelsById: Map<string, string> = new Map();
4757
4819
  #leafId: string | null = null;
@@ -5501,11 +5563,14 @@ export class SessionManager {
5501
5563
  if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
5502
5564
  const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
5503
5565
  if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
5504
- throw new Error("Prepared managed session changed before strict adoption.");
5566
+ throw new Error("Prepared session changed before strict adoption.");
5505
5567
  }
5506
5568
  let entries: FileEntry[];
5507
5569
  let candidateMigrationApplied = false;
5508
- if (this.storage.existsSync(resolvedSessionFile)) {
5570
+ if (strictAdoption?.inspection && resolvedSessionFile === strictAdoption.canonicalPath) {
5571
+ entries = strictAdoption.inspection.entries;
5572
+ candidateMigrationApplied = strictAdoption.inspection.migrationApplied;
5573
+ } else if (this.storage.existsSync(resolvedSessionFile)) {
5509
5574
  const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
5510
5575
  if ("kind" in inspected) throw new Error(`Could not switch session: ${inspected.reason}`);
5511
5576
  entries = inspected.entries;
@@ -5516,7 +5581,7 @@ export class SessionManager {
5516
5581
  if (strictAdoption) {
5517
5582
  const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
5518
5583
  if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
5519
- throw new Error("Prepared managed session changed during strict adoption.");
5584
+ throw new Error("Prepared session changed during strict adoption.");
5520
5585
  }
5521
5586
  if (entries.length > 0) {
5522
5587
  const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
@@ -5541,6 +5606,13 @@ export class SessionManager {
5541
5606
  prepared.dispose();
5542
5607
  throw error;
5543
5608
  }
5609
+ if (strictAdoption) {
5610
+ const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
5611
+ if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity)) {
5612
+ prepared.dispose();
5613
+ throw new Error("Prepared session changed before strict adoption commit.");
5614
+ }
5615
+ }
5544
5616
  const previous = {
5545
5617
  sessionId: this.#sessionId,
5546
5618
  sessionName: this.#sessionName,
@@ -8265,13 +8337,31 @@ export class SessionManager {
8265
8337
  this.#assertRecoveryHydrationWritable();
8266
8338
  const normalizedEntry = normalizeSessionEntryForStorage(entry);
8267
8339
  const residentEntry = this.#prepareEntryForCurrentResidentStore(normalizedEntry) as SessionEntry;
8340
+ const previousLeafId = this.#leafId;
8341
+ const priorPersistenceError = this.#persistError;
8268
8342
  this.#fileEntries.push(residentEntry);
8269
8343
  this.#byId.set(residentEntry.id, residentEntry);
8270
8344
  this.#leafId = residentEntry.id;
8271
8345
  this.#bumpEntryRevision();
8272
8346
  this.#leafRevision++;
8273
8347
  if (entry.type === "label") this.#labelRevision++;
8274
- this._persist(residentEntry);
8348
+ try {
8349
+ this._persist(residentEntry);
8350
+ } catch (error) {
8351
+ const removed = this.#fileEntries.pop();
8352
+ if (removed !== residentEntry)
8353
+ throw new Error("Session append rollback lost resident ordering.", { cause: error });
8354
+ this.#byId.delete(residentEntry.id);
8355
+ this.#leafId = previousLeafId;
8356
+ this.#bumpEntryRevision();
8357
+ this.#leafRevision++;
8358
+ if (entry.type === "label") this.#labelRevision++;
8359
+ throw new SessionAppendPersistenceError(
8360
+ priorPersistenceError ? "prior_failure" : "current_append",
8361
+ residentEntry.id,
8362
+ this.#persistError ?? toError(error),
8363
+ );
8364
+ }
8275
8365
  // Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
8276
8366
  // malformed task-result or assistant usage shape reaching the append path must not
8277
8367
  // crash or poison getUsageStatistics() either.
@@ -8513,6 +8603,50 @@ export class SessionManager {
8513
8603
  this.#replayMetadataRevision++;
8514
8604
  }
8515
8605
 
8606
+ /**
8607
+ * Rehydrate the canonical transcript after a synchronous persistence failure.
8608
+ *
8609
+ * The failed append may have committed before reporting an uncertain outcome,
8610
+ * so callers must not clear the sticky error or retry against the resident
8611
+ * branch. Reloading the exact session file is the only supported recovery
8612
+ * boundary for both managed and explicit persistent destinations.
8613
+ */
8614
+ async recoverPersistenceFailure(): Promise<void> {
8615
+ const persistenceError = this.#persistError;
8616
+ if (!persistenceError) return;
8617
+ if (!this.#sessionFile) throw persistenceError;
8618
+ const sessionFile = this.#sessionFile;
8619
+ try {
8620
+ await this.#closePersistWriter();
8621
+ } catch {
8622
+ // A writer reports its prior write failure after a confirmed close. A
8623
+ // second close clears that terminal writer; retryable close failures are
8624
+ // genuinely re-dispatched and still fail if the descriptor cannot settle.
8625
+ }
8626
+ await this.#closePersistWriter();
8627
+ const inspected = inspectResumeSessionFile(sessionFile, this.storage);
8628
+ if ("kind" in inspected) throw persistenceError;
8629
+ try {
8630
+ fsyncResumeSessionIdentity(inspected.identity);
8631
+ } catch {
8632
+ throw persistenceError;
8633
+ }
8634
+ const durable = inspectResumeSessionFile(sessionFile, this.storage);
8635
+ if ("kind" in durable || !sameResumeIdentity(inspected.identity, durable.identity)) {
8636
+ throw persistenceError;
8637
+ }
8638
+ const adoption = {
8639
+ canonicalPath: durable.identity.canonicalPath,
8640
+ identity: durable.identity,
8641
+ inspection: durable,
8642
+ };
8643
+ this.#pendingStrictAdoption = adoption;
8644
+ try {
8645
+ await this.setSessionFile(durable.identity.canonicalPath);
8646
+ } finally {
8647
+ if (this.#pendingStrictAdoption === adoption) this.#pendingStrictAdoption = undefined;
8648
+ }
8649
+ }
8516
8650
  /**
8517
8651
  * Rewrite the session file after in-place entry updates.
8518
8652
  * Use sparingly (e.g., pruning old tool outputs).
@@ -28,6 +28,7 @@ export interface SessionStorageStat {
28
28
  size: number;
29
29
  mtimeMs: number;
30
30
  mtimeNs: bigint;
31
+ ctimeNs: bigint;
31
32
  mtime: Date;
32
33
  isFile: boolean;
33
34
  }
@@ -47,6 +48,7 @@ function statFromNode(stats: fs.BigIntStats): SessionStorageStat {
47
48
  size: Number(stats.size),
48
49
  mtimeMs: Number(stats.mtimeMs),
49
50
  mtimeNs: stats.mtimeNs,
51
+ ctimeNs: stats.ctimeNs,
50
52
  mtime: stats.mtime,
51
53
  isFile: stats.isFile(),
52
54
  };
@@ -1624,6 +1626,7 @@ export class MemorySessionStorage implements SessionStorage {
1624
1626
  size: entry.content.byteLength,
1625
1627
  mtimeMs: entry.mtimeMs,
1626
1628
  mtimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
1629
+ ctimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
1627
1630
  mtime: new Date(entry.mtimeMs),
1628
1631
  isFile: true,
1629
1632
  };
@@ -417,6 +417,9 @@ function modelSelectionUsage(runtime: SlashCommandRuntime, currentModelLine?: st
417
417
  .join("\n\n");
418
418
  }
419
419
 
420
+ /** Opt into the paste-a-code OAuth login for browsers that cannot reach this machine. */
421
+ const MANUAL_LOGIN_FLAG = "--manual";
422
+
420
423
  const EFFORT_COMMAND_INPUT_HINT = "[inherit|off|minimal|low|medium|high|xhigh|max]";
421
424
  const EFFORT_COMMAND_ACCEPTED_VALUES = ["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
422
425
 
@@ -1364,20 +1367,45 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1364
1367
  {
1365
1368
  name: "login",
1366
1369
  description: "Login with OAuth provider",
1367
- inlineHint: "[provider|redirect URL]",
1370
+ inlineHint: "[provider|redirect URL] [--manual]",
1368
1371
  allowArgs: true,
1369
1372
  handleTui: (command, runtime) => {
1370
1373
  const manualInput = runtime.ctx.oauthManualInput;
1371
1374
  const args = command.args.trim();
1375
+ const pendingLoginMessage = (): string => {
1376
+ const pendingProvider = manualInput.pendingProviderId;
1377
+ return pendingProvider
1378
+ ? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
1379
+ : "OAuth login already in progress. Paste the redirect URL with /login <url>.";
1380
+ };
1372
1381
  if (args.length > 0) {
1382
+ const tokens = args.split(/\s+/);
1383
+ // `--manual` is resolved before the paste fallback below, otherwise
1384
+ // `/login anthropic --manual` would be submitted as an authorization code.
1385
+ if (tokens.includes(MANUAL_LOGIN_FLAG)) {
1386
+ const rest = tokens.filter(token => token !== MANUAL_LOGIN_FLAG);
1387
+ const requestedProvider = rest.length === 1 ? rest[0] : undefined;
1388
+ const manualProvider = requestedProvider
1389
+ ? getOAuthProviders().find(provider => provider.id === requestedProvider)
1390
+ : undefined;
1391
+ if (!manualProvider) {
1392
+ runtime.ctx.showWarning(`Usage: /login <provider> ${MANUAL_LOGIN_FLAG}`);
1393
+ runtime.ctx.editor.setText("");
1394
+ return;
1395
+ }
1396
+ if (manualInput.hasPending()) {
1397
+ runtime.ctx.showWarning(pendingLoginMessage());
1398
+ runtime.ctx.editor.setText("");
1399
+ return;
1400
+ }
1401
+ void runtime.ctx.showOAuthSelector("login", manualProvider.id, { manualCode: true });
1402
+ runtime.ctx.editor.setText("");
1403
+ return;
1404
+ }
1373
1405
  const matchedProvider = getOAuthProviders().find(provider => provider.id === args);
1374
1406
  if (matchedProvider) {
1375
1407
  if (manualInput.hasPending()) {
1376
- const pendingProvider = manualInput.pendingProviderId;
1377
- const message = pendingProvider
1378
- ? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
1379
- : "OAuth login already in progress. Paste the redirect URL with /login <url>.";
1380
- runtime.ctx.showWarning(message);
1408
+ runtime.ctx.showWarning(pendingLoginMessage());
1381
1409
  runtime.ctx.editor.setText("");
1382
1410
  return;
1383
1411
  }
@@ -1396,11 +1424,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1396
1424
  }
1397
1425
 
1398
1426
  if (manualInput.hasPending()) {
1399
- const provider = manualInput.pendingProviderId;
1400
- const message = provider
1401
- ? `OAuth login already in progress for ${provider}. Paste the redirect URL with /login <url>.`
1402
- : "OAuth login already in progress. Paste the redirect URL with /login <url>.";
1403
- runtime.ctx.showWarning(message);
1427
+ runtime.ctx.showWarning(pendingLoginMessage());
1404
1428
  runtime.ctx.editor.setText("");
1405
1429
  return;
1406
1430
  }
@@ -245,10 +245,14 @@ function dedupeExactContextFiles(
245
245
  // Keep the closest matching context entry when content is byte-for-byte identical.
246
246
  lastIndexByContent.set(file.content, index);
247
247
  }
248
-
249
248
  return contextFiles.filter((file, index) => lastIndexByContent.get(file.content) === index);
250
249
  }
251
250
 
251
+ export interface ProjectContextFilesResult {
252
+ contextFiles: Array<{ path: string; content: string; depth?: number }>;
253
+ warnings: string[];
254
+ }
255
+
252
256
  /**
253
257
  * Load all context files using the capability API.
254
258
  * Returns {path, content, depth} entries for all discovered context files.
@@ -257,11 +261,10 @@ function dedupeExactContextFiles(
257
261
  * prominent. User-home files from foreign providers (`~/.claude/CLAUDE.md`,
258
262
  * `~/.codex/AGENTS.md`, …) stay excluded — only gjc's own user config applies.
259
263
  */
260
- export async function loadProjectContextFiles(
264
+ export async function loadProjectContextFilesResult(
261
265
  options: LoadContextFilesOptions = {},
262
- ): Promise<Array<{ path: string; content: string; depth?: number }>> {
266
+ ): Promise<ProjectContextFilesResult> {
263
267
  const resolvedCwd = options.cwd ?? getProjectDir();
264
-
265
268
  const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
266
269
  const items = result.items as ContextFile[];
267
270
 
@@ -288,7 +291,17 @@ export async function loadProjectContextFiles(
288
291
  return depthB - depthA;
289
292
  });
290
293
 
291
- return dedupeExactContextFiles([...userFiles, ...projectFiles]);
294
+ return {
295
+ contextFiles: dedupeExactContextFiles([...userFiles, ...projectFiles]),
296
+ warnings: result.warnings,
297
+ };
298
+ }
299
+
300
+ /** Load project context files without exposing discovery diagnostics. */
301
+ export async function loadProjectContextFiles(
302
+ options: LoadContextFilesOptions = {},
303
+ ): Promise<Array<{ path: string; content: string; depth?: number }>> {
304
+ return (await loadProjectContextFilesResult(options)).contextFiles;
292
305
  }
293
306
 
294
307
  /**
@@ -386,10 +399,11 @@ export interface BuildSystemPromptOptions {
386
399
  subagent?: boolean;
387
400
  }
388
401
 
389
- /** Result of building provider-facing system prompt messages. */
390
402
  export interface BuildSystemPromptResult {
391
403
  /** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
392
404
  systemPrompt: string[];
405
+ /** Context-file discovery warnings visible to SDK and session callers. */
406
+ warnings: string[];
393
407
  }
394
408
  export interface BuildVolatileProjectContextOptions {
395
409
  cwd?: string;
@@ -421,7 +435,7 @@ export function buildVolatileProjectContext(options: BuildVolatileProjectContext
421
435
  /** Build the system prompt with tools, guidelines, and context */
422
436
  export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
423
437
  if ($env.NULL_PROMPT === "true") {
424
- return { systemPrompt: [] };
438
+ return { systemPrompt: [], warnings: [] };
425
439
  }
426
440
 
427
441
  const {
@@ -448,7 +462,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
448
462
  resolvedCustomPrompt: undefined as string | undefined,
449
463
  resolvedAppendPrompt: undefined as string | undefined,
450
464
  systemPromptCustomization: null as string | null,
451
- contextFiles: dedupeExactContextFiles(providedContextFiles ?? []),
465
+ contextFiles: { contextFiles: [] as Array<{ path: string; content: string; depth?: number }>, warnings: [] },
452
466
  workspaceTree: {
453
467
  rootPath: resolvedCwd,
454
468
  rendered: "",
@@ -490,8 +504,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
490
504
  cwd: resolvedCwd,
491
505
  });
492
506
  const contextFilesPromise = providedContextFiles
493
- ? Promise.resolve(providedContextFiles)
494
- : logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
507
+ ? Promise.resolve({ contextFiles: providedContextFiles, warnings: [] })
508
+ : logger.time("loadProjectContextFiles", loadProjectContextFilesResult, { cwd: resolvedCwd });
495
509
  const workspaceTreePromise =
496
510
  providedWorkspaceTree !== undefined
497
511
  ? Promise.resolve(providedWorkspaceTree)
@@ -499,7 +513,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
499
513
  buildWorkspaceTree(resolvedCwd, { timeoutMs: SYSTEM_PROMPT_PREP_TIMEOUT_MS }),
500
514
  );
501
515
 
502
- const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFiles, workspaceTree] =
516
+ const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFileResult, workspaceTree] =
503
517
  await Promise.all([
504
518
  withDeadline(
505
519
  "customPrompt",
@@ -516,11 +530,10 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
516
530
  systemPromptCustomizationPromise,
517
531
  prepDefaults.systemPromptCustomization,
518
532
  ),
519
- withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles).then(
520
- dedupeExactContextFiles,
521
- ),
533
+ withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles),
522
534
  withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
523
535
  ]);
536
+ const contextFiles = dedupeExactContextFiles(contextFileResult.contextFiles);
524
537
  const agentsMdFiles = Array.from(new Set(workspaceTree.agentsMdFiles)).sort().slice(0, AGENTS_MD_LIMIT);
525
538
 
526
539
  if (timedOut.length > 0) {
@@ -628,5 +641,5 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
628
641
  systemPrompt.push(pluginAppendices.trim());
629
642
  }
630
643
 
631
- return { systemPrompt };
644
+ return { systemPrompt, warnings: contextFileResult.warnings };
632
645
  }
package/src/tools/bash.ts CHANGED
@@ -982,8 +982,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
982
982
  }
983
983
  }
984
984
 
985
+ const activeModel = this.session.model;
985
986
  const composerPolicy = checkComposerBashPolicy({
986
- modelId: this.session.getActiveModelString?.() ?? this.session.getModelString?.() ?? this.session.model?.id,
987
+ modelId: this.session.getActiveModelString?.() ?? this.session.getModelString?.() ?? activeModel?.id,
988
+ provider: activeModel?.provider,
987
989
  commands: rawCommand === command ? [command] : [rawCommand, command],
988
990
  });
989
991
  if (!composerPolicy.allowed) {
@@ -1,7 +1,11 @@
1
- import { isComposerHarnessModel } from "@gajae-code/ai/providers/composer-discipline";
1
+ import {
2
+ type ComposerBashPolicyToolSurface,
3
+ formatComposerBashPolicyError,
4
+ isComposerHarnessModel,
5
+ } from "@gajae-code/ai/providers/composer-discipline";
2
6
 
3
- export const COMPOSER_BASH_POLICY_ERROR =
4
- "Composer bash policy blocked repository file I/O. Use find, search, read, and edit tools for file discovery, file inspection, and file mutation.";
7
+ export const COMPOSER_BASH_POLICY_ERROR = formatComposerBashPolicyError("generic");
8
+ export const CURSOR_COMPOSER_BASH_POLICY_ERROR = formatComposerBashPolicyError("cursor");
5
9
 
6
10
  type ComposerBashPolicyResult =
7
11
  | { allowed: true }
@@ -9,6 +13,7 @@ type ComposerBashPolicyResult =
9
13
  allowed: false;
10
14
  reason: string;
11
15
  message: string;
16
+ surface: ComposerBashPolicyToolSurface;
12
17
  };
13
18
 
14
19
  const BLOCK_PATTERNS: Array<{ id: string; pattern: RegExp }> = [
@@ -77,19 +82,30 @@ export function isComposerBashPolicyModel(modelId: string | undefined): boolean
77
82
  return Boolean(modelId && isComposerHarnessModel(modelId));
78
83
  }
79
84
 
85
+ export function resolveComposerBashPolicyToolSurface(input: {
86
+ modelId?: string;
87
+ provider?: string;
88
+ }): ComposerBashPolicyToolSurface {
89
+ if (input.provider?.toLowerCase() === "cursor") return "cursor";
90
+ return /(?:^|[/:._-])cursor(?:[/:._-]|$)/i.test(input.modelId ?? "") ? "cursor" : "generic";
91
+ }
92
+
80
93
  export function checkComposerBashPolicy(input: {
81
94
  modelId?: string;
95
+ provider?: string;
82
96
  commands: readonly string[];
83
97
  }): ComposerBashPolicyResult {
84
98
  if (!isComposerBashPolicyModel(input.modelId)) return { allowed: true };
99
+ const surface = resolveComposerBashPolicyToolSurface(input);
100
+ const message = formatComposerBashPolicyError(surface);
85
101
  for (const command of input.commands) {
86
102
  for (const block of BLOCK_PATTERNS) {
87
103
  if (block.pattern.test(command)) {
88
- return { allowed: false, reason: block.id, message: COMPOSER_BASH_POLICY_ERROR };
104
+ return { allowed: false, reason: block.id, message, surface };
89
105
  }
90
106
  }
91
107
  if (!isAllowedComposerTerminalCommand(command)) {
92
- return { allowed: false, reason: "not-allowlisted", message: COMPOSER_BASH_POLICY_ERROR };
108
+ return { allowed: false, reason: "not-allowlisted", message, surface };
93
109
  }
94
110
  }
95
111
  return { allowed: true };