@gajae-code/coding-agent 0.11.6 → 0.11.7

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 (131) hide show
  1. package/CHANGELOG.md +37 -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/commands/daemon.d.ts +2 -2
  5. package/dist/types/commands/sdk.d.ts +20 -2
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +56 -6
  8. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
  10. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
  12. package/dist/types/main.d.ts +24 -1
  13. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  14. package/dist/types/modes/components/model-selector.d.ts +2 -0
  15. package/dist/types/modes/components/tool-status-header.d.ts +1 -0
  16. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  21. package/dist/types/sdk/bus/index.d.ts +1 -1
  22. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  23. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  27. package/dist/types/sdk/client/discovery.d.ts +2 -0
  28. package/dist/types/sdk/client/liveness.d.ts +7 -0
  29. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  30. package/dist/types/sdk/transport/index.d.ts +12 -0
  31. package/dist/types/sdk/transport/relay.d.ts +43 -0
  32. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  33. package/dist/types/sdk/transport/socket.d.ts +7 -0
  34. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  35. package/dist/types/session/agent-session.d.ts +28 -5
  36. package/dist/types/session/btw-contract.d.ts +16 -0
  37. package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
  38. package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
  39. package/dist/types/session/session-manager.d.ts +25 -4
  40. package/dist/types/setup/credential-import.d.ts +5 -1
  41. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  42. package/dist/types/ssh/utils.d.ts +1 -0
  43. package/dist/types/task/discovery.d.ts +3 -1
  44. package/dist/types/tools/fetch.d.ts +5 -8
  45. package/dist/types/tools/image-gen.d.ts +16 -0
  46. package/dist/types/tools/skill-discovery.d.ts +7 -0
  47. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  48. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  49. package/dist/types/web/insane/bridge.d.ts +1 -0
  50. package/dist/types/web/insane/url-guard.d.ts +17 -0
  51. package/package.json +8 -8
  52. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  53. package/src/capability/index.ts +50 -42
  54. package/src/capability/types.ts +3 -0
  55. package/src/cli/setup-cli.ts +10 -4
  56. package/src/cli.ts +34 -9
  57. package/src/commands/daemon.ts +22 -5
  58. package/src/commands/sdk.ts +42 -9
  59. package/src/config/keybindings.ts +1 -1
  60. package/src/config/model-profiles.ts +18 -0
  61. package/src/config/settings-schema.ts +57 -6
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  63. package/src/discovery/ssh.ts +12 -1
  64. package/src/eval/py/kernel.ts +1 -21
  65. package/src/eval/py/runner-artifact.ts +84 -0
  66. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  67. package/src/gjc-runtime/ultragoal-guard.ts +88 -10
  68. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  69. package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
  70. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  71. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  72. package/src/internal-urls/docs-index.generated.ts +8 -7
  73. package/src/internal-urls/local-protocol.ts +32 -6
  74. package/src/main.ts +86 -27
  75. package/src/modes/action-registry.ts +1 -1
  76. package/src/modes/components/btw-panel.ts +82 -16
  77. package/src/modes/components/model-selector.ts +31 -3
  78. package/src/modes/components/tool-status-header.ts +6 -1
  79. package/src/modes/controllers/btw-controller.ts +119 -36
  80. package/src/modes/controllers/input-controller.ts +31 -0
  81. package/src/modes/controllers/selector-controller.ts +106 -12
  82. package/src/modes/interactive-mode.ts +16 -7
  83. package/src/modes/types.ts +1 -0
  84. package/src/prompts/system/btw-user.md +3 -8
  85. package/src/prompts/tools/skill-discovery.md +1 -0
  86. package/src/runtime-mcp/client.ts +41 -55
  87. package/src/runtime-mcp/content-limits.ts +71 -0
  88. package/src/runtime-mcp/json-rpc.ts +23 -9
  89. package/src/runtime-mcp/transports/http.ts +50 -16
  90. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  91. package/src/sdk/bus/index.ts +4 -5
  92. package/src/sdk/bus/lifecycle-commands.ts +16 -7
  93. package/src/sdk/bus/notification-service.ts +41 -18
  94. package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
  95. package/src/sdk/bus/telegram-daemon-control.ts +2 -2
  96. package/src/sdk/bus/telegram-daemon.ts +1663 -375
  97. package/src/sdk/bus/topic-registry.ts +451 -12
  98. package/src/sdk/client/discovery.ts +19 -3
  99. package/src/sdk/client/liveness.ts +49 -0
  100. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  101. package/src/sdk/session.ts +29 -5
  102. package/src/sdk/transport/auth-preface.ts +82 -0
  103. package/src/sdk/transport/index.ts +18 -0
  104. package/src/sdk/transport/relay.ts +252 -0
  105. package/src/sdk/transport/serve-cli.ts +105 -0
  106. package/src/sdk/transport/socket.ts +197 -0
  107. package/src/sdk/transport/stdio.ts +17 -0
  108. package/src/session/agent-session.ts +240 -149
  109. package/src/session/blob-store.ts +19 -6
  110. package/src/session/btw-contract.ts +59 -0
  111. package/src/session/internal/managed-session-scope.ts +313 -57
  112. package/src/session/internal/managed-session-storage.ts +146 -35
  113. package/src/session/session-manager.ts +319 -85
  114. package/src/setup/credential-import.ts +28 -3
  115. package/src/setup/provider-onboarding.ts +30 -1
  116. package/src/setup/provider-presets.json +2 -1
  117. package/src/slash-commands/builtin-registry.ts +1 -1
  118. package/src/ssh/utils.ts +14 -0
  119. package/src/task/discovery.ts +8 -2
  120. package/src/tools/ask.ts +1 -1
  121. package/src/tools/fetch.ts +13 -131
  122. package/src/tools/image-gen.ts +301 -20
  123. package/src/tools/read.ts +2 -0
  124. package/src/tools/skill-discovery.ts +39 -3
  125. package/src/tools/sqlite-reader.ts +63 -5
  126. package/src/utils/pasted-image-path.ts +15 -6
  127. package/src/utils/shell-snapshot.ts +173 -63
  128. package/src/web/insane/bridge.ts +5 -1
  129. package/src/web/insane/url-guard.ts +138 -27
  130. package/src/web/scrapers/types.ts +19 -49
  131. package/src/web/scrapers/utils.ts +12 -26
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { Args, Command, Flags } from "@gajae-code/utils/cli";
4
+ import { Args, CliParseError, Command, Flags, renderCommandHelp } from "@gajae-code/utils/cli";
5
5
  import type { Args as ParsedArgs } from "../cli/args";
6
6
  import { Settings } from "../config/settings";
7
7
  import { applyStartupModelProfiles, createSessionManager } from "../main";
@@ -25,6 +25,7 @@ import {
25
25
  type SdkStartupRollbackResult,
26
26
  SdkStartupRollbackTracker,
27
27
  } from "../sdk/startup-capability";
28
+ import { runSdkServe } from "../sdk/transport/serve-cli";
28
29
  import {
29
30
  type CapturedSessionTranscriptSnapshot,
30
31
  type ResumeSessionIdentity,
@@ -423,19 +424,51 @@ export async function runSessionHost(
423
424
  await new Promise<void>(() => {});
424
425
  }
425
426
 
427
+ export type SdkInternalArgv = { action: "broker-internal"; agentDir: string } | { action: "session-host-internal" };
428
+
429
+ /** Parses the exact private argv contracts used by SDK child-process spawns. */
430
+ export function parseSdkInternalArgv(argv: readonly string[]): SdkInternalArgv {
431
+ if (argv[0] === "session-host-internal" && argv.length === 1) return { action: "session-host-internal" };
432
+ if (argv[0] === "broker-internal" && argv.length === 3 && argv[1] === "--agent-dir" && argv[2])
433
+ return { action: "broker-internal", agentDir: argv[2] };
434
+ throw new CliParseError("Invalid internal SDK invocation.");
435
+ }
436
+
437
+ class SdkServeHelp extends Command {
438
+ static description = "gjc sdk serve --stdio | --socket <path> [--session <id>] [--pending-ceiling <bytes>]";
439
+ static flags = {
440
+ stdio: Flags.boolean({ description: "Serve SDK frames over standard input and output" }),
441
+ socket: Flags.string({ description: "Serve SDK frames over a Unix socket path" }),
442
+ session: Flags.string({ description: "Attach to a specific SDK session" }),
443
+ "pending-ceiling": Flags.string({ description: "Maximum queued relay bytes per direction" }),
444
+ };
445
+ async run(): Promise<void> {}
446
+ }
447
+
426
448
  export default class Sdk extends Command {
427
- static description = "SDK internal services";
428
- static hidden = true;
429
- static args = { action: Args.string({ required: true, options: ["broker-internal", "session-host-internal"] }) };
430
- static flags = { "agent-dir": Flags.string({ description: "Internal broker agent directory" }) };
449
+ static description = "gjc sdk serve --stdio | --socket <path> [--session <id>] [--pending-ceiling <bytes>]";
450
+ static hidden = false;
451
+ static delegateHelp = true;
452
+ static args = { action: Args.string({ required: false, options: ["serve"] }) };
453
+ static flags = SdkServeHelp.flags;
431
454
  async run(): Promise<void> {
432
- const { args, flags } = await this.parse(Sdk);
433
- if (args.action === "session-host-internal") {
455
+ const action = this.argv[0];
456
+ if (this.argv.includes("--help") || this.argv.includes("-h")) {
457
+ renderCommandHelp("gjc", action === "serve" ? "sdk serve" : "sdk", action === "serve" ? SdkServeHelp : Sdk);
458
+ return;
459
+ }
460
+ if (action === "serve") {
461
+ await runSdkServe(this.argv.slice(1));
462
+ return;
463
+ }
464
+ if (action !== "broker-internal" && action !== "session-host-internal")
465
+ throw new CliParseError("Expected action to be serve.");
466
+ const internal = parseSdkInternalArgv(this.argv);
467
+ if (internal.action === "session-host-internal") {
434
468
  await runSessionHost();
435
469
  return;
436
470
  }
437
- const agentDir = flags["agent-dir"] as string | undefined;
438
- if (!agentDir) throw new Error("--agent-dir is required for sdk broker-internal.");
471
+ const agentDir = internal.agentDir;
439
472
  const broker = new Broker({
440
473
  agentDir,
441
474
  resolveDirectoryMigration: async cwd => {
@@ -172,7 +172,7 @@ export const KEYBINDINGS = {
172
172
  },
173
173
  "app.session.fork": {
174
174
  defaultKeys: [],
175
- description: "Fork session",
175
+ description: "Branch from message",
176
176
  },
177
177
  "app.session.resume": {
178
178
  defaultKeys: [],
@@ -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(
@@ -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
 
@@ -2451,9 +2460,9 @@ export const SETTINGS_SCHEMA = {
2451
2460
  default: false,
2452
2461
  ui: {
2453
2462
  tab: "tools",
2454
- label: "Insane Search Fallback",
2463
+ label: "Insane Search Fallback (Compatibility)",
2455
2464
  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.",
2465
+ "Compatibility-only preference. Remote renderer fallback stays disabled because it cannot preserve validated per-hop network routing.",
2457
2466
  },
2458
2467
  },
2459
2468
 
@@ -3290,25 +3299,66 @@ export const SETTINGS_SCHEMA = {
3290
3299
  },
3291
3300
  "providers.image": {
3292
3301
  type: "enum",
3293
- values: ["auto", "openai", "gemini", "openrouter", "antigravity"] as const,
3302
+ values: ["auto", "openai", "gemini", "openrouter", "antigravity", "custom"] as const,
3294
3303
  default: "auto",
3295
3304
  ui: {
3296
3305
  tab: "providers",
3297
- label: "Image Provider",
3298
- description: "Provider for image generation tool",
3306
+ label: "Image Generation",
3307
+ description: "Provider and model for image generation tool",
3299
3308
  options: [
3300
3309
  {
3301
3310
  value: "auto",
3302
3311
  label: "Auto",
3303
3312
  description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
3304
3313
  },
3305
- { value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
3314
+ { value: "openai", label: "OpenAI", description: "Uses gpt-image-2 via OpenAI Responses/Codex" },
3306
3315
  { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
3307
3316
  { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
3308
3317
  { value: "antigravity", label: "Antigravity", description: "Requires login with google-antigravity" },
3318
+ {
3319
+ value: "custom",
3320
+ label: "Custom",
3321
+ description: "OpenAI-compatible endpoint (set providers.imageCustomUrl)",
3322
+ },
3309
3323
  ],
3310
3324
  },
3311
3325
  },
3326
+ "providers.imageModel": {
3327
+ type: "string",
3328
+ default: undefined,
3329
+ ui: {
3330
+ tab: "providers",
3331
+ label: "Image Model",
3332
+ description: "Override the default image generation model for the selected provider",
3333
+ },
3334
+ },
3335
+ "providers.imageCustomUrl": {
3336
+ type: "string",
3337
+ default: undefined,
3338
+ ui: {
3339
+ tab: "providers",
3340
+ label: "Image Custom URL",
3341
+ description: "Base URL for custom OpenAI-compatible image endpoint",
3342
+ },
3343
+ },
3344
+ "providers.imageCustomKey": {
3345
+ type: "string",
3346
+ default: undefined,
3347
+ ui: {
3348
+ tab: "providers",
3349
+ label: "Image Custom API Key",
3350
+ description: "API key for custom OpenAI-compatible image endpoint",
3351
+ },
3352
+ },
3353
+ "providers.imageCustomKeyEnv": {
3354
+ type: "string",
3355
+ default: undefined,
3356
+ ui: {
3357
+ tab: "providers",
3358
+ label: "Image Custom API Key Env",
3359
+ description: "Environment variable name holding the API key for custom image endpoint",
3360
+ },
3361
+ },
3312
3362
 
3313
3363
  "providers.kimiApiFormat": {
3314
3364
  type: "enum",
@@ -3740,6 +3790,7 @@ export interface StatusLineSettings {
3740
3790
  maxRows: number;
3741
3791
  showHookStatus: boolean;
3742
3792
  showSkillHud: boolean;
3793
+ showActionHints: boolean;
3743
3794
  leftSegments: StatusLineSegmentId[];
3744
3795
  rightSegments: StatusLineSegmentId[];
3745
3796
  segmentOptions: Record<string, unknown>;
@@ -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.
@@ -5,11 +5,12 @@
5
5
  * Priority: 5 (low, project/user config discovery)
6
6
  */
7
7
  import * as path from "node:path";
8
- import { getSSHConfigPath, tryParseJson } from "@gajae-code/utils";
8
+ import { getSSHConfigPath, logger, tryParseJson } from "@gajae-code/utils";
9
9
  import { registerProvider } from "../capability";
10
10
  import { readFile } from "../capability/fs";
11
11
  import { type SSHHost, sshCapability } from "../capability/ssh";
12
12
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
13
+ import { validateSshDestination } from "../ssh/utils";
13
14
  import { expandTilde } from "../tools/path-utils";
14
15
  import { createSourceMeta, expandEnvVarsDeep } from "./helpers";
15
16
 
@@ -58,6 +59,16 @@ function normalizeHost(
58
59
  warnings.push(`Missing host for SSH entry: ${name}`);
59
60
  return null;
60
61
  }
62
+ const destinationError = validateSshDestination(raw.username, raw.host);
63
+ if (destinationError) {
64
+ warnings.push(`Invalid destination for SSH entry ${name}: ${destinationError}`);
65
+ logger.warn("Ignoring SSH entry with invalid destination", {
66
+ name,
67
+ path: source.path,
68
+ reason: destinationError,
69
+ });
70
+ return null;
71
+ }
61
72
 
62
73
  const port = parsePort(raw.port);
63
74
  if (raw.port !== undefined && port === undefined) {
@@ -8,9 +8,6 @@
8
8
  * Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
9
9
  * timeout.
10
10
  */
11
- import * as fs from "node:fs";
12
- import * as os from "node:os";
13
- import * as path from "node:path";
14
11
  import { $env, isBunTestRuntime, logger, Snowflake } from "@gajae-code/utils";
15
12
  import type { Subprocess } from "bun";
16
13
  import { $ } from "bun";
@@ -18,7 +15,7 @@ import { Settings } from "../../config/settings";
18
15
  import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
19
16
  import { resolvePythonIpcTrace, resolvePythonSkipCheck } from "./env";
20
17
  import { PYTHON_PRELUDE } from "./prelude";
21
- import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
18
+ import { ensureRunnerScript } from "./runner-artifact";
22
19
  import { ensurePythonRuntime, filterEnv, type PythonRuntimeOptions } from "./runtime";
23
20
 
24
21
  export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
@@ -27,23 +24,6 @@ export { renderKernelDisplay } from "./display";
27
24
  // Dual-read: `GJC_PYTHON_IPC_TRACE` is preferred, then legacy `PI_PYTHON_IPC_TRACE`.
28
25
  const TRACE_IPC = resolvePythonIpcTrace($env);
29
26
 
30
- // Cache the runner script on disk so the subprocess loads it normally. Cached
31
- // per script hash so installs don't race across versions.
32
- const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "gjc-python-runner");
33
- let RUNNER_SCRIPT_PATH: string | null = null;
34
-
35
- async function ensureRunnerScript(): Promise<string> {
36
- if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
37
- await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
38
- const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
39
- const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.py`);
40
- if (!fs.existsSync(target)) {
41
- await Bun.write(target, RUNNER_SCRIPT);
42
- }
43
- RUNNER_SCRIPT_PATH = target;
44
- return target;
45
- }
46
-
47
27
  const SHUTDOWN_GRACE_MS = 1_000;
48
28
  const STARTUP_TIMEOUT_MS = 10_000;
49
29
  // How long to wait after SIGINT for the runner to emit `done`. If the cell is
@@ -0,0 +1,84 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { postmortem } from "@gajae-code/utils";
5
+ import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
6
+
7
+ const DIRECTORY_MODE = 0o700;
8
+ const FILE_MODE = 0o600;
9
+ const DIRECTORY_PREFIX = "gjc-python-runner-";
10
+
11
+ interface RunnerScriptArtifact {
12
+ directory: string;
13
+ scriptPath: string;
14
+ }
15
+
16
+ async function initializeRunnerScript(tempRoot: string): Promise<RunnerScriptArtifact> {
17
+ const directory = await fs.mkdtemp(path.join(tempRoot, DIRECTORY_PREFIX));
18
+ try {
19
+ if (process.platform !== "win32") await fs.chmod(directory, DIRECTORY_MODE);
20
+ const scriptPath = path.join(directory, "runner.py");
21
+ const handle = await fs.open(scriptPath, "wx", FILE_MODE);
22
+ try {
23
+ await handle.writeFile(RUNNER_SCRIPT, { encoding: "utf8" });
24
+ if (process.platform !== "win32") await handle.chmod(FILE_MODE);
25
+ } finally {
26
+ await handle.close();
27
+ }
28
+ return { directory, scriptPath };
29
+ } catch (error) {
30
+ await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ export interface RunnerScriptCache {
36
+ ensureRunnerScript(): Promise<string>;
37
+ cleanup(): Promise<void>;
38
+ }
39
+
40
+ export function createRunnerScriptCache(tempRoot: string): RunnerScriptCache {
41
+ let initialization: Promise<RunnerScriptArtifact> | null = null;
42
+ let artifact: RunnerScriptArtifact | null = null;
43
+ let cleanupAttempt: Promise<void> | null = null;
44
+
45
+ return {
46
+ async ensureRunnerScript() {
47
+ if (artifact) return artifact.scriptPath;
48
+ if (!initialization) initialization = initializeRunnerScript(tempRoot);
49
+ try {
50
+ artifact = await initialization;
51
+ return artifact.scriptPath;
52
+ } catch (error) {
53
+ if (initialization) initialization = null;
54
+ throw error;
55
+ }
56
+ },
57
+ async cleanup() {
58
+ if (cleanupAttempt) return await cleanupAttempt;
59
+ cleanupAttempt = (async () => {
60
+ const initialized = await initialization?.catch(() => null);
61
+ const directory = artifact?.directory ?? initialized?.directory;
62
+ artifact = null;
63
+ initialization = null;
64
+ if (directory) await fs.rm(directory, { recursive: true, force: true });
65
+ })();
66
+ try {
67
+ await cleanupAttempt;
68
+ } finally {
69
+ cleanupAttempt = null;
70
+ }
71
+ },
72
+ };
73
+ }
74
+
75
+ /** @internal Test-only initializer for isolated artifact assertions. */
76
+ export function createRunnerScriptInitializer(tempRoot: string): () => Promise<string> {
77
+ return createRunnerScriptCache(tempRoot).ensureRunnerScript;
78
+ }
79
+
80
+ const processRunnerScriptCache = createRunnerScriptCache(os.tmpdir());
81
+
82
+ export const ensureRunnerScript = () => processRunnerScriptCache.ensureRunnerScript();
83
+
84
+ postmortem.register("python-runner-artifact", () => processRunnerScriptCache.cleanup());