@gajae-code/coding-agent 0.5.1 → 0.5.3

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 (165) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +6 -0
  4. package/dist/types/cli/setup-cli.d.ts +8 -1
  5. package/dist/types/commands/setup.d.ts +7 -0
  6. package/dist/types/config/file-lock.d.ts +24 -2
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/models-config-schema.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +62 -0
  10. package/dist/types/dap/client.d.ts +2 -1
  11. package/dist/types/edit/read-file.d.ts +6 -0
  12. package/dist/types/eval/js/context-manager.d.ts +3 -0
  13. package/dist/types/eval/js/executor.d.ts +1 -0
  14. package/dist/types/exec/bash-executor.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  19. package/dist/types/lsp/types.d.ts +2 -0
  20. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +2 -0
  22. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  23. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  24. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  25. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
  28. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  29. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  30. package/dist/types/modes/theme/theme.d.ts +1 -0
  31. package/dist/types/modes/types.d.ts +1 -1
  32. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  33. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  34. package/dist/types/runtime-mcp/types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +17 -1
  36. package/dist/types/session/artifacts.d.ts +4 -1
  37. package/dist/types/session/history-storage.d.ts +2 -2
  38. package/dist/types/session/session-manager.d.ts +10 -1
  39. package/dist/types/session/streaming-output.d.ts +5 -0
  40. package/dist/types/setup/credential-import.d.ts +79 -0
  41. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  42. package/dist/types/task/executor.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +1 -1
  44. package/dist/types/tools/bash.d.ts +1 -0
  45. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  46. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  47. package/dist/types/tools/subagent-render.d.ts +7 -1
  48. package/dist/types/tools/subagent.d.ts +21 -0
  49. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  50. package/dist/types/web/search/index.d.ts +4 -4
  51. package/dist/types/web/search/provider.d.ts +16 -20
  52. package/dist/types/web/search/providers/base.d.ts +2 -1
  53. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  54. package/dist/types/web/search/types.d.ts +14 -2
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +7 -0
  57. package/src/async/job-manager.ts +153 -39
  58. package/src/cli/args.ts +2 -0
  59. package/src/cli/fast-help.ts +2 -0
  60. package/src/cli/setup-cli.ts +138 -3
  61. package/src/commands/setup.ts +5 -1
  62. package/src/commands/ultragoal.ts +3 -1
  63. package/src/config/file-lock-gc.ts +14 -2
  64. package/src/config/file-lock.ts +63 -13
  65. package/src/config/model-profile-activation.ts +15 -3
  66. package/src/config/model-profiles.ts +15 -15
  67. package/src/config/model-registry.ts +21 -1
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +62 -0
  70. package/src/dap/client.ts +105 -64
  71. package/src/dap/session.ts +44 -7
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  73. package/src/edit/read-file.ts +19 -1
  74. package/src/eval/js/context-manager.ts +228 -65
  75. package/src/eval/js/executor.ts +2 -0
  76. package/src/eval/js/index.ts +1 -0
  77. package/src/eval/js/worker-core.ts +10 -6
  78. package/src/eval/py/executor.ts +68 -19
  79. package/src/eval/py/kernel.ts +46 -22
  80. package/src/eval/py/runner.py +68 -14
  81. package/src/exec/bash-executor.ts +49 -13
  82. package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
  83. package/src/gjc-runtime/launch-tmux.ts +3 -4
  84. package/src/gjc-runtime/ralplan-runtime.ts +174 -12
  85. package/src/gjc-runtime/state-runtime.ts +2 -1
  86. package/src/gjc-runtime/state-writer.ts +254 -7
  87. package/src/gjc-runtime/tmux-gc.ts +88 -38
  88. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  89. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  90. package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
  91. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  92. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  93. package/src/harness-control-plane/owner.ts +3 -2
  94. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  95. package/src/hooks/skill-state.ts +121 -2
  96. package/src/internal-urls/artifact-protocol.ts +10 -1
  97. package/src/internal-urls/docs-index.generated.ts +14 -10
  98. package/src/lsp/client.ts +64 -26
  99. package/src/lsp/defaults.json +1 -0
  100. package/src/lsp/index.ts +2 -1
  101. package/src/lsp/lspmux.ts +33 -9
  102. package/src/lsp/types.ts +2 -0
  103. package/src/main.ts +14 -4
  104. package/src/modes/acp/acp-agent.ts +4 -2
  105. package/src/modes/bridge/bridge-mode.ts +23 -1
  106. package/src/modes/components/assistant-message.ts +10 -2
  107. package/src/modes/components/bash-execution.ts +5 -1
  108. package/src/modes/components/eval-execution.ts +5 -1
  109. package/src/modes/components/history-search.ts +5 -2
  110. package/src/modes/components/model-selector.ts +60 -2
  111. package/src/modes/components/oauth-selector.ts +5 -0
  112. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  113. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  114. package/src/modes/components/skill-message.ts +24 -16
  115. package/src/modes/components/tool-execution.ts +6 -0
  116. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  117. package/src/modes/controllers/input-controller.ts +5 -0
  118. package/src/modes/controllers/selector-controller.ts +86 -2
  119. package/src/modes/interactive-mode.ts +11 -1
  120. package/src/modes/rpc/rpc-mode.ts +132 -18
  121. package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
  122. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  123. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  124. package/src/modes/theme/defaults/claude-code.json +100 -0
  125. package/src/modes/theme/defaults/codex.json +100 -0
  126. package/src/modes/theme/defaults/index.ts +6 -0
  127. package/src/modes/theme/defaults/opencode.json +102 -0
  128. package/src/modes/theme/theme.ts +2 -2
  129. package/src/modes/types.ts +1 -1
  130. package/src/modes/utils/ui-helpers.ts +5 -2
  131. package/src/prompts/agents/executor.md +5 -2
  132. package/src/runtime/process-lifecycle.ts +400 -0
  133. package/src/runtime-mcp/manager.ts +164 -50
  134. package/src/runtime-mcp/transports/http.ts +12 -11
  135. package/src/runtime-mcp/transports/stdio.ts +64 -38
  136. package/src/runtime-mcp/types.ts +3 -0
  137. package/src/sdk.ts +39 -1
  138. package/src/session/agent-session.ts +190 -33
  139. package/src/session/artifacts.ts +17 -2
  140. package/src/session/blob-store.ts +36 -2
  141. package/src/session/history-storage.ts +32 -11
  142. package/src/session/session-manager.ts +99 -31
  143. package/src/session/streaming-output.ts +54 -3
  144. package/src/setup/credential-import.ts +429 -0
  145. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  146. package/src/slash-commands/builtin-registry.ts +30 -3
  147. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  148. package/src/task/executor.ts +7 -1
  149. package/src/task/render.ts +18 -7
  150. package/src/tools/archive-reader.ts +10 -1
  151. package/src/tools/ask.ts +4 -2
  152. package/src/tools/bash.ts +11 -4
  153. package/src/tools/browser/tab-supervisor.ts +22 -0
  154. package/src/tools/browser.ts +38 -4
  155. package/src/tools/cron.ts +1 -1
  156. package/src/tools/read.ts +11 -12
  157. package/src/tools/sqlite-reader.ts +19 -5
  158. package/src/tools/subagent-render.ts +119 -29
  159. package/src/tools/subagent.ts +147 -7
  160. package/src/tools/ultragoal-ask-guard.ts +39 -0
  161. package/src/web/search/index.ts +25 -25
  162. package/src/web/search/provider.ts +178 -87
  163. package/src/web/search/providers/base.ts +2 -1
  164. package/src/web/search/providers/openai-compatible.ts +151 -0
  165. package/src/web/search/types.ts +47 -22
@@ -8,9 +8,10 @@ import * as fs from "node:fs";
8
8
 
9
9
  import { worktree } from "../utils/git";
10
10
  import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
11
- import { GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
11
+ import { GJC_TMUX_PROFILE_VALUE, GJC_TMUX_SESSION_PREFIX } from "./tmux-common";
12
12
  import {
13
13
  type GjcTmuxSessionStatus,
14
+ type GjcTmuxSessionsForGc,
14
15
  listTmuxSessionsForGc,
15
16
  readTmuxSessionTagsForGc,
16
17
  removeGjcTmuxSession,
@@ -18,6 +19,7 @@ import {
18
19
 
19
20
  const STORE = "tmux_sessions" as const;
20
21
  const TOCTOU_SKIP = "tmux_revalidation_failed_or_became_live";
22
+ const ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
21
23
 
22
24
  function pathExists(path: string): boolean {
23
25
  try {
@@ -64,39 +66,62 @@ async function hasLiveWorktreeForBranch(project: string, branch: string): Promis
64
66
  return entries.some(entry => branchMatches(entry.branch, branch));
65
67
  }
66
68
 
69
+ function isSessionLive(session: Pick<GjcTmuxSessionStatus, "attached" | "panePids">): boolean {
70
+ return session.attached || session.panePids.length > 0;
71
+ }
72
+
73
+ function liveRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
74
+ return {
75
+ store: STORE,
76
+ id: session.name,
77
+ path: session.project,
78
+ root: session.project,
79
+ pid_status: "alive",
80
+ status: "live",
81
+ stale: false,
82
+ removable: false,
83
+ action: "none",
84
+ reason,
85
+ detail: detail(session.project, session.branch),
86
+ };
87
+ }
88
+
89
+ function staleRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
90
+ return {
91
+ store: STORE,
92
+ id: session.name,
93
+ path: session.project,
94
+ root: session.project,
95
+ pid_status: "none",
96
+ status: "stale",
97
+ stale: true,
98
+ removable: true,
99
+ action: "none",
100
+ reason,
101
+ detail: `${detail(session.project, session.branch) ?? ""} createdAt=${session.createdAt}`.trim(),
102
+ };
103
+ }
104
+
105
+ function isOldEnoughForOrphanGc(session: GjcTmuxSessionStatus): boolean {
106
+ const createdAt = Date.parse(session.createdAt);
107
+ return Number.isFinite(createdAt) && Date.now() - createdAt >= ORPHAN_MAX_AGE_MS;
108
+ }
109
+
110
+ function isGjcOwnedOrphan(session: GjcTmuxSessionStatus): boolean {
111
+ return session.name.startsWith(GJC_TMUX_SESSION_PREFIX) || session.name === "gajae_code";
112
+ }
113
+
67
114
  async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcRecord> {
68
115
  const { name, project, branch } = session;
69
- if (!project || !branch) return unclassifiedRecord(name, "missing_project_or_branch_tag", project, branch);
70
- if (!pathExists(project)) {
71
- return {
72
- store: STORE,
73
- id: name,
74
- path: project,
75
- root: project,
76
- pid_status: "none",
77
- status: "stale",
78
- stale: true,
79
- removable: true,
80
- action: "none",
81
- reason: "project_missing",
82
- detail: detail(project, branch),
83
- };
84
- }
85
- if (!(await hasLiveWorktreeForBranch(project, branch))) {
86
- return {
87
- store: STORE,
88
- id: name,
89
- path: project,
90
- root: project,
91
- pid_status: "none",
92
- status: "stale",
93
- stale: true,
94
- removable: true,
95
- action: "none",
96
- reason: "branch_no_worktree",
97
- detail: detail(project, branch),
98
- };
116
+ if (isSessionLive(session)) return liveRecord(session, "tmux_session_attached_or_has_live_panes");
117
+ if (!project || !branch) {
118
+ if (isGjcOwnedOrphan(session) && isOldEnoughForOrphanGc(session)) {
119
+ return staleRecord(session, "metadata_less_gjc_owned_idle_orphan");
120
+ }
121
+ return unclassifiedRecord(name, "missing_project_or_branch_tag", project, branch);
99
122
  }
123
+ if (!pathExists(project)) return staleRecord(session, "project_missing");
124
+ if (!(await hasLiveWorktreeForBranch(project, branch))) return staleRecord(session, "branch_no_worktree");
100
125
  return {
101
126
  store: STORE,
102
127
  id: name,
@@ -112,9 +137,34 @@ async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcR
112
137
  };
113
138
  }
114
139
 
115
- async function revalidateRemovable(name: string, env: NodeJS.ProcessEnv): Promise<boolean> {
116
- const tags = readTmuxSessionTagsForGc(name, env);
117
- if (tags.profile !== GJC_TMUX_PROFILE_VALUE || !tags.project || !tags.branch) return false;
140
+ function classifyUntaggedSession(session: GjcTmuxSessionStatus): GcRecord {
141
+ return unclassifiedRecord(session.name, "untagged_tmux_session");
142
+ }
143
+
144
+ async function revalidateRemovable(record: GcRecord, env: NodeJS.ProcessEnv): Promise<boolean> {
145
+ const tags = readTmuxSessionTagsForGc(record.id, env);
146
+ if (
147
+ tags.createdAt &&
148
+ record.detail?.includes("createdAt=") &&
149
+ !record.detail.includes(`createdAt=${tags.createdAt}`)
150
+ ) {
151
+ return false;
152
+ }
153
+ if (tags.attached || (tags.panePids?.length ?? 0) > 0) return false;
154
+ if (tags.profile !== GJC_TMUX_PROFILE_VALUE) return false;
155
+ if (!tags.project || !tags.branch)
156
+ return (
157
+ record.reason === "metadata_less_gjc_owned_idle_orphan" &&
158
+ isGjcOwnedOrphan({
159
+ name: record.id,
160
+ attached: false,
161
+ windows: 0,
162
+ panes: 0,
163
+ panePids: [],
164
+ bindings: "",
165
+ createdAt: tags.createdAt ?? "",
166
+ })
167
+ );
118
168
  if (!pathExists(tags.project)) return true;
119
169
  return !(await hasLiveWorktreeForBranch(tags.project, tags.branch));
120
170
  }
@@ -124,7 +174,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
124
174
  async collect(ctx: GcContext): Promise<GcCollectResult> {
125
175
  const records: GcRecord[] = [];
126
176
  const errors: GcCollectResult["errors"] = [];
127
- let sessions: ReturnType<typeof listTmuxSessionsForGc>;
177
+ let sessions: GjcTmuxSessionsForGc;
128
178
  try {
129
179
  sessions = listTmuxSessionsForGc(ctx.env);
130
180
  } catch (error) {
@@ -153,8 +203,8 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
153
203
  }
154
204
  }
155
205
 
156
- for (const name of sessions.untagged) {
157
- records.push(unclassifiedRecord(name, "untagged_tmux_session"));
206
+ for (const session of sessions.untagged) {
207
+ records.push(classifyUntaggedSession(session));
158
208
  }
159
209
 
160
210
  return { records, errors };
@@ -164,7 +214,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
164
214
  return { removed: false, skipped: "not_removable_tmux_session" };
165
215
  }
166
216
  try {
167
- if (!(await revalidateRemovable(record.id, ctx.env))) {
217
+ if (!(await revalidateRemovable(record, ctx.env))) {
168
218
  return { removed: false, skipped: TOCTOU_SKIP };
169
219
  }
170
220
  removeGjcTmuxSession(record.id, ctx.env);
@@ -22,17 +22,23 @@ export interface GjcTmuxSessionStatus {
22
22
  branch?: string;
23
23
  branchSlug?: string;
24
24
  project?: string;
25
+ panePids: number[];
26
+ profile?: string;
25
27
  }
26
28
 
27
29
  export interface GjcTmuxSessionTagsForGc {
28
30
  profile?: string;
29
31
  project?: string;
30
32
  branch?: string;
33
+ branchSlug?: string;
34
+ createdAt?: string;
35
+ attached?: boolean;
36
+ panePids?: number[];
31
37
  }
32
38
 
33
39
  export interface GjcTmuxSessionsForGc {
34
40
  tagged: GjcTmuxSessionStatus[];
35
- untagged: string[];
41
+ untagged: GjcTmuxSessionStatus[];
36
42
  }
37
43
 
38
44
  function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
@@ -68,21 +74,27 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
68
74
  profile = "",
69
75
  bindings = "",
70
76
  panes = "0",
77
+ panePids = "",
71
78
  branch = "",
72
79
  branchSlug = "",
73
80
  project = "",
74
81
  ] = line.split("\t");
75
- if (!name || profile !== GJC_TMUX_PROFILE_VALUE) return null;
82
+ if (!name) return null;
76
83
  return {
77
84
  name,
78
85
  attached: parseBooleanFlag(attached),
79
86
  windows: parseNumber(windows),
80
87
  panes: parseNumber(panes),
88
+ panePids: panePids
89
+ .split(",")
90
+ .map(pid => parseNumber(pid))
91
+ .filter(pid => pid > 0),
81
92
  bindings,
82
93
  createdAt: normalizeTmuxCreatedAt(created),
83
94
  branch: branch || undefined,
84
95
  branchSlug: branchSlug || undefined,
85
96
  project: project || undefined,
97
+ profile: profile || undefined,
86
98
  };
87
99
  }
88
100
 
@@ -103,7 +115,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
103
115
 
104
116
  function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
105
117
  return runListSessions(
106
- `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
118
+ `#{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}}`,
107
119
  env,
108
120
  );
109
121
  }
@@ -115,17 +127,35 @@ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[]
115
127
  export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
116
128
  return listSessionLines(env)
117
129
  .map(parseSessionLine)
118
- .filter((session): session is GjcTmuxSessionStatus => session != null)
130
+ .filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
119
131
  .sort((a, b) => a.name.localeCompare(b.name));
120
132
  }
121
133
 
122
134
  /** @internal */
123
135
  export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
124
- const tagged = listGjcTmuxSessions(env);
136
+ const sessions = listSessionLines(env)
137
+ .map(parseSessionLine)
138
+ .filter((session): session is GjcTmuxSessionStatus => session != null);
139
+ const tagged = sessions
140
+ .filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
141
+ .sort((a, b) => a.name.localeCompare(b.name));
125
142
  const taggedNames = new Set(tagged.map(session => session.name));
143
+ const byName = new Map(sessions.map(session => [session.name, session]));
126
144
  const untagged = listRawTmuxSessionNames(env)
127
145
  .filter(name => !taggedNames.has(name))
128
- .sort((a, b) => a.localeCompare(b));
146
+ .map(
147
+ name =>
148
+ byName.get(name) ?? {
149
+ name,
150
+ attached: false,
151
+ windows: 0,
152
+ panes: 0,
153
+ panePids: [],
154
+ bindings: "",
155
+ createdAt: "",
156
+ },
157
+ )
158
+ .sort((a, b) => a.name.localeCompare(b.name));
129
159
  return { tagged, untagged };
130
160
  }
131
161
 
@@ -192,15 +222,23 @@ export function readTmuxSessionTagsForGc(
192
222
  sessionName: string,
193
223
  env: NodeJS.ProcessEnv = process.env,
194
224
  ): GjcTmuxSessionTagsForGc {
225
+ const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
195
226
  return {
196
227
  profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, env),
197
228
  project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, env),
198
229
  branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, env),
230
+ branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
231
+ createdAt: session?.createdAt,
232
+ attached: session?.attached,
233
+ panePids: session?.panePids,
199
234
  };
200
235
  }
201
236
 
202
237
  export function removeGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
203
238
  const session = statusGjcTmuxSession(sessionName, env);
239
+ if (session.attached || session.panePids.length > 0) {
240
+ throw new Error(`gjc_tmux_session_live:${sessionName}`);
241
+ }
204
242
  if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE) {
205
243
  throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
206
244
  }
@@ -32,6 +32,16 @@ export interface UltragoalGuardDiagnostic {
32
32
  goalId?: string;
33
33
  }
34
34
 
35
+ export interface UltragoalAskBlockDiagnostic {
36
+ active: boolean;
37
+ reason: string;
38
+ source: "absent" | "durable_state" | "durable_state_unreadable" | "ledger" | "goals_json";
39
+ goalsPath?: string;
40
+ ledgerPath?: string;
41
+ goalIds?: string[];
42
+ message: string;
43
+ }
44
+
35
45
  export interface CurrentGoalLike {
36
46
  objective: string;
37
47
  status?: string;
@@ -70,6 +80,48 @@ async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
70
80
  }
71
81
  }
72
82
 
83
+ function isEnoent(error: unknown): boolean {
84
+ return (
85
+ typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ENOENT"
86
+ );
87
+ }
88
+
89
+ function activeAskDiagnostic(input: {
90
+ reason: string;
91
+ source: UltragoalAskBlockDiagnostic["source"];
92
+ goalsPath?: string;
93
+ ledgerPath?: string;
94
+ goalIds?: string[];
95
+ }): UltragoalAskBlockDiagnostic {
96
+ return {
97
+ active: true,
98
+ reason: input.reason,
99
+ source: input.source,
100
+ goalsPath: input.goalsPath,
101
+ ledgerPath: input.ledgerPath,
102
+ goalIds: input.goalIds,
103
+ message: `${input.reason} Use \`gjc ultragoal record-review-blockers\` instead of asking the user.`,
104
+ };
105
+ }
106
+
107
+ function inactiveAskDiagnostic(input: {
108
+ reason: string;
109
+ source: UltragoalAskBlockDiagnostic["source"];
110
+ goalsPath?: string;
111
+ ledgerPath?: string;
112
+ goalIds?: string[];
113
+ }): UltragoalAskBlockDiagnostic {
114
+ return {
115
+ active: false,
116
+ reason: input.reason,
117
+ source: input.source,
118
+ goalsPath: input.goalsPath,
119
+ ledgerPath: input.ledgerPath,
120
+ goalIds: input.goalIds,
121
+ message: input.reason,
122
+ };
123
+ }
124
+
73
125
  function requiredGoals(plan: UltragoalPlan): UltragoalGoal[] {
74
126
  return plan.goals.filter(goal => goal.status !== "superseded");
75
127
  }
@@ -278,6 +330,109 @@ export async function readUltragoalVerificationState(input: {
278
330
  return receiptDiagnostic;
279
331
  }
280
332
 
333
+ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
334
+ const paths = getUltragoalPaths(cwd);
335
+ try {
336
+ await fs.stat(paths.dir);
337
+ } catch (error) {
338
+ if (isEnoent(error)) {
339
+ return inactiveAskDiagnostic({
340
+ reason: "No durable .gjc/ultragoal state exists.",
341
+ source: "absent",
342
+ goalsPath: paths.goalsPath,
343
+ ledgerPath: paths.ledgerPath,
344
+ });
345
+ }
346
+ return activeAskDiagnostic({
347
+ reason: `Durable .gjc/ultragoal state is present but unreadable: ${error instanceof Error ? error.message : String(error)}`,
348
+ source: "durable_state_unreadable",
349
+ goalsPath: paths.goalsPath,
350
+ ledgerPath: paths.ledgerPath,
351
+ });
352
+ }
353
+
354
+ let plan: UltragoalPlan | null;
355
+ let ledger: UltragoalLedgerEvent[];
356
+ try {
357
+ plan = await readUltragoalPlan(cwd);
358
+ ledger = await readUltragoalLedger(cwd);
359
+ } catch (error) {
360
+ return activeAskDiagnostic({
361
+ reason: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
362
+ source: "durable_state_unreadable",
363
+ goalsPath: paths.goalsPath,
364
+ ledgerPath: paths.ledgerPath,
365
+ });
366
+ }
367
+ if (!plan) {
368
+ return activeAskDiagnostic({
369
+ reason: "Durable .gjc/ultragoal state exists but goals.json is missing or empty.",
370
+ source: "durable_state_unreadable",
371
+ goalsPath: paths.goalsPath,
372
+ ledgerPath: paths.ledgerPath,
373
+ });
374
+ }
375
+
376
+ if (plan.goals.some(goal => goal.status === "review_blocked")) {
377
+ const goalIds = plan.goals.filter(goal => goal.status === "review_blocked").map(goal => goal.id);
378
+ return activeAskDiagnostic({
379
+ reason: `Ultragoal has recorded review blockers: ${goalIds.join(", ")}.`,
380
+ source: "goals_json",
381
+ goalsPath: paths.goalsPath,
382
+ ledgerPath: paths.ledgerPath,
383
+ goalIds,
384
+ });
385
+ }
386
+
387
+ const runState = getUltragoalRunCompletionState(plan);
388
+ if (runState.incompleteGoals.length > 0) {
389
+ const goalIds = runState.incompleteGoals.map(goal => goal.id);
390
+ return activeAskDiagnostic({
391
+ reason: `Ultragoal has incomplete required goals: ${goalIds.join(", ")}.`,
392
+ source: "goals_json",
393
+ goalsPath: paths.goalsPath,
394
+ ledgerPath: paths.ledgerPath,
395
+ goalIds,
396
+ });
397
+ }
398
+
399
+ const finalReceiptGoal = [...requiredGoals(plan)]
400
+ .reverse()
401
+ .find(goal => goal.completionVerification?.receiptKind === "final-aggregate");
402
+ if (!finalReceiptGoal) {
403
+ return activeAskDiagnostic({
404
+ reason: "Ultragoal aggregate completion is missing a final aggregate receipt.",
405
+ source: "durable_state",
406
+ goalsPath: paths.goalsPath,
407
+ ledgerPath: paths.ledgerPath,
408
+ goalIds: requiredGoals(plan).map(goal => goal.id),
409
+ });
410
+ }
411
+
412
+ const diagnostic = validateCompletionReceipt({
413
+ plan,
414
+ ledger,
415
+ goal: finalReceiptGoal,
416
+ receiptKind: "final-aggregate",
417
+ });
418
+ if (diagnostic.state !== "active_verified_complete") {
419
+ return activeAskDiagnostic({
420
+ reason: diagnostic.message,
421
+ source: diagnostic.state === "active_dirty_quality_gate" ? "ledger" : "durable_state",
422
+ goalsPath: paths.goalsPath,
423
+ ledgerPath: paths.ledgerPath,
424
+ goalIds: diagnostic.goalId ? [diagnostic.goalId] : undefined,
425
+ });
426
+ }
427
+ return inactiveAskDiagnostic({
428
+ reason: "Ultragoal run is verified complete.",
429
+ source: "durable_state",
430
+ goalsPath: paths.goalsPath,
431
+ ledgerPath: paths.ledgerPath,
432
+ goalIds: [finalReceiptGoal.id],
433
+ });
434
+ }
435
+
281
436
  export async function assertCanCompleteCurrentGoal(input: {
282
437
  cwd: string;
283
438
  currentGoal?: CurrentGoalLike | null;