@gajae-code/coding-agent 0.11.6 → 0.11.8

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 (259) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/commands/daemon.d.ts +2 -2
  6. package/dist/types/commands/sdk.d.ts +20 -2
  7. package/dist/types/config/keybindings.d.ts +34 -4
  8. package/dist/types/config/model-profile-activation.d.ts +4 -3
  9. package/dist/types/config/model-profile-contract.d.ts +38 -0
  10. package/dist/types/config/settings-schema.d.ts +70 -7
  11. package/dist/types/edit/modes/patch.d.ts +13 -0
  12. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  13. package/dist/types/eval/py/executor.d.ts +1 -1
  14. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  16. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
  18. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
  20. package/dist/types/main.d.ts +32 -1
  21. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  22. package/dist/types/modes/components/model-selector.d.ts +2 -0
  23. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  25. package/dist/types/modes/components/welcome.d.ts +2 -0
  26. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  27. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  28. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  29. package/dist/types/modes/interactive-mode.d.ts +7 -2
  30. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  31. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  32. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  33. package/dist/types/modes/types.d.ts +4 -0
  34. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  35. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/sdk/broker/broker.d.ts +2 -0
  40. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  41. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
  42. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  43. package/dist/types/sdk/bus/config.d.ts +2 -0
  44. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  45. package/dist/types/sdk/bus/index.d.ts +26 -1
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  49. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  50. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
  51. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  52. package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
  53. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  54. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  55. package/dist/types/sdk/client/discovery.d.ts +2 -0
  56. package/dist/types/sdk/client/liveness.d.ts +7 -0
  57. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  58. package/dist/types/sdk/host/host.d.ts +1 -1
  59. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  60. package/dist/types/sdk/index.d.ts +2 -0
  61. package/dist/types/sdk/prompt-status.d.ts +88 -0
  62. package/dist/types/sdk/session-directory.d.ts +1 -1
  63. package/dist/types/sdk/startup-capability.d.ts +3 -0
  64. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  65. package/dist/types/sdk/transport/index.d.ts +12 -0
  66. package/dist/types/sdk/transport/relay.d.ts +43 -0
  67. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  68. package/dist/types/sdk/transport/socket.d.ts +7 -0
  69. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  70. package/dist/types/session/agent-session.d.ts +30 -5
  71. package/dist/types/session/btw-contract.d.ts +16 -0
  72. package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
  73. package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
  74. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  75. package/dist/types/session/session-manager.d.ts +25 -4
  76. package/dist/types/session/session-storage.d.ts +18 -0
  77. package/dist/types/setup/credential-import.d.ts +5 -1
  78. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  79. package/dist/types/ssh/utils.d.ts +1 -0
  80. package/dist/types/task/discovery.d.ts +3 -1
  81. package/dist/types/task/index.d.ts +3 -1
  82. package/dist/types/task/receipt.d.ts +2 -0
  83. package/dist/types/task/types.d.ts +94 -0
  84. package/dist/types/tools/ask.d.ts +454 -174
  85. package/dist/types/tools/fetch.d.ts +5 -8
  86. package/dist/types/tools/image-gen.d.ts +63 -1
  87. package/dist/types/tools/index.d.ts +4 -0
  88. package/dist/types/tools/skill-discovery.d.ts +7 -0
  89. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  90. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  91. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  92. package/dist/types/web/insane/bridge.d.ts +1 -0
  93. package/dist/types/web/insane/url-guard.d.ts +17 -0
  94. package/package.json +8 -8
  95. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  96. package/scripts/dogfood-repository-binding.ts +209 -0
  97. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  98. package/scripts/run-test-manifest.ts +4 -4
  99. package/src/capability/index.ts +50 -42
  100. package/src/capability/types.ts +3 -0
  101. package/src/cli/args.ts +2 -92
  102. package/src/cli/notify-cli.ts +5 -38
  103. package/src/cli/setup-cli.ts +10 -4
  104. package/src/cli.ts +52 -9
  105. package/src/commands/daemon.ts +22 -5
  106. package/src/commands/sdk.ts +44 -11
  107. package/src/config/keybindings.ts +158 -25
  108. package/src/config/model-profile-activation.ts +7 -18
  109. package/src/config/model-profile-contract.ts +161 -0
  110. package/src/config/model-profiles.ts +18 -0
  111. package/src/config/model-registry.ts +11 -1
  112. package/src/config/settings-schema.ts +73 -7
  113. package/src/coordinator-mcp/model-preset.ts +21 -74
  114. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  115. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  116. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  117. package/src/discovery/ssh.ts +12 -1
  118. package/src/edit/index.ts +3 -6
  119. package/src/edit/modes/patch.ts +38 -1
  120. package/src/edit/modes/replace.ts +42 -0
  121. package/src/edit/path-mutation-lock.ts +112 -0
  122. package/src/eval/py/executor.ts +30 -11
  123. package/src/eval/py/kernel.ts +1 -21
  124. package/src/eval/py/prelude.py +2 -2
  125. package/src/eval/py/runner-artifact.ts +84 -0
  126. package/src/eval/py/tool-bridge.ts +34 -18
  127. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  128. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  129. package/src/gjc-runtime/launch-tmux.ts +1 -1
  130. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  131. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  132. package/src/gjc-runtime/repository-binding.ts +267 -0
  133. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  134. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  135. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  136. package/src/gjc-runtime/ultragoal-guard.ts +93 -15
  137. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  138. package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
  139. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  140. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  141. package/src/internal-urls/docs-index.generated.ts +12 -10
  142. package/src/internal-urls/local-protocol.ts +64 -21
  143. package/src/internal-urls/mcp-protocol.ts +73 -11
  144. package/src/main.ts +111 -37
  145. package/src/modes/DESIGN.md +5 -0
  146. package/src/modes/acp/acp-agent.ts +50 -16
  147. package/src/modes/action-registry.ts +1 -1
  148. package/src/modes/components/btw-panel.ts +82 -16
  149. package/src/modes/components/model-selector.ts +73 -11
  150. package/src/modes/components/plan-preview-overlay.ts +1 -1
  151. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  152. package/src/modes/components/tool-status-header.ts +25 -4
  153. package/src/modes/components/welcome.ts +22 -18
  154. package/src/modes/controllers/btw-controller.ts +119 -36
  155. package/src/modes/controllers/command-controller.ts +97 -54
  156. package/src/modes/controllers/event-controller.ts +10 -0
  157. package/src/modes/controllers/input-controller.ts +38 -2
  158. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  159. package/src/modes/controllers/selector-controller.ts +149 -63
  160. package/src/modes/interactive-mode.ts +57 -34
  161. package/src/modes/prompt-action-autocomplete.ts +32 -13
  162. package/src/modes/prompt-suggestion-controller.ts +96 -0
  163. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  164. package/src/modes/types.ts +4 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  166. package/src/modes/utils/ui-helpers.ts +5 -3
  167. package/src/prompts/agents/architect.md +0 -1
  168. package/src/prompts/agents/critic.md +0 -1
  169. package/src/prompts/system/btw-user.md +3 -8
  170. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  171. package/src/prompts/system/system-prompt.md +6 -4
  172. package/src/prompts/tools/cron.md +3 -1
  173. package/src/prompts/tools/skill-discovery.md +1 -0
  174. package/src/runtime-mcp/client.ts +41 -55
  175. package/src/runtime-mcp/content-limits.ts +71 -0
  176. package/src/runtime-mcp/json-rpc.ts +23 -9
  177. package/src/runtime-mcp/manager.ts +51 -53
  178. package/src/runtime-mcp/oauth-flow.ts +0 -72
  179. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  180. package/src/runtime-mcp/transports/http.ts +63 -22
  181. package/src/sdk/broker/broker.ts +34 -20
  182. package/src/sdk/broker/lifecycle.ts +185 -36
  183. package/src/sdk/bus/chat-daemon-control.ts +19 -5
  184. package/src/sdk/bus/config-commands.ts +2 -2
  185. package/src/sdk/bus/config.ts +9 -1
  186. package/src/sdk/bus/conversation-store.ts +40 -6
  187. package/src/sdk/bus/index.ts +244 -70
  188. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  189. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  190. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  191. package/src/sdk/bus/notification-orchestration.ts +12 -0
  192. package/src/sdk/bus/notification-service.ts +68 -20
  193. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  194. package/src/sdk/bus/recent-activity.ts +90 -26
  195. package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
  196. package/src/sdk/bus/telegram-daemon-control.ts +147 -35
  197. package/src/sdk/bus/telegram-daemon.ts +2463 -450
  198. package/src/sdk/bus/telegram-reference.ts +20 -2
  199. package/src/sdk/bus/topic-registry.ts +451 -12
  200. package/src/sdk/cli/session-cli.ts +15 -2
  201. package/src/sdk/client/discovery.ts +19 -3
  202. package/src/sdk/client/liveness.ts +49 -0
  203. package/src/sdk/host/control/dispatch.ts +1 -1
  204. package/src/sdk/host/control/operations.ts +1 -1
  205. package/src/sdk/host/host.ts +2 -6
  206. package/src/sdk/host/query/handlers.ts +70 -5
  207. package/src/sdk/index.ts +12 -0
  208. package/src/sdk/prompt-status.ts +97 -0
  209. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  210. package/src/sdk/protocol/operation-registry.ts +18 -4
  211. package/src/sdk/session-directory.ts +5 -1
  212. package/src/sdk/session.ts +31 -5
  213. package/src/sdk/startup-capability.ts +9 -1
  214. package/src/sdk/transport/auth-preface.ts +82 -0
  215. package/src/sdk/transport/index.ts +18 -0
  216. package/src/sdk/transport/relay.ts +252 -0
  217. package/src/sdk/transport/serve-cli.ts +105 -0
  218. package/src/sdk/transport/socket.ts +197 -0
  219. package/src/sdk/transport/stdio.ts +17 -0
  220. package/src/session/agent-session.ts +292 -154
  221. package/src/session/blob-store.ts +25 -7
  222. package/src/session/btw-contract.ts +59 -0
  223. package/src/session/internal/managed-session-scope.ts +1163 -137
  224. package/src/session/internal/managed-session-storage.ts +397 -234
  225. package/src/session/internal/native-publish-outcome.ts +266 -0
  226. package/src/session/session-manager.ts +620 -403
  227. package/src/session/session-storage.ts +128 -11
  228. package/src/setup/credential-import.ts +28 -3
  229. package/src/setup/provider-onboarding.ts +30 -1
  230. package/src/setup/provider-presets.json +2 -1
  231. package/src/slash-commands/builtin-registry.ts +1 -1
  232. package/src/ssh/utils.ts +14 -0
  233. package/src/task/discovery.ts +8 -2
  234. package/src/task/gjc-command.ts +2 -2
  235. package/src/task/index.ts +121 -15
  236. package/src/task/receipt.ts +3 -0
  237. package/src/task/types.ts +34 -3
  238. package/src/tools/ask.ts +199 -89
  239. package/src/tools/ast-edit.ts +2 -2
  240. package/src/tools/bash-pty-selection.ts +2 -2
  241. package/src/tools/browser/screenshot-format.ts +6 -1
  242. package/src/tools/cron.ts +1 -1
  243. package/src/tools/fetch.ts +13 -131
  244. package/src/tools/image-gen.ts +476 -23
  245. package/src/tools/index.ts +4 -0
  246. package/src/tools/read.ts +2 -0
  247. package/src/tools/render-utils.ts +7 -1
  248. package/src/tools/skill-discovery.ts +39 -3
  249. package/src/tools/sqlite-reader.ts +63 -5
  250. package/src/utils/edit-mode.ts +2 -2
  251. package/src/utils/pasted-image-path.ts +15 -6
  252. package/src/utils/prompt-suggestion.ts +265 -0
  253. package/src/utils/shell-snapshot.ts +173 -63
  254. package/src/utils/sixel.ts +3 -3
  255. package/src/web/insane/bridge.ts +5 -1
  256. package/src/web/insane/url-guard.ts +140 -27
  257. package/src/web/scrapers/docs-rs.ts +30 -5
  258. package/src/web/scrapers/types.ts +19 -49
  259. package/src/web/scrapers/utils.ts +12 -26
@@ -0,0 +1,161 @@
1
+ import { formatModelProfileDisplayLabel, type ModelProfileDefinition } from "./model-profiles";
2
+
3
+ export const MODEL_PROFILE_DISCOVERY_QUERY = "models.profiles.list";
4
+ export const MODEL_PROFILE_ERROR_DETAIL_MAX_BYTES = 2048;
5
+ const REQUESTED_PROFILE_MAX_BYTES = 256;
6
+
7
+ const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([["codex-standard", "codex-medium"]]);
8
+
9
+ export interface ModelProfileCatalogItem {
10
+ id: string;
11
+ displayName: string;
12
+ source: "builtin" | "configured";
13
+ }
14
+
15
+ export interface UnknownModelProfileDetails {
16
+ requestedProfile: string;
17
+ availableProfiles: string[];
18
+ discoveryQuery: typeof MODEL_PROFILE_DISCOVERY_QUERY;
19
+ }
20
+
21
+ export interface ModelProfileRegistryErrorDetails {
22
+ requestedProfile?: string;
23
+ availableProfiles: [];
24
+ discoveryQuery: typeof MODEL_PROFILE_DISCOVERY_QUERY;
25
+ }
26
+
27
+ export type ModelProfileErrorDetails = UnknownModelProfileDetails | ModelProfileRegistryErrorDetails;
28
+ export type ModelProfileErrorCode = "unknown_model_profile" | "model_profile_registry_error";
29
+
30
+ function truncateUtf8(value: string, maxBytes: number): string {
31
+ if (Buffer.byteLength(value) <= maxBytes) return value;
32
+ let end = value.length;
33
+ while (end > 0 && Buffer.byteLength(value.slice(0, end)) > maxBytes) end--;
34
+ return value.slice(0, end);
35
+ }
36
+
37
+ function diagnosticProfileEcho(value: string): string {
38
+ return truncateUtf8(value.normalize("NFKC").replace(/[\p{Cc}\p{Cf}]+/gu, " "), REQUESTED_PROFILE_MAX_BYTES);
39
+ }
40
+
41
+ function boundedAvailableProfiles(requestedProfile: string, profiles: ReadonlyMap<string, unknown>): string[] {
42
+ const availableProfiles: string[] = [];
43
+ for (const id of [...new Set(profiles.keys())].sort((left, right) => left.localeCompare(right))) {
44
+ const candidate: UnknownModelProfileDetails = {
45
+ requestedProfile,
46
+ availableProfiles: [...availableProfiles, id],
47
+ discoveryQuery: MODEL_PROFILE_DISCOVERY_QUERY,
48
+ };
49
+ if (Buffer.byteLength(JSON.stringify(candidate)) > MODEL_PROFILE_ERROR_DETAIL_MAX_BYTES) continue;
50
+ availableProfiles.push(id);
51
+ }
52
+ return availableProfiles;
53
+ }
54
+
55
+ export class UnknownModelProfileError extends Error {
56
+ readonly code = "unknown_model_profile" as const;
57
+ readonly details: UnknownModelProfileDetails;
58
+
59
+ constructor(requestedProfile: string, profiles: ReadonlyMap<string, unknown>) {
60
+ const echoed = diagnosticProfileEcho(requestedProfile);
61
+ const availableProfiles = boundedAvailableProfiles(echoed, profiles);
62
+ const available = availableProfiles.length > 0 ? availableProfiles.join(", ") : "none";
63
+ super(
64
+ truncateUtf8(
65
+ `Unknown model profile ${JSON.stringify(echoed)}. Available profiles: ${available}. Query ${MODEL_PROFILE_DISCOVERY_QUERY} for the complete catalog.`,
66
+ 512,
67
+ ),
68
+ );
69
+ this.name = "UnknownModelProfileError";
70
+ this.details = {
71
+ requestedProfile: echoed,
72
+ availableProfiles,
73
+ discoveryQuery: MODEL_PROFILE_DISCOVERY_QUERY,
74
+ };
75
+ }
76
+ }
77
+
78
+ export class ModelProfileRegistryError extends Error {
79
+ readonly code = "model_profile_registry_error" as const;
80
+ readonly details: ModelProfileRegistryErrorDetails;
81
+
82
+ constructor(requestedProfile?: string) {
83
+ super(
84
+ `The model profile registry is unavailable. Query ${MODEL_PROFILE_DISCOVERY_QUERY} after fixing models.yml.`,
85
+ );
86
+ this.name = "ModelProfileRegistryError";
87
+ const echoed = requestedProfile === undefined ? undefined : diagnosticProfileEcho(requestedProfile);
88
+ this.details = {
89
+ ...(echoed ? { requestedProfile: echoed } : {}),
90
+ availableProfiles: [],
91
+ discoveryQuery: MODEL_PROFILE_DISCOVERY_QUERY,
92
+ };
93
+ }
94
+ }
95
+
96
+ export function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
97
+ if (profiles.has(profileName)) return profileName;
98
+ const replacement = LEGACY_MODEL_PROFILE_ALIASES.get(profileName);
99
+ return replacement && profiles.has(replacement) ? replacement : profileName;
100
+ }
101
+
102
+ export function validateModelProfileName(
103
+ profileName: string,
104
+ profiles: ReadonlyMap<string, unknown>,
105
+ registryError?: unknown,
106
+ ): string {
107
+ if (registryError !== undefined) throw new ModelProfileRegistryError(profileName);
108
+ const resolved = resolveModelProfileName(profileName, profiles);
109
+ if (!profiles.has(resolved)) throw new UnknownModelProfileError(profileName, profiles);
110
+ return resolved;
111
+ }
112
+
113
+ export function projectModelProfileCatalog(
114
+ profiles: ReadonlyMap<string, ModelProfileDefinition>,
115
+ registryError?: unknown,
116
+ ): ModelProfileCatalogItem[] {
117
+ if (registryError !== undefined) throw new ModelProfileRegistryError();
118
+ return [...profiles.entries()]
119
+ .map(([id, definition]) => ({
120
+ id,
121
+ displayName: formatModelProfileDisplayLabel(definition),
122
+ source: definition.source === "user" ? ("configured" as const) : ("builtin" as const),
123
+ }))
124
+ .sort((left, right) => left.id.localeCompare(right.id));
125
+ }
126
+
127
+ export function isModelProfileError(value: unknown): value is {
128
+ code: ModelProfileErrorCode;
129
+ message: string;
130
+ details: ModelProfileErrorDetails;
131
+ } {
132
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
133
+ const error = value as Record<string, unknown>;
134
+ if (
135
+ (error.code !== "unknown_model_profile" && error.code !== "model_profile_registry_error") ||
136
+ typeof error.message !== "string" ||
137
+ !error.details ||
138
+ typeof error.details !== "object" ||
139
+ Array.isArray(error.details)
140
+ )
141
+ return false;
142
+ const details = error.details as Record<string, unknown>;
143
+ const detailKeys = Object.keys(details);
144
+ if (
145
+ !detailKeys.every(key => key === "requestedProfile" || key === "availableProfiles" || key === "discoveryQuery") ||
146
+ details.discoveryQuery !== MODEL_PROFILE_DISCOVERY_QUERY ||
147
+ !Array.isArray(details.availableProfiles) ||
148
+ !details.availableProfiles.every(id => typeof id === "string") ||
149
+ Buffer.byteLength(JSON.stringify(details)) > MODEL_PROFILE_ERROR_DETAIL_MAX_BYTES ||
150
+ (typeof details.requestedProfile === "string" &&
151
+ Buffer.byteLength(details.requestedProfile) > REQUESTED_PROFILE_MAX_BYTES)
152
+ )
153
+ return false;
154
+ if (error.code === "unknown_model_profile")
155
+ return detailKeys.length === 3 && typeof details.requestedProfile === "string";
156
+ return (
157
+ detailKeys.length === (details.requestedProfile === undefined ? 2 : 3) &&
158
+ details.availableProfiles.length === 0 &&
159
+ (details.requestedProfile === undefined || typeof details.requestedProfile === "string")
160
+ );
161
+ }
@@ -257,6 +257,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
257
257
  critic: "minimax-code/minimax-m3:xhigh",
258
258
  architect: "minimax-code/minimax-m3:xhigh",
259
259
  }),
260
+ profile("alibaba-token-plan-balanced", ["alibaba-token-plan"], {
261
+ default: "alibaba-token-plan/qwen3.8-max-preview:medium",
262
+ executor: "alibaba-token-plan/deepseek-v4-pro:xhigh",
263
+ planner: "alibaba-token-plan/glm-5.2:high",
264
+ architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
265
+ critic: "alibaba-token-plan/glm-5.2:high",
266
+ }),
267
+ profile("alibaba-token-plan-qwenmaxxing", ["alibaba-token-plan"], {
268
+ default: "alibaba-token-plan/qwen3.8-max-preview:medium",
269
+ executor: "alibaba-token-plan/qwen3.8-max-preview:low",
270
+ planner: "alibaba-token-plan/qwen3.8-max-preview:medium",
271
+ architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
272
+ critic: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
273
+ }),
260
274
  profile("opus-codex", ["anthropic", "openai-codex"], {
261
275
  default: "anthropic/claude-opus-4-8:xhigh",
262
276
  executor: "openai-codex/gpt-5.6-terra:low",
@@ -315,6 +329,8 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
315
329
  "minimax-eco": { displayName: "MiniMax Eco", providerGroup: "MINIMAX" },
316
330
  "minimax-medium": { displayName: "MiniMax Medium", providerGroup: "MINIMAX" },
317
331
  "minimax-pro": { displayName: "MiniMax Pro", providerGroup: "MINIMAX" },
332
+ "alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
333
+ "alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
318
334
  "opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
319
335
  "codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
320
336
  "fable-opus-codex": { displayName: "Fable + Opus + Codex", providerGroup: "COMBOS" },
@@ -330,6 +346,7 @@ const PROFILE_GROUP_ORDER = [
330
346
  "GROK",
331
347
  "CURSOR",
332
348
  "MINIMAX",
349
+ "ALIBABA TOKEN PLAN",
333
350
  "COMBOS",
334
351
  ];
335
352
 
@@ -347,6 +364,7 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
347
364
  "grok-build": "grok-build-pro",
348
365
  cursor: "cursor-medium",
349
366
  "minimax-code": "minimax-medium",
367
+ "alibaba-token-plan": "alibaba-token-plan-balanced",
350
368
  };
351
369
 
352
370
  export function getModelProfilePresentation(
@@ -2049,7 +2049,10 @@ export class ModelRegistry {
2049
2049
  const modelsUrl = `${baseUrl}/models`;
2050
2050
 
2051
2051
  const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
2052
- const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
2052
+ // Resolve with the same baseUrl context completion requests use so an
2053
+ // endpoint-scoped (or config-pinned) credential wins here exactly as it
2054
+ // does for chat completions.
2055
+ const apiKey = await this.authStorage.getApiKey(providerConfig.provider, undefined, { baseUrl });
2053
2056
  if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
2054
2057
  headers.Authorization = `Bearer ${apiKey}`;
2055
2058
  }
@@ -2059,6 +2062,13 @@ export class ModelRegistry {
2059
2062
  signal: AbortSignal.timeout(250),
2060
2063
  });
2061
2064
  if (!response.ok) {
2065
+ if (response.status === 401 || response.status === 403) {
2066
+ // Redacted by construction: name the provider, endpoint, and the
2067
+ // config surface to fix — never the resolved key.
2068
+ throw new Error(
2069
+ `HTTP ${response.status} from ${modelsUrl}: provider "${providerConfig.provider}" credential was rejected for OpenAI models-list discovery; check providers.${providerConfig.provider}.apiKey/apiKeyEnv.`,
2070
+ );
2071
+ }
2062
2072
  throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
2063
2073
  }
2064
2074
  const payload = (await response.json()) as { data?: Array<{ id: string }> };
@@ -819,6 +819,15 @@ export const SETTINGS_SCHEMA = {
819
819
  type: "boolean",
820
820
  default: true,
821
821
  },
822
+ "statusLine.showActionHints": {
823
+ type: "boolean",
824
+ default: true,
825
+ ui: {
826
+ tab: "appearance",
827
+ label: "Status Line Action Hints",
828
+ description: "Show contextual keyboard shortcuts in the status line",
829
+ },
830
+ },
822
831
 
823
832
  "statusLine.leftSegments": { type: "array", default: [] as StatusLineSegmentId[] },
824
833
 
@@ -1334,6 +1343,16 @@ export const SETTINGS_SCHEMA = {
1334
1343
  description: "Suggest emojis from `:name:` shortcodes and expand text emoticons like `:D` or `:-)`",
1335
1344
  },
1336
1345
  },
1346
+ promptSuggestions: {
1347
+ type: "boolean",
1348
+ default: false,
1349
+ ui: {
1350
+ tab: "interaction",
1351
+ label: "Prompt Suggestions",
1352
+ description:
1353
+ "Predict your likely next prompt after each turn (smol-model call) and show it as ghost text; Tab accepts",
1354
+ },
1355
+ },
1337
1356
 
1338
1357
  "startup.quiet": {
1339
1358
  type: "boolean",
@@ -2451,9 +2470,9 @@ export const SETTINGS_SCHEMA = {
2451
2470
  default: false,
2452
2471
  ui: {
2453
2472
  tab: "tools",
2454
- label: "Insane Search Fallback",
2473
+ label: "Insane Search Fallback (Compatibility)",
2455
2474
  description:
2456
- "Opt in to the vendored insane-search escalation for blocked public URL reads (403/WAF/JS-gated). Off by default. Requires preinstalled python3 + curl_cffi (and node + playwright/stealth for the browser phase); changes network posture by enabling TLS/browser impersonation for public pages.",
2475
+ "Compatibility-only preference. Remote renderer fallback stays disabled because it cannot preserve validated per-hop network routing.",
2457
2476
  },
2458
2477
  },
2459
2478
 
@@ -3290,25 +3309,71 @@ export const SETTINGS_SCHEMA = {
3290
3309
  },
3291
3310
  "providers.image": {
3292
3311
  type: "enum",
3293
- values: ["auto", "openai", "gemini", "openrouter", "antigravity"] as const,
3312
+ values: ["auto", "openai", "gemini", "openrouter", "antigravity", "alibaba", "custom"] as const,
3294
3313
  default: "auto",
3295
3314
  ui: {
3296
3315
  tab: "providers",
3297
- label: "Image Provider",
3298
- description: "Provider for image generation tool",
3316
+ label: "Image Generation",
3317
+ description: "Provider and model for image generation tool",
3299
3318
  options: [
3300
3319
  {
3301
3320
  value: "auto",
3302
3321
  label: "Auto",
3303
- description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
3322
+ description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini > Alibaba",
3304
3323
  },
3305
- { value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
3324
+ { value: "openai", label: "OpenAI", description: "Uses gpt-image-2 via OpenAI Responses/Codex" },
3306
3325
  { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
3307
3326
  { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
3308
3327
  { value: "antigravity", label: "Antigravity", description: "Requires login with google-antigravity" },
3328
+ {
3329
+ value: "alibaba",
3330
+ label: "Alibaba Bailian",
3331
+ description: "Requires ALIBABA_TOKEN_PLAN_API_KEY (wan2.7-image via Token Plan)",
3332
+ },
3333
+ {
3334
+ value: "custom",
3335
+ label: "Custom",
3336
+ description: "OpenAI-compatible endpoint (set providers.imageCustomUrl)",
3337
+ },
3309
3338
  ],
3310
3339
  },
3311
3340
  },
3341
+ "providers.imageModel": {
3342
+ type: "string",
3343
+ default: undefined,
3344
+ ui: {
3345
+ tab: "providers",
3346
+ label: "Image Model",
3347
+ description: "Override the default image generation model for the selected provider",
3348
+ },
3349
+ },
3350
+ "providers.imageCustomUrl": {
3351
+ type: "string",
3352
+ default: undefined,
3353
+ ui: {
3354
+ tab: "providers",
3355
+ label: "Image Custom URL",
3356
+ description: "Base URL for custom OpenAI-compatible image endpoint",
3357
+ },
3358
+ },
3359
+ "providers.imageCustomKey": {
3360
+ type: "string",
3361
+ default: undefined,
3362
+ ui: {
3363
+ tab: "providers",
3364
+ label: "Image Custom API Key",
3365
+ description: "API key for custom OpenAI-compatible image endpoint",
3366
+ },
3367
+ },
3368
+ "providers.imageCustomKeyEnv": {
3369
+ type: "string",
3370
+ default: undefined,
3371
+ ui: {
3372
+ tab: "providers",
3373
+ label: "Image Custom API Key Env",
3374
+ description: "Environment variable name holding the API key for custom image endpoint",
3375
+ },
3376
+ },
3312
3377
 
3313
3378
  "providers.kimiApiFormat": {
3314
3379
  type: "enum",
@@ -3740,6 +3805,7 @@ export interface StatusLineSettings {
3740
3805
  maxRows: number;
3741
3806
  showHookStatus: boolean;
3742
3807
  showSkillHud: boolean;
3808
+ showActionHints: boolean;
3743
3809
  leftSegments: StatusLineSegmentId[];
3744
3810
  rightSegments: StatusLineSegmentId[];
3745
3811
  segmentOptions: Record<string, unknown>;
@@ -1,44 +1,10 @@
1
1
  import * as path from "node:path";
2
2
  import { getAgentDir } from "@gajae-code/utils";
3
3
  import { YAML } from "bun";
4
+ import { UnknownModelProfileError, validateModelProfileName } from "../config/model-profile-contract";
5
+ import { mergeModelProfiles } from "../config/model-profiles";
4
6
  import { ModelsConfigSchema } from "../config/models-config-schema";
5
7
 
6
- /**
7
- * The coordinator runs in the shipped MCP process and must stay outside the
8
- * session host import graph. Keep this identity catalog in the coordinator
9
- * boundary; profile activation and model registry code belong to that host.
10
- */
11
- const BUILTIN_MODEL_PROFILE_NAMES = [
12
- "codex-eco",
13
- "codex-medium",
14
- "codex-pro",
15
- "opencodego",
16
- "claude-opus",
17
- "claude-fable",
18
- "glm-eco",
19
- "glm-medium",
20
- "glm-pro",
21
- "kimi-coding-plan-eco",
22
- "kimi-coding-plan-medium",
23
- "kimi-coding-plan-pro",
24
- "mimo-eco",
25
- "mimo-medium",
26
- "mimo-pro",
27
- "grok-eco",
28
- "grok-medium",
29
- "grok-pro",
30
- "grok-build-pro",
31
- "cursor-eco",
32
- "cursor-medium",
33
- "cursor-pro",
34
- "minimax-eco",
35
- "minimax-medium",
36
- "minimax-pro",
37
- "opus-codex",
38
- "codex-opencodego",
39
- "fable-opus-codex",
40
- ] as const;
41
-
42
8
  export interface CoordinatorModelProfile {
43
9
  name: string;
44
10
  }
@@ -53,8 +19,6 @@ export type CoordinatorModelProfileLoader = () =>
53
19
  | Promise<Map<string, CoordinatorModelProfile>>;
54
20
 
55
21
  const MAX_ECHOED_MPRESET_LENGTH = 128;
56
- const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([["codex-standard", "codex-medium"]]);
57
-
58
22
  /**
59
23
  * Thrown by the default loader when `models.yml` exists but is invalid or
60
24
  * unreadable. This lets the resolver fail closed with a distinct, stable reason
@@ -69,35 +33,25 @@ export class CoordinatorModelProfileRegistryError extends Error {
69
33
  }
70
34
  }
71
35
 
72
- function builtInCoordinatorModelProfiles(): Map<string, CoordinatorModelProfile> {
73
- return new Map(BUILTIN_MODEL_PROFILE_NAMES.map(name => [name, { name }]));
36
+ function coordinatorModelProfiles(
37
+ profiles?: Parameters<typeof mergeModelProfiles>[0],
38
+ ): Map<string, CoordinatorModelProfile> {
39
+ return new Map([...mergeModelProfiles(profiles).keys()].map(name => [name, { name }]));
74
40
  }
75
41
 
76
42
  export const loadCoordinatorModelProfiles: CoordinatorModelProfileLoader = async () => {
77
43
  const modelsFile = Bun.file(path.join(getAgentDir(), "models.yml"));
78
- if (!(await modelsFile.exists())) return builtInCoordinatorModelProfiles();
44
+ if (!(await modelsFile.exists())) return coordinatorModelProfiles();
79
45
  try {
80
46
  const parsed = YAML.parse(await modelsFile.text());
81
47
  const config = ModelsConfigSchema.safeParse(parsed);
82
48
  if (!config.success) throw config.error;
83
- const profiles = builtInCoordinatorModelProfiles();
84
- for (const name of Object.keys(config.data.profiles ?? {})) profiles.set(name, { name });
85
- return profiles;
49
+ return coordinatorModelProfiles(config.data.profiles);
86
50
  } catch (error) {
87
51
  throw new CoordinatorModelProfileRegistryError(error);
88
52
  }
89
53
  };
90
54
 
91
- function sortedProfileNames(profiles: ReadonlyMap<string, CoordinatorModelProfile>): string[] {
92
- return [...profiles.keys()].sort((left, right) => left.localeCompare(right));
93
- }
94
-
95
- function resolveCoordinatorModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
96
- if (profiles.has(profileName)) return profileName;
97
- const replacement = LEGACY_MODEL_PROFILE_ALIASES.get(profileName);
98
- return replacement && profiles.has(replacement) ? replacement : profileName;
99
- }
100
-
101
55
  export type CoordinatorMpresetResolution =
102
56
  | { ok: true; mpreset: string | null }
103
57
  | { ok: false; reason: "unknown_model_profile"; mpreset: string; available_profiles: string[] }
@@ -120,7 +74,7 @@ export async function resolveCoordinatorMpreset(
120
74
  loadProfiles: CoordinatorModelProfileLoader,
121
75
  ): Promise<CoordinatorMpresetResolution> {
122
76
  if (raw === undefined || raw === null) return { ok: true, mpreset: null };
123
- const requested = typeof raw === "string" ? raw.trim() : "";
77
+ const requested = typeof raw === "string" ? raw : "";
124
78
  const echoed = requested.slice(0, MAX_ECHOED_MPRESET_LENGTH);
125
79
  let profiles: Map<string, CoordinatorModelProfile>;
126
80
  try {
@@ -131,24 +85,17 @@ export async function resolveCoordinatorMpreset(
131
85
  }
132
86
  throw error;
133
87
  }
134
- // Non-string input and explicit blank/whitespace strings can never name a
135
- // profile; only absent/null (handled above) means "no selection".
136
- if (typeof raw !== "string" || requested.length === 0) {
137
- return {
138
- ok: false,
139
- reason: "unknown_model_profile",
140
- mpreset: echoed,
141
- available_profiles: sortedProfileNames(profiles),
142
- };
143
- }
144
- const canonical = resolveCoordinatorModelProfileName(requested, profiles);
145
- if (!profiles.has(canonical)) {
146
- return {
147
- ok: false,
148
- reason: "unknown_model_profile",
149
- mpreset: echoed,
150
- available_profiles: sortedProfileNames(profiles),
151
- };
88
+ try {
89
+ const canonical = validateModelProfileName(requested, profiles);
90
+ return { ok: true, mpreset: canonical };
91
+ } catch (error) {
92
+ if (error instanceof UnknownModelProfileError)
93
+ return {
94
+ ok: false,
95
+ reason: "unknown_model_profile",
96
+ mpreset: error.details.requestedProfile.slice(0, MAX_ECHOED_MPRESET_LENGTH),
97
+ available_profiles: error.details.availableProfiles,
98
+ };
99
+ throw error;
152
100
  }
153
- return { ok: true, mpreset: canonical };
154
101
  }
@@ -382,7 +382,7 @@ Auto-answer has a clarity cap: unless the architect confidence is `high` and unc
382
382
 
383
383
  ### Step 2b″: Refine Free-Text Answers
384
384
 
385
- When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections) — then confirm with exactly one `ask` that nothing is lost or misrepresented. Apply `language.instruction` when present.
385
+ When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections). Then confirm with exactly one `ask` that nothing is lost or misrepresented: the `ask` question body MUST render the full structured interpretation — every non-empty canonical section, verbatim — before the confirmation prompt. The user is approving that specific interpretation, so it must be visible inside the question body; never ask "does this capture it?" / "이 해석이 맞아?" without first displaying the interpretation itself. A confirmation `ask` whose body omits the interpretation it is asking about is a hard error: re-issue it with the interpretation shown. Apply `language.instruction` when present.
386
386
 
387
387
  Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
388
388
 
@@ -601,7 +601,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
601
601
 
602
602
  **4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, no disputed established fact lacks a `superseded_by` resolution, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
603
603
 
604
- **4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask`: "If someone read only this line, would they reach the same outcome you have in mind?" Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. Because this gate has options, it MUST go through `ask`: do not print the Restate question and options as assistant prose with `Question:`/`Options:` labels. If the Restate gate was already printed that way, immediately call `ask` with the same question/options before accepting or waiting for any answer. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
604
+ **4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask` whose body MUST begin by stating that one-sentence goal verbatim, followed by: "If someone read only this line, would they reach the same outcome you have in mind?" The goal line must be visible inside the `ask` body; never ask the confirmation without first displaying the collapsed goal it refers to. Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. Because this gate has options, it MUST go through `ask`: do not print the Restate question and options as assistant prose with `Question:`/`Options:` labels. If the Restate gate was already printed that way, immediately call `ask` with the same question/options before accepting or waiting for any answer. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
605
605
 
606
606
  1. **Generate the specification** using opus model with the prompt-safe transcript. If the full interview transcript or initial context is too large, include the summary plus all concrete decisions, acceptance criteria, unresolved gaps, and ontology snapshots; never overflow the prompt with raw oversized context.
607
607
  - Apply `language.instruction` when present so user-facing prose in the spec preserves the session language; keep code identifiers, file paths, commands, JSON/settings keys, and quoted source text unchanged.
@@ -34,6 +34,8 @@ For corrupt, tampered, unreadable, or stale current-session ralplan state, run `
34
34
 
35
35
  Ralplan is planning only. It may inspect context and draft plan/spec/proposal artifacts, but those remain `pending approval` until explicit current-turn or structured-UI execution approval. Before that approval, do not mutate product source, run mutation-oriented shell, commit, push, open PRs, invoke execution skills, or delegate implementation.
36
36
 
37
+ Explicitly naming `ultragoal` or `team` (including `/skill:` and `gjc` forms) counts as opting into execution for that skill — do not re-ask for the same consent.
38
+
37
39
  Persist planning artifacts and handoffs through the ralplan CLI writer, never direct `.gjc/` edits:
38
40
  Direct `write`, `edit`, or `ast_edit` calls against `.gjc/_session-{sessionid}/specs`, `.gjc/_session-{sessionid}/plans`, `.gjc/_session-{sessionid}/state`, or any other `.gjc/` path are forbidden unless an explicit force override is active.
39
41
 
@@ -82,7 +84,7 @@ The consensus workflow:
82
84
  - For every confirmed open item, embed the resolved outcome into the final plan under an **## Intent Reconciliation** section so the `pending approval` artifact records each decision; record any item the user explicitly defers as an open confirmation under that same section.
83
85
  d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus a compact status (reconciled-clean / reconciled-with-revision / open-confirmations-pending) instead of pasting the full body.
84
86
  7. On reconciliation completion, re-check the review join gate (Critic `OKAY` plus Architect `CLEAR`/`APPROVE` for the same Planner artifact/pass), mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
85
- 8. **Always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
87
+ 8. **Final approval gate (with explicit-execution exception):** If the user already explicitly named an execution skill in the current turn or via the structured approval UI (`ultragoal`, `/skill:ultragoal`, `gjc ultragoal`, `team`, `/skill:team`, `gjc team`, or "Approve execution via ultragoal/team"), that is execution approval — skip the re-ask and proceed to step 9 with that skill. Otherwise, **always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
86
88
  - **Refine further** — re-run the consensus loop / request changes, then return here
87
89
  - **Approve execution via ultragoal (Recommended)** — goal-tracked autonomous execution
88
90
  - **Approve execution via team** — only when tmux-based interactive worker parallelization is required
@@ -123,10 +123,12 @@ An active Ultragoal run must not give up on a blocker by pausing the goal and as
123
123
  - **`resolvable`** — anything the agent can act on: failing tests, missing implementation, a dependency to install, an ambiguous-but-inferable detail, investigation. **Never pause.** Exhaust autonomous resolution first: investigate, `gjc ultragoal steer --kind add_subgoal --title "Investigate blocker" --objective "..." --evidence "..." --rationale "..."`, delegate an `executor`, or preserve the blocker durably with `gjc ultragoal checkpoint --status blocked` / `gjc ultragoal record-review-blockers` and keep scheduling the next goal.
124
124
  - **`human_blocked`** — only the user can act: credentials/secrets, a manual or physical step, an external approval/decision, access the agent lacks. Pause is the last resort and is gated.
125
125
 
126
- `goal({"op":"pause"})` is **blocked at runtime** while an Ultragoal run is active unless the latest durable ledger event classifies the current blocker as `human_blocked`. To pause, record the classification immediately before pausing and cite the human-only dependency as evidence:
126
+ `goal({"op":"pause"})` is **blocked at runtime** while an Ultragoal run is active unless the latest `blocker_classified` ledger event is `human_blocked` and a later bound clean pause terminal critic verdict is recorded for it (see [Terminal critic gate](#terminal-critic-gate)). `assertUltragoalPauseAllowed` first consumes a pre-existing give-up nudge (a durable ledger write) before it runs the read-only pause diagnostic; only `isUltragoalPauseBlocked` is a pure reader. To pause, first record the human-only classification and capture its event id, then record the terminal critic's clean bound pause verdict, and only then pause:
127
127
 
128
128
  ```sh
129
129
  gjc ultragoal classify-blocker --classification human_blocked --evidence "<the specific human-only dependency>" [--goal-id <id>]
130
+ gjc ultragoal record-critic-verdict --terminus pause --classification-event-id <eventId> --verdict OKAY --evidence "<terminal critic evidence>"
131
+ goal({"op":"pause"})
130
132
  ```
131
133
 
132
134
  Recording `--classification resolvable` is an audit note only; it never authorizes a pause. The `ask` tool stays blocked during active runs regardless of classification — record unresolved decisions as durable blockers instead of prompting.
@@ -343,6 +345,44 @@ Provide one `artifactRefs` entry per live surface actually exercised, using the
343
345
 
344
346
  For CLI replay artifacts, the JSON at `path` must be an object like `{"schemaVersion":1,"kind":"cli-replay","replaySafe":true,"command":["bun","-e","console.log(\"ultragoal-cli-ok\")"],"cwd":".","env":{"LC_ALL":"C"},"timeoutMs":30000,"expectedExitCode":0,"recordedStdout":"ultragoal-cli-ok\n","recordedStderr":"","invariants":[{"type":"substring","value":"ultragoal-cli-ok"},{"type":"not-substring","value":"error"}]}`. Accepted replay fields are `command` (string array), optional `cwd`, safe `env`, `timeoutMs`, `expectedExitCode`, `recordedStdout`, `recordedStderr`, `normalization`, and `invariants`. The conservative command allowlist is intentionally small: `bun --version`, `node --version`, deterministic `bun/node -e "console.log(...)"`, `npm|pnpm|yarn --version`, `npm|pnpm|yarn list`, read-only `git status|rev-parse|merge-base|diff|show|log` with safe args, and `gjc read|status`. `env` must contain only safe deterministic variables, never credentials or machine/user-specific secrets. `normalization` is optional and, when provided, must be exactly the string `"default"` (the built-in normalizer already strips ANSI codes, normalizes line endings, scrubs paths, and trims trailing whitespace); object-shaped normalization is rejected. Invariants may be substring, regex, or not-substring checks; when present, they replace exact `recordedStdout` equality — without `invariants`, replayed normalized stdout must match `recordedStdout` exactly. Unsafe, non-deterministic, credentialed, interactive, or otherwise unallowlisted commands require audited `replayExempt` metadata with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs` plus a structurally valid same-surface fallback artifact. `reason` must be substantive and audited, and `approvedBy` must identify the verifier. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
345
347
 
348
+ ## Terminal critic gate
349
+
350
+ The terminal critic gate is a fail-closed, once-per-run-terminus review. It guards both terminal exits with a read-only `critic` role agent's `OKAY` verdict; it does not run per story. It is additive to, and does not change, the existing per-story `architect` review and `executor` QA/red-team lanes.
351
+
352
+ ### Completion terminus
353
+
354
+ Before assembling the final-aggregate `--quality-gate-json`, the leader delegates the terminal critic. Only the final-aggregate completion checkpoint requires the additional top-level `criticReview` key; `criticReview` is tolerated but ignored on non-final checkpoints. A clean final aggregate requires `verdict: "OKAY"`, non-empty `evidence`, and an empty `blockers` array:
355
+
356
+ ```json
357
+ {
358
+ "criticReview": {
359
+ "verdict": "OKAY",
360
+ "evidence": "terminal critic review of the final required-goal state",
361
+ "blockers": []
362
+ }
363
+ }
364
+ ```
365
+
366
+ ### Pause/blocked terminus
367
+
368
+ At a `human_blocked` terminus, the leader first runs `gjc ultragoal classify-blocker --classification human_blocked` (capturing that classification's ledger `eventId`), then delegates the terminal critic and records its verdict with `gjc ultragoal record-critic-verdict --terminus pause --classification-event-id <eventId>` before calling `goal({"op":"pause"})`. The pause is allowed only when a later fresh `critic_verdict` ledger receipt exists with `terminus: "pause"`, `verdict: "OKAY"`, non-empty evidence, an empty blockers array, the current `planGeneration`, and a `classificationEventId` bound to the latest `blocker_classified` event, which must be `human_blocked`. Freshness is scoped to the final required-goal state, so required-goal or steer changes stale the receipt, and a newer classification supersedes an older verdict.
369
+
370
+ The critic must verify that the `human_blocked` classification is genuine, including catching false pauses where needed resources exist locally or the asserted blocker is resolvable. A `REJECT` (or `ITERATE`) verdict refuses the terminal pause; the run keeps executing. The pause (`goal({"op":"pause"})`) is the gated terminal park-and-wait exit — a per-goal `gjc ultragoal checkpoint --status blocked` remains available as non-terminal blocker bookkeeping that never signals run completion and keeps the blocker outstanding until resolved.
371
+
372
+ ### Invocation and containment
373
+
374
+ At each terminus, the leader gives the read-only `critic` role agent `brief.md`, `goals.json`, `ledger.jsonl`, and the cumulative change set. For completion, invoke it before assembling the final-aggregate gate JSON. For pause, invoke it after the `human_blocked` classification and before `goal({"op":"pause"})`. The terminal critic must not spawn nested `ralplan`, `team`, `deep-interview`, or `ultragoal` workflows. This creates no interactive surface: `ask` remains blocked while an Ultragoal run is active.
375
+
376
+ ### Non-OKAY loop and ceiling
377
+
378
+ For completion-side `ITERATE` or `REJECT`, the leader MUST first record the terminal verdict so the run-level counter observes it: `gjc ultragoal record-critic-verdict --terminus completion --verdict <ITERATE|REJECT> --evidence "<critic findings>"`; then record the findings with `gjc ultragoal record-review-blockers` and reopen the run. The dedicated counter ceiling is 5, independently of the give-up nudge budget, and is **RUN-LEVEL**: it counts every non-OKAY terminal-critic verdict across the whole run and all reopen cycles. On reaching that ceiling, both pause and final completion are blocked until a human or leader records `gjc ultragoal record-critic-gate-override --evidence "<authorization evidence>"`. There is no automatic pause override.
379
+
380
+ This gate is always fail-closed and has no grandfathering: in-flight runs must obtain a terminal verdict when they reach a terminus.
381
+
382
+ #### Deferred / out of scope
383
+
384
+ Gating active-aggregate `goal drop` after nudge exhaustion is a known follow-up not covered by this gate; `drop` remains governed by the existing nudge discipline.
385
+
346
386
  ## Review mode
347
387
 
348
388
  `gjc ultragoal review` runs the same hardened gate against an already implemented PR, branch, or worktree. Use `--pr <number>` for a PR, `--branch <ref>` for a branch diff, omit both for the current worktree, and pass `--spec <path>` when a real contract exists. `--mode review-only` emits the verdict/findings without creating fix work; `--mode review-start` records review blockers for follow-up. Review mode validates the same `executorQa` shape and live-surface artifacts as `checkpoint --status complete`. A thin or derived-only contract can never clean-pass: the verdict is capped at `inconclusive: weak-contract` until a supplied spec or equivalent strong acceptance criteria are available.
@@ -372,4 +412,5 @@ The skill tool then dispatches `/skill:ralplan` or `/skill:deep-interview` same-
372
412
  - Never call `goal({"op":"complete"})` unless the aggregate run or legacy per-story goal is actually complete.
373
413
  - In aggregate mode, intermediate and final story checkpoints update durable `goals.json` state and append receipt proof to `ledger.jsonl`; the final story checkpoint creates the final aggregate receipt before the agent may call `goal({"op":"complete"})`.
374
414
  - Completion checkpoints require `--quality-gate-json` only. Shell commands and hooks must not mutate goal state; the agent reconciles inline goal-tool state after durable completion.
415
+ - Final-aggregate completion additionally requires a `criticReview` `OKAY`; a `human_blocked` pause additionally requires a fresh `OKAY` `critic_verdict` receipt.
375
416
  - Treat `ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.