@gajae-code/coding-agent 0.6.5 → 0.7.1

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 (135) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +38 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
  20. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  21. package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
  22. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  23. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
  24. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  28. package/dist/types/modes/types.d.ts +7 -1
  29. package/dist/types/notifications/config-commands.d.ts +26 -0
  30. package/dist/types/notifications/config.d.ts +61 -0
  31. package/dist/types/notifications/helpers.d.ts +55 -0
  32. package/dist/types/notifications/html-format.d.ts +62 -0
  33. package/dist/types/notifications/index.d.ts +28 -0
  34. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  35. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  36. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  37. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  38. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  39. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  40. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  41. package/dist/types/notifications/threaded-render.d.ts +66 -0
  42. package/dist/types/notifications/topic-registry.d.ts +67 -0
  43. package/dist/types/rlm/index.d.ts +12 -0
  44. package/dist/types/session/agent-session.d.ts +39 -2
  45. package/dist/types/session/auth-storage.d.ts +1 -1
  46. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  47. package/dist/types/setup/credential-import.d.ts +3 -0
  48. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  49. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  50. package/dist/types/tools/index.d.ts +18 -0
  51. package/dist/types/tools/subagent.d.ts +3 -0
  52. package/package.json +7 -7
  53. package/scripts/build-binary.ts +3 -0
  54. package/src/async/job-manager.ts +5 -1
  55. package/src/cli/daemon-cli.ts +122 -0
  56. package/src/cli/notify-cli.ts +274 -0
  57. package/src/cli/setup-cli.ts +173 -84
  58. package/src/cli.ts +3 -3
  59. package/src/commands/daemon.ts +47 -0
  60. package/src/commands/notify.ts +61 -0
  61. package/src/commands/setup.ts +11 -1
  62. package/src/config/model-profile-activation.ts +74 -5
  63. package/src/config/model-profiles.ts +7 -4
  64. package/src/config/model-registry.ts +6 -3
  65. package/src/config/models-config-schema.ts +1 -1
  66. package/src/config/settings-schema.ts +29 -0
  67. package/src/coordinator/contract.ts +3 -0
  68. package/src/coordinator-mcp/server.ts +270 -1
  69. package/src/daemon/builtin.ts +46 -0
  70. package/src/daemon/control-types.ts +65 -0
  71. package/src/daemon/runtime.ts +51 -0
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  73. package/src/edit/modes/replace.ts +1 -1
  74. package/src/extensibility/extensions/runner.ts +4 -0
  75. package/src/extensibility/extensions/types.ts +8 -0
  76. package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
  77. package/src/gjc-runtime/launch-tmux.ts +10 -2
  78. package/src/gjc-runtime/state-runtime.ts +18 -4
  79. package/src/gjc-runtime/state-writer.ts +8 -8
  80. package/src/gjc-runtime/tmux-common.ts +8 -0
  81. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  82. package/src/gjc-runtime/ultragoal-guard.ts +57 -2
  83. package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
  84. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  85. package/src/gjc-runtime/workflow-manifest.ts +11 -1
  86. package/src/goals/tools/goal-tool.ts +11 -2
  87. package/src/hashline/hash.ts +1 -1
  88. package/src/internal-urls/docs-index.generated.ts +9 -7
  89. package/src/main.ts +30 -0
  90. package/src/modes/acp/acp-event-mapper.ts +1 -0
  91. package/src/modes/components/hook-editor.ts +7 -2
  92. package/src/modes/components/oauth-selector.ts +19 -0
  93. package/src/modes/controllers/event-controller.ts +20 -0
  94. package/src/modes/controllers/selector-controller.ts +80 -17
  95. package/src/modes/interactive-mode.ts +6 -2
  96. package/src/modes/runtime-init.ts +1 -0
  97. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  98. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  99. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  101. package/src/modes/types.ts +7 -1
  102. package/src/modes/utils/ui-helpers.ts +23 -0
  103. package/src/notifications/config-commands.ts +50 -0
  104. package/src/notifications/config.ts +107 -0
  105. package/src/notifications/helpers.ts +135 -0
  106. package/src/notifications/html-format.ts +389 -0
  107. package/src/notifications/index.ts +700 -0
  108. package/src/notifications/rate-limit-pool.ts +179 -0
  109. package/src/notifications/telegram-cli.ts +194 -0
  110. package/src/notifications/telegram-daemon-cli.ts +74 -0
  111. package/src/notifications/telegram-daemon-control.ts +370 -0
  112. package/src/notifications/telegram-daemon.ts +1370 -0
  113. package/src/notifications/telegram-reference.ts +335 -0
  114. package/src/notifications/threaded-inbound.ts +80 -0
  115. package/src/notifications/threaded-render.ts +155 -0
  116. package/src/notifications/topic-registry.ts +133 -0
  117. package/src/rlm/index.ts +19 -0
  118. package/src/sdk.ts +16 -0
  119. package/src/session/agent-session.ts +113 -3
  120. package/src/session/auth-storage.ts +3 -0
  121. package/src/session/session-dump-format.ts +43 -2
  122. package/src/session/session-manager.ts +39 -5
  123. package/src/setup/credential-auto-import.ts +258 -0
  124. package/src/setup/credential-import.ts +17 -0
  125. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  126. package/src/setup/host-plugin-setup.ts +142 -0
  127. package/src/slash-commands/builtin-registry.ts +4 -1
  128. package/src/task/executor.ts +5 -1
  129. package/src/tools/ask-answer-registry.ts +25 -0
  130. package/src/tools/ask.ts +77 -6
  131. package/src/tools/image-gen.ts +5 -8
  132. package/src/tools/index.ts +19 -0
  133. package/src/tools/inspect-image.ts +16 -11
  134. package/src/tools/subagent-render.ts +7 -0
  135. package/src/tools/subagent.ts +38 -7
@@ -32,6 +32,7 @@ import type { PythonResult } from "../../eval/py/executor";
32
32
  import type { BashResult } from "../../exec/bash-executor";
33
33
  import type { ExecOptions, ExecResult } from "../../exec/exec";
34
34
  import type { CustomEditor } from "../../modes/components/custom-editor";
35
+ import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/unattended-session";
35
36
  import type { Theme } from "../../modes/theme/theme";
36
37
  import type { CustomMessage } from "../../session/messages";
37
38
  import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
@@ -310,6 +311,11 @@ export interface ExtensionContext {
310
311
  getSystemPrompt(): string[];
311
312
  /** @deprecated Use hasPendingMessages() instead */
312
313
  hasQueuedMessages(): boolean;
314
+ /**
315
+ * Unattended workflow-gate bridge. Present only when the session runs in
316
+ * unattended/RPC mode; `undefined` in interactive/TUI mode (notify-only).
317
+ */
318
+ workflowGate?: WorkflowGateEmitter;
313
319
  }
314
320
 
315
321
  /**
@@ -1234,6 +1240,8 @@ export interface ExtensionContextActions {
1234
1240
  getContextUsage: () => ContextUsage | undefined;
1235
1241
  compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
1236
1242
  getSystemPrompt: () => string[];
1243
+ /** Unattended workflow-gate bridge (present only in unattended/RPC mode). */
1244
+ getWorkflowGate?: () => WorkflowGateEmitter | undefined;
1237
1245
  }
1238
1246
 
1239
1247
  /** Actions for ExtensionCommandContext (ctx.* in command handlers). */
@@ -326,13 +326,21 @@ async function persistEnvelope(
326
326
  payload.version ??= WORKFLOW_STATE_VERSION;
327
327
  payload.active ??= true;
328
328
  payload.current_phase ??= "interviewing";
329
- await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
329
+ const expectedRevision = existingStateRevision(envelope);
330
+ const writeResult = await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
330
331
  cwd,
331
332
  policy: "source",
332
- expectedRevision: existingStateRevision(envelope),
333
+ expectedRevision,
333
334
  receipt: { cwd, skill: "deep-interview", owner: "gjc-runtime", command, sessionId, nowIso: now },
334
335
  audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview", sessionId },
335
336
  });
337
+ // Reflect the freshly written revision back onto the in-memory envelope so a
338
+ // follow-up HUD sync derives its `sourceRevision` from the persisted revision
339
+ // (not the stale pre-write value), otherwise the active-state writer treats the
340
+ // newer HUD as stale and skips it (e.g. dropping the ambiguity chip after scoring).
341
+ if (writeResult.written && typeof expectedRevision === "number") {
342
+ (envelope as Record<string, unknown>).state_revision = expectedRevision + 1;
343
+ }
336
344
  await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-recorder", path: statePath });
337
345
  }
338
346
 
@@ -354,8 +362,8 @@ async function syncRecorderHud(
354
362
  phase,
355
363
  sessionId,
356
364
  source: "gjc-runtime-deep-interview-recorder",
357
- hud: deriveDeepInterviewHud(envelope as Record<string, unknown>, { phase }),
358
- sourceRevision: existingStateRevision(envelope),
365
+ hud: deriveDeepInterviewHud(normalizeDeepInterviewEnvelope(envelope) as Record<string, unknown>, { phase }),
366
+ sourceRevision: (existingStateRevision(envelope) ?? 0) + 1,
359
367
  });
360
368
  }
361
369
 
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import * as path from "node:path";
3
3
  import { safeStderrWrite } from "@gajae-code/utils";
4
+ import { VERSION } from "@gajae-code/utils/dirs";
4
5
  import type { Args } from "../cli/args";
5
6
  import { tmuxRuntimeSessionPath } from "./session-layout";
6
7
  import { GJC_COORDINATOR_SESSION_ID_ENV, GJC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
@@ -16,7 +17,7 @@ import {
16
17
  type GjcTmuxProfileCommand,
17
18
  resolveGjcTmuxCommand,
18
19
  } from "./tmux-common";
19
- import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope } from "./tmux-sessions";
20
+ import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope, type GjcTmuxSessionStatus } from "./tmux-sessions";
20
21
 
21
22
  export {
22
23
  buildGjcTmuxProfileCommands,
@@ -88,6 +89,9 @@ export interface TmuxLaunchPlan {
88
89
  function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
89
90
  return env.GJC_TMUX_SESSION?.trim() || undefined;
90
91
  }
92
+ function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolean {
93
+ return session?.version === VERSION;
94
+ }
91
95
 
92
96
  function findExistingSessionForLaunch(context: {
93
97
  env: NodeJS.ProcessEnv;
@@ -96,7 +100,8 @@ function findExistingSessionForLaunch(context: {
96
100
  }): string | undefined {
97
101
  const explicit = explicitTmuxSessionName(context.env);
98
102
  if (explicit) return findGjcTmuxSessionByName(explicit, context.env)?.name;
99
- return findGjcTmuxSessionByScope(context.project, context.branch, context.env)?.name;
103
+ const scoped = findGjcTmuxSessionByScope(context.project, context.branch, context.env);
104
+ return hasCurrentGjcVersion(scoped) ? scoped?.name : undefined;
100
105
  }
101
106
 
102
107
  export interface GjcTmuxProfileResult {
@@ -116,6 +121,7 @@ export interface GjcTmuxProfileContext {
116
121
  project?: string | null;
117
122
  sessionId?: string | null;
118
123
  sessionStateFile?: string | null;
124
+ version?: string | null;
119
125
  }
120
126
 
121
127
  interface CommandResolutionContext {
@@ -195,6 +201,7 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
195
201
  project: context.project ?? null,
196
202
  sessionId: context.sessionId ?? env[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
197
203
  sessionStateFile: context.sessionStateFile ?? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
204
+ version: context.version ?? null,
198
205
  });
199
206
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
200
207
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
@@ -457,6 +464,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
457
464
  project: plan.project,
458
465
  sessionId: plan.sessionId ?? null,
459
466
  sessionStateFile: plan.sessionStateFile ?? null,
467
+ version: VERSION,
460
468
  });
461
469
  const ownershipFailure = profile.failures.find(item => item.command.args.includes("@gjc-profile"));
462
470
  if (ownershipFailure) {
@@ -818,7 +818,7 @@ async function writeJsonAtomic(
818
818
  toPhase?: string;
819
819
  owner?: WorkflowStateMutationOwner;
820
820
  },
821
- ): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
821
+ ): Promise<{ warning?: string; stamped: Record<string, unknown>; revision: number }> {
822
822
  const warning = options?.skill
823
823
  ? await warnAndAuditOutOfBandIfNeeded(cwd, options.sessionId, filePath, options.skill, {
824
824
  mutationId: options.mutationId,
@@ -832,7 +832,7 @@ async function writeJsonAtomic(
832
832
  // writer lock; do not enforce an optimistic `expectedRevision` here (tamper
833
833
  // detection is handled by warnAndAuditOutOfBandIfNeeded above, and a forced
834
834
  // write must succeed over corrupt/missing prior state).
835
- await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
835
+ const writeResult = await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
836
836
  cwd,
837
837
  policy: "source",
838
838
  audit: {
@@ -847,7 +847,11 @@ async function writeJsonAtomic(
847
847
  forced: options?.force ?? false,
848
848
  },
849
849
  });
850
- return { warning, stamped: (await readJsonFile(filePath)) ?? {} };
850
+ // `writeResult.revision` is computed inside the writer lock, so it is the revision this
851
+ // write actually owns. Prefer it over a post-lock file re-read, which a concurrent writer
852
+ // could have advanced before the read — that race could otherwise let this payload be
853
+ // published with another writer's newer revision.
854
+ return { warning, stamped: (await readJsonFile(filePath)) ?? {}, revision: writeResult.revision };
851
855
  }
852
856
 
853
857
  function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
@@ -1399,7 +1403,11 @@ async function handleWrite(
1399
1403
  const validation = validateWorkflowStateEnvelope(mode, merged);
1400
1404
  if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
1401
1405
 
1402
- const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, merged, "write", {
1406
+ const {
1407
+ warning: outOfBandWarning,
1408
+ stamped,
1409
+ revision: stampedRevision,
1410
+ } = await writeJsonAtomic(cwd, filePath, merged, "write", {
1403
1411
  sessionId,
1404
1412
  skill: mode,
1405
1413
  mutationId,
@@ -1411,6 +1419,12 @@ async function handleWrite(
1411
1419
 
1412
1420
  const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
1413
1421
  const active = merged.active !== false;
1422
+ // Reflect the lock-owned mode-state revision onto the in-memory payload so the active-state/HUD
1423
+ // sync derives a `sourceRevision` from the revision this write actually owns (computed inside the
1424
+ // writer lock), not the stale pre-write value or a post-lock re-read a concurrent writer could
1425
+ // have advanced; otherwise the active-state writer stale-skips the update and the mirror keeps the
1426
+ // prior phase (e.g. staying "interviewing" after a "handoff" write).
1427
+ merged.state_revision = stampedRevision;
1414
1428
  await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
1415
1429
  await touchStateActivityMarker(cwd, sessionId, filePath);
1416
1430
 
@@ -99,8 +99,8 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
99
99
  }
100
100
 
101
101
  export type GuardedWriteResult =
102
- | { path: string; written: true }
103
- | { path: string; written: false; reason: "stale-skip" };
102
+ | { path: string; written: true; revision: number }
103
+ | { path: string; written: false; reason: "stale-skip"; revision: number };
104
104
 
105
105
  export interface StateWriterOptions {
106
106
  cwd?: string;
@@ -511,13 +511,13 @@ async function writeGuardedResolvedJsonAtomic(
511
511
  const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
512
512
  await atomicWrite(filePath, jsonText(next));
513
513
  await maybeAudit(filePath, options);
514
- return { path: filePath, written: true };
514
+ return { path: filePath, written: true, revision: currentRevision + 1 };
515
515
  }
516
516
 
517
517
  const incomingSourceRevision =
518
518
  options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
519
519
  if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
520
- return { path: filePath, written: false, reason: "stale-skip" };
520
+ return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
521
521
  }
522
522
  const next = stampStateRevision(
523
523
  withWorkflowReceipt(value, buildReceipt(options)),
@@ -526,7 +526,7 @@ async function writeGuardedResolvedJsonAtomic(
526
526
  );
527
527
  await atomicWrite(filePath, jsonText(next));
528
528
  await maybeAudit(filePath, options);
529
- return { path: filePath, written: true };
529
+ return { path: filePath, written: true, revision: currentRevision + 1 };
530
530
  },
531
531
  options.lock,
532
532
  );
@@ -574,13 +574,13 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
574
574
  }
575
575
  await atomicWrite(filePath, jsonText(next));
576
576
  await maybeAudit(filePath, options);
577
- return { path: filePath, written: true };
577
+ return { path: filePath, written: true, revision: currentRevision + 1 };
578
578
  }
579
579
 
580
580
  const incomingSourceRevision =
581
581
  options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
582
582
  if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
583
- return { path: filePath, written: false, reason: "stale-skip" };
583
+ return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
584
584
  }
585
585
  const next = stampWorkflowEnvelopeRevisionAndChecksum(
586
586
  value,
@@ -599,7 +599,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
599
599
  }
600
600
  await atomicWrite(filePath, jsonText(next));
601
601
  await maybeAudit(filePath, options);
602
- return { path: filePath, written: true };
602
+ return { path: filePath, written: true, revision: currentRevision + 1 };
603
603
  },
604
604
  options.lock,
605
605
  );
@@ -10,6 +10,7 @@ export const GJC_TMUX_BRANCH_SLUG_OPTION = "@gjc-branch-slug";
10
10
  export const GJC_TMUX_PROJECT_OPTION = "@gjc-project";
11
11
  export const GJC_TMUX_SESSION_ID_OPTION = "@gjc-session-id";
12
12
  export const GJC_TMUX_SESSION_STATE_FILE_OPTION = "@gjc-session-state-file";
13
+ export const GJC_TMUX_VERSION_OPTION = "@gjc-version";
13
14
 
14
15
  export interface GjcTmuxProfileCommand {
15
16
  description: string;
@@ -101,6 +102,7 @@ export function buildGjcTmuxRequiredProfileCommands(
101
102
  project?: string | null;
102
103
  sessionId?: string | null;
103
104
  sessionStateFile?: string | null;
105
+ version?: string | null;
104
106
  } = {},
105
107
  ): GjcTmuxProfileCommand[] {
106
108
  const commands: GjcTmuxProfileCommand[] = [
@@ -134,6 +136,11 @@ export function buildGjcTmuxRequiredProfileCommands(
134
136
  description: "record GJC session state marker",
135
137
  args: ["set-option", "-t", target, GJC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
136
138
  });
139
+ if (metadata.version)
140
+ commands.push({
141
+ description: "record GJC version identity",
142
+ args: ["set-option", "-t", target, GJC_TMUX_VERSION_OPTION, metadata.version],
143
+ });
137
144
  return commands;
138
145
  }
139
146
 
@@ -146,6 +153,7 @@ export function buildGjcTmuxProfileCommands(
146
153
  project?: string | null;
147
154
  sessionId?: string | null;
148
155
  sessionStateFile?: string | null;
156
+ version?: string | null;
149
157
  } = {},
150
158
  ): GjcTmuxProfileCommand[] {
151
159
  const commands = buildGjcTmuxRequiredProfileCommands(target, metadata);
@@ -10,6 +10,7 @@ import {
10
10
  GJC_TMUX_PROJECT_OPTION,
11
11
  GJC_TMUX_SESSION_ID_OPTION,
12
12
  GJC_TMUX_SESSION_STATE_FILE_OPTION,
13
+ GJC_TMUX_VERSION_OPTION,
13
14
  normalizeTmuxCreatedAt,
14
15
  resolveGjcTmuxCommand,
15
16
  } from "./tmux-common";
@@ -26,6 +27,7 @@ export interface GjcTmuxSessionStatus {
26
27
  project?: string;
27
28
  sessionId?: string;
28
29
  sessionStateFile?: string;
30
+ version?: string;
29
31
  panePids: number[];
30
32
  profile?: string;
31
33
  }
@@ -37,6 +39,7 @@ export interface GjcTmuxSessionTagsForGc {
37
39
  branchSlug?: string;
38
40
  sessionId?: string;
39
41
  sessionStateFile?: string;
42
+ version?: string;
40
43
  createdAt?: string;
41
44
  attached?: boolean;
42
45
  panePids?: number[];
@@ -86,6 +89,7 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
86
89
  project = "",
87
90
  sessionId = "",
88
91
  sessionStateFile = "",
92
+ version = "",
89
93
  ] = line.split("\t");
90
94
  if (!name) return null;
91
95
  return {
@@ -105,6 +109,7 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
105
109
  profile: profile || undefined,
106
110
  sessionId: sessionId || undefined,
107
111
  sessionStateFile: sessionStateFile || undefined,
112
+ version: version || undefined,
108
113
  };
109
114
  }
110
115
 
@@ -131,7 +136,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
131
136
 
132
137
  function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
133
138
  return runListSessions(
134
- `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}`,
139
+ `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${GJC_TMUX_VERSION_OPTION}}`,
135
140
  env,
136
141
  );
137
142
  }
@@ -265,6 +270,7 @@ function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: Node
265
270
  sessionId: session.sessionId ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, env),
266
271
  sessionStateFile:
267
272
  session.sessionStateFile ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
273
+ version: session.version ?? readExactOptionForGc(session.name, GJC_TMUX_VERSION_OPTION, env),
268
274
  };
269
275
  }
270
276
 
@@ -281,6 +287,7 @@ export function readTmuxSessionTagsForGc(
281
287
  branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
282
288
  sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION, env),
283
289
  sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
290
+ version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION, env),
284
291
  createdAt: session?.createdAt,
285
292
  attached: session?.attached,
286
293
  panePids: session?.panePids,
@@ -288,14 +288,15 @@ export function validateCompletionReceipt(input: {
288
288
  export async function readUltragoalVerificationState(input: {
289
289
  cwd: string;
290
290
  currentGoal?: CurrentGoalLike | null;
291
+ sessionId?: string | null;
291
292
  }): Promise<UltragoalGuardDiagnostic> {
292
293
  const currentObjective = input.currentGoal?.objective?.trim() ?? "";
293
294
  if (!currentObjective) return { state: "inactive", message: "No current goal objective is active." };
294
295
  let plan: UltragoalPlan | null;
295
296
  let ledger: UltragoalLedgerEvent[];
296
297
  try {
297
- plan = await readUltragoalPlan(input.cwd);
298
- ledger = await readUltragoalLedger(input.cwd);
298
+ plan = await readUltragoalPlan(input.cwd, input.sessionId ?? undefined);
299
+ ledger = await readUltragoalLedger(input.cwd, input.sessionId ?? undefined);
299
300
  } catch (error) {
300
301
  if (currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE) {
301
302
  return {
@@ -479,6 +480,7 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
479
480
  export async function assertCanCompleteCurrentGoal(input: {
480
481
  cwd: string;
481
482
  currentGoal?: CurrentGoalLike | null;
483
+ sessionId?: string | null;
482
484
  }): Promise<void> {
483
485
  if (!input.cwd) return;
484
486
  const diagnostic = await readUltragoalVerificationState(input);
@@ -496,3 +498,56 @@ export function isUltragoalBypassPrompt(prompt: string): boolean {
496
498
  ) || /goal[\s\S]{0,80}complete/i.test(normalized)
497
499
  );
498
500
  }
501
+ export interface UltragoalPauseBlockDiagnostic {
502
+ blocked: boolean;
503
+ reason: string;
504
+ }
505
+
506
+ /**
507
+ * While an Ultragoal run is active, `goal({"op":"pause"})` is only allowed when the
508
+ * current durable Ultragoal state is readable and the latest durable ledger event
509
+ * classifies the current blocker as `human_blocked`. Resolvable blockers must be
510
+ * worked, not parked. Reads fail closed so unreadable durable state or ledger data
511
+ * blocks pause rather than silently allowing a give-up.
512
+ */
513
+ export async function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPauseBlockDiagnostic> {
514
+ if (!cwd) return { blocked: false, reason: "No cwd to resolve durable Ultragoal state." };
515
+ const ask = await isUltragoalAskBlocked(cwd);
516
+ if (ask.source === "durable_state_unreadable") {
517
+ return {
518
+ blocked: true,
519
+ reason: `Unable to verify current durable Ultragoal state for pause: ${ask.reason}`,
520
+ };
521
+ }
522
+ if (!ask.active) return { blocked: false, reason: "No active Ultragoal run." };
523
+ let ledger: UltragoalLedgerEvent[];
524
+ try {
525
+ ledger = await readUltragoalLedger(cwd);
526
+ } catch (error) {
527
+ return {
528
+ blocked: true,
529
+ reason: `Unable to read durable Ultragoal ledger: ${error instanceof Error ? error.message : String(error)}`,
530
+ };
531
+ }
532
+ const latest = ledger.at(-1);
533
+ if (latest?.event === "blocker_classified" && latest.classification === "human_blocked") {
534
+ return { blocked: false, reason: "Latest Ultragoal ledger event classifies the blocker as human_blocked." };
535
+ }
536
+ return {
537
+ blocked: true,
538
+ reason:
539
+ "An Ultragoal run is active. Pausing requires the current blocker to be classified human_blocked as the latest ledger event.",
540
+ };
541
+ }
542
+
543
+ export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
544
+ const diagnostic = await isUltragoalPauseBlocked(cwd);
545
+ if (!diagnostic.blocked) return;
546
+ throw new Error(
547
+ [
548
+ diagnostic.reason,
549
+ "Resolvable blockers must be worked, not paused: investigate, `gjc ultragoal steer --kind add_subgoal`, delegate an executor, or `gjc ultragoal record-review-blockers`.",
550
+ 'If the blocker is genuinely human-only, record `gjc ultragoal classify-blocker --classification human_blocked --evidence "<human-only dependency>"` immediately before pausing.',
551
+ ].join("\n"),
552
+ );
553
+ }
@@ -898,32 +898,29 @@ function categorizeComputerChangePath(value: string): UltragoalChangeCategory {
898
898
  return "other";
899
899
  }
900
900
 
901
- function isComputerChangePath(row: UltragoalChangeSetPath): boolean {
901
+ function isComputerControlSurfaceCategory(category: UltragoalChangeCategory): boolean {
902
902
  return (
903
- categorizeComputerChangePath(row.path) !== "other" ||
904
- (row.oldPath ? categorizeComputerChangePath(row.oldPath) !== "other" : false)
903
+ category === "code" || category === "generated-binding" || category === "tool" || category === "settings-registry"
905
904
  );
906
905
  }
907
906
 
908
- function isDocsOnlyStaticComputerChangeSet(changeSet: UltragoalChangeSet | undefined): boolean {
909
- if (!changeSet || changeSet.paths.length === 0) return false;
910
- return changeSet.paths.every(row => {
911
- const category = row.category ?? categorizeComputerChangePath(row.path);
912
- const oldCategory = row.oldPath ? categorizeComputerChangePath(row.oldPath) : category;
913
- return category === "docs-static" && oldCategory === "docs-static";
914
- });
907
+ function isComputerControlSurfaceChangePath(row: UltragoalChangeSetPath): boolean {
908
+ const category = row.category ?? categorizeComputerChangePath(row.path);
909
+ const oldCategory = row.oldPath ? categorizeComputerChangePath(row.oldPath) : category;
910
+ return isComputerControlSurfaceCategory(category) || isComputerControlSurfaceCategory(oldCategory);
915
911
  }
916
912
 
917
913
  function trustedChangeSetRequiresComputerSuite(changeSet: UltragoalChangeSet | undefined): boolean {
918
914
  if (!changeSet?.trusted) return false;
919
- if (isDocsOnlyStaticComputerChangeSet(changeSet)) return false;
920
- return changeSet.paths.some(isComputerChangePath);
915
+ return changeSet.paths.some(isComputerControlSurfaceChangePath);
921
916
  }
922
917
 
923
918
  function requiresComputerRedTeamSuite(executorQa: JsonObject, changeSet: UltragoalChangeSet | undefined): boolean {
924
919
  if (trustedChangeSetRequiresComputerSuite(changeSet)) return true;
925
920
  const declaredPaths = Array.isArray(executorQa.changedPaths) ? executorQa.changedPaths : [];
926
- return declaredPaths.some(value => typeof value === "string" && categorizeComputerChangePath(value) !== "other");
921
+ return declaredPaths.some(
922
+ value => typeof value === "string" && isComputerControlSurfaceCategory(categorizeComputerChangePath(value)),
923
+ );
927
924
  }
928
925
 
929
926
  function normalizeAdversarialCaseId(value: string): string {
@@ -2853,6 +2850,33 @@ export async function recordUltragoalReviewBlockers(input: {
2853
2850
  return plan;
2854
2851
  }
2855
2852
 
2853
+ export type UltragoalBlockerClassification = "human_blocked" | "resolvable";
2854
+
2855
+ /**
2856
+ * Record an audited blocker triage classification in the durable ledger. A
2857
+ * `human_blocked` classification is the only thing that authorizes
2858
+ * `goal({"op":"pause"})` while an Ultragoal run is active; `resolvable` is an
2859
+ * audit note and never unblocks pause.
2860
+ */
2861
+ export async function recordUltragoalBlockerClassification(input: {
2862
+ cwd: string;
2863
+ classification: UltragoalBlockerClassification;
2864
+ evidence: string;
2865
+ goalId?: string;
2866
+ }): Promise<UltragoalLedgerEvent> {
2867
+ const evidence = input.evidence.trim();
2868
+ if (!evidence) throw new Error("classify-blocker --evidence is required");
2869
+ if (input.classification !== "human_blocked" && input.classification !== "resolvable") {
2870
+ throw new Error('classify-blocker --classification must be "human_blocked" or "resolvable"');
2871
+ }
2872
+ return appendLedger(input.cwd, {
2873
+ event: "blocker_classified",
2874
+ classification: input.classification,
2875
+ ...(input.goalId?.trim() ? { goalId: input.goalId.trim() } : {}),
2876
+ evidence,
2877
+ });
2878
+ }
2879
+
2856
2880
  type UltragoalReviewMode = "review-only" | "review-start";
2857
2881
  type UltragoalReviewContractStrength = "strong" | "thin-derived";
2858
2882
 
@@ -2920,11 +2944,33 @@ async function spawnText(
2920
2944
  }
2921
2945
  }
2922
2946
 
2923
- async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
2924
- const candidates = branch ? [branch] : ["origin/main", "origin/master", "main", "master"];
2925
- for (const candidate of candidates) {
2926
- const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
2927
- if (exists.ok) return candidate;
2947
+ export async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
2948
+ if (branch) {
2949
+ const exists = await spawnText(["git", "rev-parse", "--verify", branch], { cwd, timeoutMs: 3000 });
2950
+ if (exists.ok) return branch;
2951
+ } else {
2952
+ // Prefer the NEAREST integration base (the branch this work actually forks
2953
+ // from) rather than always `main`. A branch opened against `dev` must be
2954
+ // scoped to `dev`; using a stale `main` sweeps in unrelated trunk history
2955
+ // and mis-attributes other people's changes to this story (e.g. falsely
2956
+ // tripping change-scoped gates). Among existing candidates, pick the one
2957
+ // whose merge-base with HEAD is closest to HEAD (fewest commits ahead).
2958
+ const candidates = ["origin/dev", "dev", "origin/main", "origin/master", "main", "master"];
2959
+ let best: { ref: string; ahead: number } | undefined;
2960
+ for (const candidate of candidates) {
2961
+ const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
2962
+ if (!exists.ok) continue;
2963
+ const mergeBase = await spawnText(["git", "merge-base", "HEAD", candidate], { cwd, timeoutMs: 3000 });
2964
+ if (!mergeBase.ok || !mergeBase.stdout.trim()) continue;
2965
+ const count = await spawnText(["git", "rev-list", "--count", `${mergeBase.stdout.trim()}..HEAD`], {
2966
+ cwd,
2967
+ timeoutMs: 3000,
2968
+ });
2969
+ const ahead = Number.parseInt(count.stdout.trim(), 10);
2970
+ if (!Number.isFinite(ahead)) continue;
2971
+ if (!best || ahead < best.ahead) best = { ref: candidate, ahead };
2972
+ }
2973
+ if (best) return best.ref;
2928
2974
  }
2929
2975
  const mergeBase = await spawnText(["git", "merge-base", "HEAD", "origin/main"], { cwd, timeoutMs: 3000 });
2930
2976
  if (mergeBase.ok && mergeBase.stdout.trim()) return mergeBase.stdout.trim();
@@ -3245,6 +3291,7 @@ const FLAGS_WITH_VALUES = new Set([
3245
3291
  "--rationale",
3246
3292
  "--replacements-json",
3247
3293
  "--order-json",
3294
+ "--classification",
3248
3295
  ]);
3249
3296
 
3250
3297
  function isHelpArg(arg: string): boolean {
@@ -3319,6 +3366,25 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
3319
3366
  "",
3320
3367
  ].join("\n");
3321
3368
  }
3369
+ if (subject === "classify-blocker") {
3370
+ return [
3371
+ "Run native GJC Ultragoal workflow commands",
3372
+ "",
3373
+ "USAGE",
3374
+ " $ gjc ultragoal classify-blocker --classification <human_blocked|resolvable> --evidence <text> [FLAGS]",
3375
+ "",
3376
+ "FLAGS",
3377
+ " --classification=<value> Required. human_blocked authorizes pause only as the latest ledger event; resolvable never authorizes pause",
3378
+ " --evidence=<value> Required. Specific blocker evidence; must name the human-only dependency for human_blocked",
3379
+ " --goal-id=<value> Optional durable .gjc/ultragoal goal id, e.g. G001",
3380
+ " --json Output a machine-readable receipt",
3381
+ "",
3382
+ "EXAMPLES",
3383
+ ' $ gjc ultragoal classify-blocker --classification resolvable --evidence "failing test can be fixed autonomously"',
3384
+ ' $ gjc ultragoal classify-blocker --classification human_blocked --evidence "user must provide production API credentials" --goal-id G001',
3385
+ "",
3386
+ ].join("\n");
3387
+ }
3322
3388
  return [
3323
3389
  "Run native GJC Ultragoal workflow commands",
3324
3390
  "",
@@ -3333,8 +3399,9 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
3333
3399
  " review",
3334
3400
  " steer",
3335
3401
  " record-review-blockers",
3402
+ " classify-blocker",
3336
3403
  "",
3337
- "Run `gjc ultragoal checkpoint --help` or `gjc ultragoal review --help` for command-specific requirements.",
3404
+ "Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, or `gjc ultragoal classify-blocker --help` for command-specific requirements.",
3338
3405
  "",
3339
3406
  ].join("\n");
3340
3407
  }
@@ -3653,6 +3720,24 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3653
3720
  : "Recorded review blockers.\n",
3654
3721
  };
3655
3722
  }
3723
+ case "classify-blocker": {
3724
+ const event = await recordUltragoalBlockerClassification({
3725
+ cwd,
3726
+ classification: (flagValue(args, "--classification") ?? "") as UltragoalBlockerClassification,
3727
+ evidence: flagValue(args, "--evidence") ?? "",
3728
+ goalId: flagValue(args, "--goal-id"),
3729
+ });
3730
+ return {
3731
+ status: 0,
3732
+ stdout: json
3733
+ ? renderCliWriteReceipt({
3734
+ ok: true,
3735
+ event: "blocker_classified",
3736
+ classification: event.classification,
3737
+ })
3738
+ : `Recorded blocker classification: ${String(event.classification)}.\n`,
3739
+ };
3740
+ }
3656
3741
  default:
3657
3742
  return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };
3658
3743
  }
@@ -3670,6 +3755,7 @@ const RECONCILE_COMMANDS = new Set([
3670
3755
  "steer",
3671
3756
  "record-review-blockers",
3672
3757
  "review",
3758
+ "classify-blocker",
3673
3759
  ]);
3674
3760
 
3675
3761
  /**
@@ -1443,7 +1443,8 @@
1443
1443
  "appliesToVerbs": [
1444
1444
  "checkpoint",
1445
1445
  "record-review-blockers",
1446
- "steer"
1446
+ "steer",
1447
+ "classify-blocker"
1447
1448
  ],
1448
1449
  "name": "evidence",
1449
1450
  "required": true,
@@ -1471,6 +1472,25 @@
1471
1472
  "name": "goal-id",
1472
1473
  "type": "string"
1473
1474
  },
1475
+ {
1476
+ "appliesToVerbs": [
1477
+ "classify-blocker"
1478
+ ],
1479
+ "name": "goal-id",
1480
+ "type": "string"
1481
+ },
1482
+ {
1483
+ "appliesToVerbs": [
1484
+ "classify-blocker"
1485
+ ],
1486
+ "enumValues": [
1487
+ "human_blocked",
1488
+ "resolvable"
1489
+ ],
1490
+ "name": "classification",
1491
+ "required": true,
1492
+ "type": "enum"
1493
+ },
1474
1494
  {
1475
1495
  "appliesToVerbs": [
1476
1496
  "steer"
@@ -1570,7 +1590,8 @@
1570
1590
  "review",
1571
1591
  "checkpoint",
1572
1592
  "record-review-blockers",
1573
- "steer"
1593
+ "steer",
1594
+ "classify-blocker"
1574
1595
  ],
1575
1596
  "name": "json",
1576
1597
  "type": "boolean"
@@ -1651,6 +1672,10 @@
1651
1672
  "name": "steer",
1652
1673
  "surface": "command-positional"
1653
1674
  },
1675
+ {
1676
+ "name": "classify-blocker",
1677
+ "surface": "command-positional"
1678
+ },
1654
1679
  {
1655
1680
  "name": "graph",
1656
1681
  "planned": true,