@gajae-code/coding-agent 0.11.11 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -2,8 +2,22 @@ import { Command, Flags } from "@gajae-code/utils/cli";
2
2
  import { runNativeDeepInterviewCommand } from "../gjc-runtime/deep-interview-runtime";
3
3
 
4
4
  export default class DeepInterview extends Command {
5
- static description =
6
- "Run native GJC deep-interview workflow. Use `draft` for editable CLI-owned mutation inputs; JSON repair verbs are compatibility-only.";
5
+ static description = `Run native GJC deep-interview workflow.
6
+
7
+ All deep-interview state operations go through this command — no gjc state needed:
8
+ read Print the persisted envelope, revision, content sha, and any pending draft
9
+ write One-shot incremental JSON merge into state (--reset replaces; the locked
10
+ intent contract survives a reset)
11
+ stage Stage one JSON transition draft (--for <transition> --input '<json>'|@file)
12
+ check Dry-run the staged draft against current state (same merge apply performs)
13
+ apply Commit the staged draft with runtime-owned revision+sha CAS
14
+ discard Remove the pending draft
15
+ clear Clear deep-interview state for the session (lifecycle passthrough)
16
+ handoff Hand off to the next workflow skill (lifecycle passthrough)
17
+
18
+ Ambiguity is runtime-owned: apply/write derive current_ambiguity from the latest valid scored
19
+ round and clamp it to the deterministic floor. Sessions resolve from --session-id, payload
20
+ session_id, or GJC_SESSION_ID.`;
7
21
  static strict = false;
8
22
  static flags = {
9
23
  quick: Flags.boolean({ description: "Seed a quick deep-interview run" }),
@@ -15,6 +29,11 @@ export default class DeepInterview extends Command {
15
29
  "session-id": Flags.string({
16
30
  description: "Route state/spec handoff through a session-scoped .gjc/_session-{sessionid} directory",
17
31
  }),
32
+ input: Flags.string({ description: "JSON payload (or @file) for the write/stage verbs" }),
33
+ for: Flags.string({
34
+ description: "Transition for stage: initialize-context | record-round | update-facts | merge-state",
35
+ }),
36
+ reset: Flags.boolean({ description: "With write: replace state instead of incremental merge" }),
18
37
  write: Flags.boolean({ description: "Persist a final deep-interview spec through the sanctioned GJC CLI/API" }),
19
38
  stage: Flags.string({ description: 'Spec stage for --write (currently "final")' }),
20
39
  slug: Flags.string({ description: "Safe slug for .gjc/_session-{sessionid}/specs/deep-interview-<slug>.md" }),
@@ -27,13 +46,14 @@ export default class DeepInterview extends Command {
27
46
  json: Flags.boolean({ description: "Output JSON" }),
28
47
  };
29
48
  static examples = [
30
- '$ gjc deep-interview --trace --standard "<idea>" --json',
31
- "$ gjc deep-interview draft create --for initialize-context --session-id <id> --json",
32
- "$ gjc deep-interview draft edit --draft-id <id> --expected-draft-revision 1 --op set --path /type --value greenfield --json",
33
- "$ gjc deep-interview draft check --draft-id <id> --json",
34
- "$ gjc deep-interview initialize-context --draft-id <id> --expected-draft-revision <n> --json",
35
- "$ gjc deep-interview --write --stage final --slug my-feature --spec ./final-spec.md --json",
36
- "$ gjc deep-interview inspect --session-id <id> --selector summary --json",
49
+ '$ gjc deep-interview --trace --standard "<idea>"',
50
+ "$ gjc deep-interview read --json",
51
+ '$ gjc deep-interview write --input \'{"state":{"threshold":0.05}}\' --json',
52
+ '$ gjc deep-interview stage --for record-round --input \'{"state":{"rounds":[{"round":1,"round_key":"r1"}]}}\' --json',
53
+ "$ gjc deep-interview check --json",
54
+ "$ gjc deep-interview apply --json",
55
+ "$ gjc deep-interview --write --stage final --slug my-feature --spec ./final-spec.md",
56
+ "$ gjc deep-interview --write --stage final --slug my-feature --spec ./final-spec.md --deliberate",
37
57
  ];
38
58
 
39
59
  async run(): Promise<void> {
@@ -14,6 +14,7 @@ import { existsSync, readFileSync } from "node:fs";
14
14
  import * as fs from "node:fs/promises";
15
15
  import * as path from "node:path";
16
16
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
17
+ import { $credentialEnv } from "@gajae-code/utils/env";
17
18
  import {
18
19
  GJC_TMUX_OWNER_GENERATION_ENV,
19
20
  GJC_TMUX_OWNER_SERVER_KEY_ENV,
@@ -565,20 +566,42 @@ function ownerIsolationPlatform(): NodeJS.Platform {
565
566
  : "linux";
566
567
  }
567
568
 
569
+ /**
570
+ * Operator override for the process-start probe command, resolved from trusted
571
+ * environment sources only.
572
+ *
573
+ * The result is spawned, so whatever can set it chooses which binary runs.
574
+ * `$env` merges the caller's `cwd/.env` into `process.env`, so reading it there
575
+ * would let repository content pick the command; resolve it the same way
576
+ * provider credentials are (launching shell plus GJC/user-owned `.env` files,
577
+ * never the project `.env`). A malformed override stays fatal rather than
578
+ * silently falling back to `ps`, matching the previous behavior.
579
+ */
580
+ type ProcessStartCommandOverride = { kind: "none" } | { kind: "invalid" } | { kind: "command"; command: string[] };
581
+
582
+ function processStartCommandOverride(): ProcessStartCommandOverride {
583
+ const configured = $credentialEnv("GJC_HARNESS_PROCESS_START_COMMAND");
584
+ if (!configured) return { kind: "none" };
585
+ try {
586
+ const parsed = JSON.parse(configured) as unknown;
587
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some(value => typeof value !== "string" || !value))
588
+ return { kind: "invalid" };
589
+ return { kind: "command", command: parsed as string[] };
590
+ } catch {
591
+ return { kind: "invalid" };
592
+ }
593
+ }
594
+
595
+ /** Test seam: the process-start command override as resolved from trusted env. */
596
+ export function processStartCommandOverrideForTest(): ProcessStartCommandOverride {
597
+ return processStartCommandOverride();
598
+ }
599
+
568
600
  function portableProcessStartTime(pid: number): string | null {
569
601
  if (process.platform === "linux") return null;
570
- const configured = process.env.GJC_HARNESS_PROCESS_START_COMMAND;
571
- let command: string[] = ["ps", "-o", "lstart=", "-p"];
572
- if (configured) {
573
- try {
574
- const parsed = JSON.parse(configured) as unknown;
575
- if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some(value => typeof value !== "string" || !value))
576
- return null;
577
- command = parsed;
578
- } catch {
579
- return null;
580
- }
581
- }
602
+ const override = processStartCommandOverride();
603
+ if (override.kind === "invalid") return null;
604
+ const command: string[] = override.kind === "command" ? override.command : ["ps", "-o", "lstart=", "-p"];
582
605
  const result = Bun.spawnSync([...command, String(pid)], {
583
606
  stdout: "pipe",
584
607
  stderr: "ignore",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Show what the read tool will return for a given path.
3
3
  */
4
- import { Args, Command } from "@gajae-code/utils/cli";
4
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
5
  import { type ReadCommandArgs, runReadCommand } from "../cli/read-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -15,8 +15,16 @@ export default class Read extends Command {
15
15
  }),
16
16
  };
17
17
 
18
+ static flags = {
19
+ truncation: Flags.string({
20
+ options: ["head", "last", "both"],
21
+ description: "Which end of an over-budget result to keep",
22
+ }),
23
+ };
24
+
18
25
  static examples = [
19
26
  "gjc read src/foo.ts",
27
+ "gjc read src/foo.ts --truncation head",
20
28
  "gjc read src/foo.ts:50-100",
21
29
  "gjc read src/foo.ts:raw",
22
30
  "gjc read https://example.com",
@@ -25,9 +33,10 @@ export default class Read extends Command {
25
33
  ];
26
34
 
27
35
  async run(): Promise<void> {
28
- const { args } = await this.parse(Read);
36
+ const { args, flags } = await this.parse(Read);
29
37
  const cmd: ReadCommandArgs = {
30
38
  path: args.path ?? "",
39
+ truncation: flags.truncation as ReadCommandArgs["truncation"],
31
40
  };
32
41
  await initTheme();
33
42
  await runReadCommand(cmd);
@@ -173,7 +173,7 @@ export default class Team extends Command {
173
173
  "Supported operations:",
174
174
  "send-message broadcast mailbox-list mailbox-mark-delivered mailbox-mark-notified notification-list notification-read notification-replay notification-mark-pane-attempt worker-startup-ack",
175
175
  "create-task read-task list-tasks update-task claim-task transition-task-status release-task-claim",
176
- "read-config read-manifest read-worker-status update-worker-status read-worker-heartbeat recover-stale-claims update-worker-heartbeat write-worker-inbox write-worker-identity",
176
+ "read-config read-manifest read-worker-status update-worker-status read-worker-heartbeat recover-stale-claims update-worker-heartbeat read-worker-memory-guard update-worker-memory-guard apply-worker-memory-guard write-worker-inbox write-worker-identity",
177
177
  "append-event read-events read-traces await-event write-shutdown-request read-shutdown-ack read-monitor-snapshot write-monitor-snapshot read-task-approval write-task-approval",
178
178
  "Completion example:",
179
179
  'transition-task-status --input \'{"team_name":"demo","task_id":"task-1","to":"completed","claim_token":"...","completion_evidence":{"summary":"done","items":[{"kind":"command","status":"passed","summary":"focused tests passed","command":"bun test packages/coding-agent/test/gjc-runtime/team-runtime.test.ts"}]}}\' --json',
@@ -34,7 +34,7 @@ const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
34
34
 
35
35
  import { registerOAuthProvider, unregisterOAuthProviders } from "@gajae-code/ai/utils/oauth";
36
36
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@gajae-code/ai/utils/oauth/types";
37
- import { $pickenv, isRecord, logger } from "@gajae-code/utils";
37
+ import { $pickCredentialEnv, isRecord, logger } from "@gajae-code/utils";
38
38
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
39
39
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
40
40
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
@@ -450,8 +450,20 @@ function getProviderBaseUrlEnvKeys(provider: string): string[] {
450
450
  return keys;
451
451
  }
452
452
 
453
+ /**
454
+ * Provider base URL from the environment, trusted sources only.
455
+ *
456
+ * The result is baked into the provider override and reaches `model.baseUrl`,
457
+ * which the provider resolvers use as the request endpoint that carries the
458
+ * provider credential. `$env` (and therefore `$pickenv`) merges the caller's
459
+ * `cwd/.env`, so reading it there would let repository content redirect
460
+ * authenticated traffic for any provider — including re-admitting a redirect
461
+ * that the provider-level resolvers already reject. Resolve it the same way
462
+ * provider credentials are: launching shell plus GJC/user-owned `.env` files,
463
+ * never the project `.env`.
464
+ */
453
465
  function resolveProviderBaseUrlFromEnv(provider: string): string | undefined {
454
- return $pickenv(...getProviderBaseUrlEnvKeys(provider));
466
+ return $pickCredentialEnv(...getProviderBaseUrlEnvKeys(provider));
455
467
  }
456
468
 
457
469
  function normalizeLocalOpenAICompatBaseUrl(baseUrl: string): string {
@@ -2507,6 +2519,13 @@ export class ModelRegistry {
2507
2519
  return this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider);
2508
2520
  }
2509
2521
 
2522
+ /**
2523
+ * Check whether auth is configured for a provider.
2524
+ */
2525
+ hasConfiguredProviderAuth(provider: string): boolean {
2526
+ return this.#keylessProviders.has(provider) || this.authStorage.hasAuth(provider);
2527
+ }
2528
+
2510
2529
  getDiscoverableProviders(): string[] {
2511
2530
  const disabledProviders = getDisabledProviderIdsFromSettings();
2512
2531
  return this.#discoveryManager.providers
@@ -0,0 +1,42 @@
1
+ import type { AuthStorage } from "@gajae-code/ai/auth-storage";
2
+
3
+ /**
4
+ * Most recent OAuth validation outcome per provider, used only as an ordering
5
+ * hint so `/model` can rank a provider whose stored credentials failed
6
+ * validation below one that is healthy, without re-running validation itself.
7
+ *
8
+ * Entries are scoped to the `AuthStorage` that produced them and to its
9
+ * credential generation, so any login, logout, import, or deletion discards the
10
+ * stale result instead of overriding current auth state.
11
+ */
12
+ export type ProviderAuthHealth = "valid" | "invalid";
13
+
14
+ interface HealthEntry {
15
+ generation: number;
16
+ health: ProviderAuthHealth;
17
+ }
18
+
19
+ const healthByStorage = new WeakMap<AuthStorage, Map<string, HealthEntry>>();
20
+
21
+ export function recordProviderAuthHealth(
22
+ authStorage: AuthStorage,
23
+ providerId: string,
24
+ health: ProviderAuthHealth,
25
+ ): void {
26
+ let entries = healthByStorage.get(authStorage);
27
+ if (!entries) {
28
+ entries = new Map();
29
+ healthByStorage.set(authStorage, entries);
30
+ }
31
+ entries.set(providerId, { generation: authStorage.getGeneration(), health });
32
+ }
33
+
34
+ export function getProviderAuthHealth(authStorage: AuthStorage, providerId: string): ProviderAuthHealth | undefined {
35
+ const entry = healthByStorage.get(authStorage)?.get(providerId);
36
+ if (!entry || entry.generation !== authStorage.getGeneration()) return undefined;
37
+ return entry.health;
38
+ }
39
+
40
+ export function clearProviderAuthHealth(authStorage: AuthStorage): void {
41
+ healthByStorage.delete(authStorage);
42
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Shared provider ordering used by every provider-facing selector (`/login`,
3
+ * `/model`, `/provider`).
4
+ *
5
+ * Providers the user already has come first, then a curated list of well-known
6
+ * providers, then everything else alphabetically. This is the single source of
7
+ * truth for that order — surfaces must not keep their own famous-provider list.
8
+ */
9
+
10
+ /**
11
+ * Auth/config state of a provider as seen by the calling surface.
12
+ *
13
+ * - `valid` — stored credentials that validated successfully.
14
+ * - `checking` — stored credentials whose async validation is still in flight.
15
+ * Ranked with `valid` so rows do not reflow when validation resolves.
16
+ * - `configured` — no OAuth record, but the provider is present in the model
17
+ * registry with a working API key (custom/API-compatible providers).
18
+ * - `invalid` — stored credentials that failed validation (problematic login).
19
+ * - `none` — nothing stored and nothing configured.
20
+ */
21
+ export type ProviderAuthState = "valid" | "checking" | "configured" | "invalid" | "none";
22
+
23
+ export const PROVIDER_RANK_TIER = {
24
+ existing: 0,
25
+ problematic: 1,
26
+ famous: 2,
27
+ other: 3,
28
+ } as const;
29
+
30
+ export type ProviderRankTier = (typeof PROVIDER_RANK_TIER)[keyof typeof PROVIDER_RANK_TIER];
31
+
32
+ /**
33
+ * Curated provider order for the famous tier. Regional and device variants sit
34
+ * immediately behind their primary so related entries stay grouped.
35
+ */
36
+ export const FAMOUS_PROVIDER_ORDER: readonly string[] = [
37
+ "openai-codex",
38
+ "openai-codex-device",
39
+ "anthropic",
40
+ "xai",
41
+ "opencode-go",
42
+ "zai",
43
+ "glm-zcode",
44
+ "alibaba-token-plan",
45
+ "qwen-portal",
46
+ "kimi-code",
47
+ "moonshot",
48
+ "minimax-code",
49
+ "minimax-code-cn",
50
+ "xiaomi",
51
+ "xiaomi-token-plan-sgp",
52
+ "xiaomi-token-plan-ams",
53
+ "xiaomi-token-plan-cn",
54
+ "opengateway",
55
+ "bizrouter",
56
+ "github-copilot",
57
+ "cursor",
58
+ ];
59
+
60
+ const FAMOUS_PROVIDER_INDEX = new Map(FAMOUS_PROVIDER_ORDER.map((id, index) => [id, index]));
61
+
62
+ /** A provider as ranked by a surface. `label` is what the user sees. */
63
+ export interface RankableProvider {
64
+ id: string;
65
+ label: string;
66
+ authState: ProviderAuthState;
67
+ }
68
+
69
+ /** A provider's position in the ordering: its tier plus its rank inside that tier. */
70
+ export interface ProviderRank {
71
+ tier: ProviderRankTier;
72
+ intraTierRank: number;
73
+ }
74
+
75
+ /**
76
+ * The single ranking result for a provider. `intraTierRank` is the curated
77
+ * famous-list position, or `Number.MAX_SAFE_INTEGER` for providers that are not
78
+ * on the list and therefore order by display label.
79
+ */
80
+ export function rankProvider(provider: RankableProvider): ProviderRank {
81
+ return {
82
+ tier: providerRankTier(provider.authState, provider.id),
83
+ intraTierRank: FAMOUS_PROVIDER_INDEX.get(provider.id) ?? Number.MAX_SAFE_INTEGER,
84
+ };
85
+ }
86
+
87
+ export function providerRankTier(authState: ProviderAuthState, id: string): ProviderRankTier {
88
+ if (authState === "valid" || authState === "checking" || authState === "configured") {
89
+ return PROVIDER_RANK_TIER.existing;
90
+ }
91
+ if (authState === "invalid") return PROVIDER_RANK_TIER.problematic;
92
+ return FAMOUS_PROVIDER_INDEX.has(id) ? PROVIDER_RANK_TIER.famous : PROVIDER_RANK_TIER.other;
93
+ }
94
+
95
+ /** Position within the famous list, or `undefined` for providers not on it. */
96
+ export function famousProviderIndex(id: string): number | undefined {
97
+ return FAMOUS_PROVIDER_INDEX.get(id);
98
+ }
99
+
100
+ /**
101
+ * Total order over providers: tier, then famous-list position, then display
102
+ * label, then id. The trailing id comparison guarantees no ties.
103
+ */
104
+ export function compareRankedProviders(left: RankableProvider, right: RankableProvider): number {
105
+ const leftRank = rankProvider(left);
106
+ const rightRank = rankProvider(right);
107
+ if (leftRank.tier !== rightRank.tier) return leftRank.tier - rightRank.tier;
108
+ if (leftRank.intraTierRank !== rightRank.intraTierRank) return leftRank.intraTierRank - rightRank.intraTierRank;
109
+
110
+ const label = left.label.localeCompare(right.label);
111
+ if (label !== 0) return label;
112
+
113
+ return left.id.localeCompare(right.id);
114
+ }
115
+
116
+ /** Convenience wrapper returning a new array in ranked order. */
117
+ export function sortRankedProviders<T extends RankableProvider>(providers: readonly T[]): T[] {
118
+ return [...providers].sort(compareRankedProviders);
119
+ }
@@ -506,6 +506,11 @@ export const SETTINGS_SCHEMA = {
506
506
  default: 5,
507
507
  validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 20,
508
508
  },
509
+ "gjc.ralplan.maxReviewPassesPerLane": {
510
+ type: "number",
511
+ default: 1,
512
+ validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 10,
513
+ },
509
514
 
510
515
  // ────────────────────────────────────────────────────────────────────────
511
516
  // Appearance
@@ -2054,7 +2059,8 @@ export const SETTINGS_SCHEMA = {
2054
2059
  ui: {
2055
2060
  tab: "editing",
2056
2061
  label: "Default Read Limit",
2057
- description: "Default number of lines returned when agent calls read without a limit",
2062
+ description:
2063
+ "Default collection/selection limit for read operations; bare local receipts use the separate 50-line / 10 KiB receipt budgets",
2058
2064
  options: [
2059
2065
  { value: "200", label: "200 lines" },
2060
2066
  { value: "300", label: "300 lines" },
@@ -2094,6 +2100,22 @@ export const SETTINGS_SCHEMA = {
2094
2100
  ],
2095
2101
  },
2096
2102
  },
2103
+ "read.truncation": {
2104
+ type: "enum",
2105
+ values: ["head", "last", "both"] as const,
2106
+ default: "last",
2107
+ ui: {
2108
+ tab: "editing",
2109
+ label: "Read Truncation",
2110
+ description:
2111
+ "Configured default direction for routes that support directional truncation; bare local and archive reads use this value (factory default: last), while explicit truncation always wins",
2112
+ options: [
2113
+ { value: "head", label: "Head", description: "Keep the first N lines" },
2114
+ { value: "last", label: "Last", description: "Keep the last N lines (default)" },
2115
+ { value: "both", label: "Both", description: "Keep the start and the end, elide the middle" },
2116
+ ],
2117
+ },
2118
+ },
2097
2119
  "read.summaryMaxBytes": {
2098
2120
  type: "number",
2099
2121
  default: 20,