@gajae-code/coding-agent 0.7.9 → 0.7.11

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 (123) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/dist/types/config/keybindings.d.ts +11 -5
  4. package/dist/types/config/settings-schema.d.ts +10 -1
  5. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  6. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +10 -0
  8. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  9. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  11. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
  12. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  13. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  14. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  15. package/dist/types/modes/components/skill-message.d.ts +1 -0
  16. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  17. package/dist/types/modes/controllers/input-controller.d.ts +4 -0
  18. package/dist/types/modes/interactive-mode.d.ts +1 -0
  19. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  20. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
  21. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  22. package/dist/types/modes/types.d.ts +1 -0
  23. package/dist/types/notifications/html-format.d.ts +2 -0
  24. package/dist/types/notifications/recent-activity.d.ts +5 -1
  25. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  26. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  27. package/dist/types/notifications/threaded-render.d.ts +1 -0
  28. package/dist/types/sdk.d.ts +1 -1
  29. package/dist/types/session/agent-session.d.ts +8 -5
  30. package/dist/types/slash-commands/types.d.ts +2 -0
  31. package/dist/types/tools/ask.d.ts +2 -0
  32. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  33. package/dist/types/web/search/index.d.ts +4 -2
  34. package/dist/types/web/search/provider.d.ts +14 -0
  35. package/dist/types/web/search/providers/utils.d.ts +42 -13
  36. package/dist/types/web/search/render.d.ts +1 -0
  37. package/package.json +7 -7
  38. package/src/cli/update-cli.ts +24 -0
  39. package/src/cli/web-search-cli.ts +5 -7
  40. package/src/cli.ts +1 -1
  41. package/src/commands/launch.ts +1 -1
  42. package/src/commands/ralplan.ts +1 -0
  43. package/src/config/keybindings.ts +14 -5
  44. package/src/config/model-profiles.ts +16 -0
  45. package/src/config/settings-schema.ts +11 -1
  46. package/src/coordinator-mcp/server.ts +9 -3
  47. package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
  48. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  49. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  50. package/src/defaults/gjc-defaults.ts +16 -0
  51. package/src/extensibility/extensions/runner.ts +2 -1
  52. package/src/extensibility/extensions/types.ts +11 -0
  53. package/src/gjc-runtime/launch-tmux.ts +209 -26
  54. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  55. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  56. package/src/gjc-runtime/state-runtime.ts +8 -5
  57. package/src/gjc-runtime/state-writer.ts +5 -5
  58. package/src/gjc-runtime/ultragoal-guard.ts +139 -19
  59. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  60. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  61. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  62. package/src/hooks/skill-state.ts +33 -73
  63. package/src/internal-urls/docs-index.generated.ts +6 -4
  64. package/src/main.ts +2 -2
  65. package/src/migrate/executor.ts +23 -6
  66. package/src/modes/components/custom-editor.ts +8 -0
  67. package/src/modes/components/hook-selector.ts +21 -3
  68. package/src/modes/components/model-selector.ts +6 -2
  69. package/src/modes/components/settings-selector.ts +47 -5
  70. package/src/modes/components/skill-message.ts +18 -10
  71. package/src/modes/components/status-line/presets.ts +11 -0
  72. package/src/modes/controllers/command-controller.ts +23 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +18 -2
  74. package/src/modes/controllers/input-controller.ts +41 -9
  75. package/src/modes/interactive-mode.ts +16 -7
  76. package/src/modes/prompt-action-autocomplete.ts +23 -1
  77. package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
  78. package/src/modes/slash-command-visibility.ts +9 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  81. package/src/modes/utils/ui-helpers.ts +76 -3
  82. package/src/notifications/html-format.ts +75 -1
  83. package/src/notifications/index.ts +12 -1
  84. package/src/notifications/recent-activity.ts +32 -8
  85. package/src/notifications/telegram-daemon-control.ts +9 -0
  86. package/src/notifications/telegram-daemon.ts +68 -25
  87. package/src/notifications/telegram-reference.ts +36 -14
  88. package/src/notifications/threaded-render.ts +14 -6
  89. package/src/prompts/agents/architect.md +7 -4
  90. package/src/prompts/agents/critic.md +15 -6
  91. package/src/prompts/agents/planner.md +8 -3
  92. package/src/prompts/system/system-prompt.md +5 -1
  93. package/src/prompts/tools/bash.md +1 -1
  94. package/src/sdk.ts +6 -11
  95. package/src/session/agent-session.ts +60 -17
  96. package/src/slash-commands/builtin-registry.ts +30 -10
  97. package/src/slash-commands/types.ts +2 -0
  98. package/src/tools/ask.ts +136 -16
  99. package/src/tools/bash.ts +24 -8
  100. package/src/utils/cmux-workspace.ts +233 -0
  101. package/src/utils/title-generator.ts +2 -0
  102. package/src/web/kagi.ts +1 -1
  103. package/src/web/parallel.ts +2 -2
  104. package/src/web/search/index.ts +137 -52
  105. package/src/web/search/provider.ts +80 -0
  106. package/src/web/search/providers/anthropic.ts +1 -1
  107. package/src/web/search/providers/brave.ts +1 -1
  108. package/src/web/search/providers/codex.ts +1 -1
  109. package/src/web/search/providers/duckduckgo.ts +1 -1
  110. package/src/web/search/providers/exa.ts +1 -1
  111. package/src/web/search/providers/gemini.ts +7 -3
  112. package/src/web/search/providers/jina.ts +1 -1
  113. package/src/web/search/providers/kimi.ts +6 -1
  114. package/src/web/search/providers/openai-compatible.ts +1 -1
  115. package/src/web/search/providers/parallel.ts +1 -1
  116. package/src/web/search/providers/perplexity.ts +2 -2
  117. package/src/web/search/providers/searxng.ts +1 -1
  118. package/src/web/search/providers/synthetic.ts +1 -1
  119. package/src/web/search/providers/tavily.ts +1 -1
  120. package/src/web/search/providers/utils.ts +67 -17
  121. package/src/web/search/providers/xai.ts +1 -1
  122. package/src/web/search/providers/zai.ts +1 -1
  123. package/src/web/search/render.ts +3 -0
@@ -11,7 +11,7 @@ Use when the user asks for `ultragoal`, `create-goals`, `complete-goals`, durabl
11
11
 
12
12
  ## Purpose
13
13
 
14
- `ultragoal` turns a brief into repo-native artifacts and then drives a GJC goal safely through the unified `goal` tool. New plans default to a stable pointer-style aggregate GJC goal for the whole durable plan in `.gjc/_session-{sessionid}/ultragoal/goals.json`, including later accepted/appended stories under the original brief constraints, while GJC tracks G001/G002 story progress in the ledger. Ultragoal does not require any `/goal` slash-command between runs. For back-to-back ultragoal runs in one session/thread, call `goal({"op":"drop"})` only when `goal({"op":"get"})` still reports an active aggregate; then call `goal({"op":"create"})`. The goal tool stays armed across drop so the next create works in-session, and no slash-command cleanup exists or is required.
14
+ `ultragoal` turns a brief into repo-native durable artifacts and then drives execution through the unified `goal` tool as a UX bridge only. `goals.json` is the canonical source of goal identity and state; `ledger.jsonl` is the canonical proof stream for checkpoints, receipts, blockers, steering, and reviews. The inline `goal` tool and goal-mode-request create-bridge exist only to keep the agent's interactive loop focused on the current aggregate or story objective. Completion is verified purely from durable `goals.json` plus fresh `ledger.jsonl` receipts, never from inline goal state. The agent, not the CLI or hooks, calls `goal({"op":"complete"})` or `goal({"op":"drop"})` after durable run completion or cleanup; CLI commands and hooks never mutate goal state.
15
15
 
16
16
  - `.gjc/_session-{sessionid}/ultragoal/brief.md`
17
17
  - `.gjc/_session-{sessionid}/ultragoal/goals.json`
@@ -36,7 +36,7 @@ gjc ultragoal complete-goals
36
36
  gjc ultragoal complete-goals --retry-failed
37
37
  gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --quality-gate-json <quality-gate-json-or-path>
38
38
  gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"
39
- gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve final review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --gjc-goal-json <active-goal-get-json-or-path>
39
+ gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve final review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>"
40
40
  ```
41
41
 
42
42
  Use these exact goal-tool calls for the inline goal state:
@@ -50,7 +50,7 @@ goal({"op":"resume"})
50
50
  ```
51
51
  `drop` clears the active goal without exiting goal mode; `resume` reactivates a paused goal.
52
52
 
53
- Complete checkpoints source the active GJC goal snapshot from the current session when `--gjc-goal-json` is omitted. Use an explicit `--gjc-goal-json` only as an override; supplied values are still strictly validated and must be active goal-mode snapshots, not `.gjc/ultragoal/goals.json` records.
53
+ Durable completion is single-source: `goals.json` defines which goals exist and their required state, while `ledger.jsonl` provides the receipt proof used to verify completion. Inline goal state is UX-only and is reconciled by the agent after durable completion, not by CLI checkpoints or hooks.
54
54
 
55
55
  ## Create goals
56
56
 
@@ -98,13 +98,13 @@ Loop until `gjc ultragoal status` reports all goals complete:
98
98
  5. Complete the current GJC story only.
99
99
  6. Run a completion audit against the story objective and real artifacts/tests.
100
100
  7. Before any `--status complete` checkpoint, run the mandatory final cleanup/review gate below. In aggregate mode, do **not** call `goal({"op":"complete"})` for intermediate stories; checkpoint each story while the aggregate objective is still `active`. On the final story, create the final aggregate receipt first; only after that receipt exists may `goal({"op":"complete"})` run.
101
- 8. Checkpoint the durable ledger. Complete checkpoints require `--quality-gate-json`; the runtime sources the active GJC goal snapshot from current session state when `--gjc-goal-json` is omitted, and rejects any explicitly supplied bad snapshot:
101
+ 8. Checkpoint the durable ledger. Complete checkpoints require `--quality-gate-json` only:
102
102
  `gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --quality-gate-json <quality-gate-json-or-path>`
103
103
  A successful complete checkpoint is story completion, not automatic run completion. Read the checkpoint output: when it prints `Next ultragoal goal: <id>`, continue that active story under the same aggregate GJC goal; when it prints `All ultragoal goals are complete`, the durable run is terminal. `gjc ultragoal complete-goals` remains the supported manual next-story command if continuation output was missed.
104
104
  9. If blocked or failed, checkpoint failure:
105
105
  `gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"`
106
106
  10. For legacy per-story completed-goal blockers, preserve the non-terminal blocker with:
107
- `gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks goal create in this thread>" --gjc-goal-json <goal-get-json-or-path>`
107
+ `gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks goal create in this thread>"`
108
108
  11. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
109
109
 
110
110
  ## Blocker triage and pause discipline
@@ -167,19 +167,36 @@ Ultragoal execution should use GJC's bundled role-agent roster when a durable st
167
167
  - Use `architect` for read-only architecture and code-review lanes, including `CLEAR` / `WATCH` / `BLOCK` status.
168
168
  - Use `critic` for read-only plan or handoff critique before execution proceeds.
169
169
 
170
+ ### Mandatory implementation delegation on big scope
171
+
172
+ When a story's implementation scope is **big enough**, the Ultragoal leader MUST delegate the implementation to one or more `executor` subagents instead of writing the code inline itself. This is a hard requirement, not a preference: solo inline implementation of a big-scope story is a gate violation, and the completion cleanup/review gate must treat missing delegation on a big-scope story as a blocker.
173
+
174
+ A story's implementation scope is **big enough** to force delegation when any of the following hold:
175
+
176
+ - It spans **3+ files** or **2+ cleanly separable surfaces/modules** that can be implemented against bounded, independent acceptance criteria.
177
+ - It is estimated at **~200+ lines of net implementation change**, or is otherwise large enough that a single inline pass would crowd out the leader's checkpoint/verification duties.
178
+ - It decomposes into **independent slices** that can proceed in parallel without shared-file contention.
179
+ - The leader has already made **2+ inline edit passes** on the same story and implementation is still materially incomplete.
180
+
181
+ Forced-delegation rules:
182
+
183
+ - Split the story into cleanly separable slices, give each `executor` bounded targets and explicit acceptance criteria, and keep checkpoint/goal-state ownership in the leader.
184
+ - Prefer **parallel** `executor` subagents for independent slices; sequence only slices with a real dependency.
185
+ - If a big-scope story cannot be cleanly split, record the reason as a durable ledger note and delegate the whole implementation to a single `executor` rather than doing it inline; the leader still owns verification.
186
+ - Small, atomic, single-file changes below these thresholds stay with the leader — do not over-delegate trivial work.
187
+ - After integrating delegated slices, run `architect` / `critic` review lanes; worker agents never mutate `.gjc/_session-{sessionid}/ultragoal` or call goal tools.
188
+
170
189
  When delegating with native subagents, an await timeout only limits the leader's wait. It is not subagent failure evidence and must not be used as a cancellation reason; inspect or continue independent work, and cancel only when the subagent has actually failed, gone off-track, or become unrecoverably wrong.
171
190
 
172
191
  If an Ultragoal request has no approved plan or consensus artifact, run `ralplan` first and preserve its PRD, test spec, role roster, and verification guidance in the Ultragoal ledger. Do not silently substitute ad-hoc execution for missing planning.
173
192
 
174
193
  The Ultragoal leader owns `.gjc/_session-{sessionid}/ultragoal/goals.json` and `.gjc/_session-{sessionid}/ultragoal/ledger.jsonl`. Role agents return implementation/review evidence; they do not checkpoint Ultragoal or mutate goal state.
175
194
 
176
- For large subgoals with independent slices, the Ultragoal leader must spawn parallel `executor` subagents instead of doing serial solo work. Split only cleanly separable files/surfaces, give each executor bounded targets and acceptance criteria, and keep checkpoint ownership in the leader. Use `architect` / `critic` review lanes after integration; do not let worker agents mutate `.gjc/_session-{sessionid}/ultragoal` or call goal tools.
177
-
178
195
  ## Use Ultragoal and Team together
179
196
 
180
197
  Use ultragoal and team together for a durable Ultragoal story that benefits from one visible tmux worker session. Ultragoal remains leader-owned: `.gjc/_session-{sessionid}/ultragoal/goals.json` stores the story plan and `.gjc/_session-{sessionid}/ultragoal/ledger.jsonl` stores checkpoints. Team is the single-worker tmux execution engine and returns task/evidence status to the leader.
181
198
 
182
- The leader checkpoints Ultragoal from Team evidence; the runtime uses the active current-session GJC goal snapshot unless an explicit `--gjc-goal-json` override is supplied:
199
+ The leader checkpoints Ultragoal from Team evidence plus the current-session GJC goal snapshot; durable state remains leader-owned in `goals.json` and `ledger.jsonl`:
183
200
 
184
201
  ```sh
185
202
  gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<team evidence mentioning .gjc/_session-{sessionid}/ultragoal and <id>>" --quality-gate-json <quality-gate-json-or-path>
@@ -219,10 +236,10 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
219
236
  8. Run a final code review pass and fold it into the strict quality gate. Clean means `architectReview.architectureStatus`, `architectReview.productStatus`, and `architectReview.codeStatus` are all `"CLEAR"`, `architectReview.recommendation` is `"APPROVE"`, executor QA statuses are `"passed"`, iteration is `"passed"` with `fullRerun: true`, every evidence field is non-empty, every required matrix row is present, and every blockers array is empty. `COMMENT`, `WATCH`, `REQUEST CHANGES`, `BLOCK`, missing evidence, missing or shallow matrix rows, plan/code mismatches, or non-empty blockers are non-clean.
220
237
  9. If any lane finds an issue, do **not** checkpoint `complete` and do **not** call `goal({"op":"complete"})`. Record durable blocker work instead:
221
238
  ```sh
222
- gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve verification blockers" --objective "<blocker-resolution objective>" --evidence "<architect/executor findings>" --gjc-goal-json <active-goal-get-json-or-path>
239
+ gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve verification blockers" --objective "<blocker-resolution objective>" --evidence "<architect/executor findings>"
223
240
  ```
224
241
  10. Complete or steer through the blocker story, then rerun the full blocking verification loop. Repeat until all verifier lanes are clean.
225
- 11. Only after the loop is clean, checkpoint the story as complete with a structured quality gate and current-session active goal snapshot. The checkpoint creates a receipt; `goals.json.status` alone is not proof. In aggregate mode, the final aggregate receipt must exist before `goal({"op":"complete"})` is allowed.
242
+ 11. Only after the loop is clean, checkpoint the story as complete with a structured quality gate. The checkpoint creates a receipt in `ledger.jsonl`; `goals.json.status` alone is not proof. In aggregate mode, the final aggregate receipt must exist before the agent calls `goal({"op":"complete"})` to reconcile the inline UX goal state.
226
243
 
227
244
  While an Ultragoal run is active, the `ask` tool is blocked for all agents. Record unresolved review decisions as durable blockers with `gjc ultragoal record-review-blockers` instead of prompting interactively.
228
245
 
@@ -235,7 +252,7 @@ The native `checkpoint --status complete` command rejects missing or shallow gat
235
252
  "productStatus": "CLEAR",
236
253
  "codeStatus": "CLEAR",
237
254
  "recommendation": "APPROVE",
238
- "evidence": "architect review synthesis with architecture/product/code coverage",
255
+ "evidence": "architect review synthesis across architecture/product/code",
239
256
  "commands": ["architect review command or agent evidence id"],
240
257
  "blockers": []
241
258
  },
@@ -247,116 +264,22 @@ The native `checkpoint --status complete` command rejects missing or shallow gat
247
264
  "e2eCommands": ["bun test:e2e"],
248
265
  "redTeamCommands": ["bun test:red-team"],
249
266
  "artifactRefs": [
250
- {
251
- "id": "browser-run",
252
- "kind": "browser-automation",
253
- "path": "artifacts/browser-run.json",
254
- "description": "valid automation transcript with actions, monotonic timestamps, and selectors"
255
- },
256
- {
257
- "id": "gui-screenshot",
258
- "kind": "screenshot",
259
- "path": "artifacts/gui-screenshot.png",
260
- "description": "non-uniform screenshot evidence for the GUI/web result"
261
- },
262
- {
263
- "id": "cli-replay",
264
- "kind": "command-replay",
265
- "path": "artifacts/cli-replay.json",
266
- "description": "artifact file containing argv-only CLI replay JSON: schemaVersion 1, kind cli-replay, replaySafe true, allowlisted command such as bun/node --version or deterministic bun/node -e console.log(...), recordedStdout"
267
- },
268
- {
269
- "id": "adversarial-report",
270
- "kind": "failure-mode-test",
271
- "path": "artifacts/adversarial-report.txt",
272
- "description": "boundary, property, adversarial, or failure-mode result"
273
- },
274
- {
275
- "id": "api-package-report",
276
- "kind": "api-package-test-report",
277
- "path": "artifacts/api-package-report.txt",
278
- "description": "API/package consumer or endpoint verification output"
279
- },
280
- {
281
- "id": "algorithm-report",
282
- "kind": "property-test-report",
283
- "path": "artifacts/algorithm-report.txt",
284
- "description": "Algorithm/math property, boundary, or invariant verification output"
285
- }
267
+ { "id": "<ref-id>", "kind": "<surface-appropriate kind; see step 6>", "path": "artifacts/<file>", "description": "live-surface evidence" }
286
268
  ],
287
269
  "contractCoverage": [
288
- {
289
- "id": "contract-goal",
290
- "contractRef": "approved plan/spec/acceptance criterion or user-facing contract id",
291
- "obligation": "required behavior from the approved contract",
292
- "status": "covered",
293
- "surfaceEvidenceRefs": ["surface-gui"],
294
- "adversarialCaseRefs": ["case-invalid-input"]
295
- },
296
- {
297
- "id": "contract-out-of-scope",
298
- "contractRef": "contract intentionally outside this story",
299
- "obligation": "explicitly omitted approved-contract surface",
300
- "status": "not_applicable",
301
- "reason": "why this contract does not apply to the current story"
302
- }
270
+ { "id": "<id>", "contractRef": "<approved contract id>", "obligation": "<required behavior>", "status": "covered", "surfaceEvidenceRefs": ["<surface-id>"], "adversarialCaseRefs": ["<case-id>"] }
303
271
  ],
304
272
  "surfaceEvidence": [
305
- {
306
- "id": "surface-gui",
307
- "contractRef": "user-facing surface or public interface under test",
308
- "surface": "gui|web|cli|api|package|algorithm|math|native|desktop|tui",
309
- "invocation": "real browser action, CLI command, API/package consumer call, or algorithm/property check",
310
- "verdict": "passed",
311
- "artifactRefs": ["browser-run", "gui-screenshot"]
312
- },
313
- {
314
- "id": "surface-cli",
315
- "contractRef": "CLI or command-line interface under test",
316
- "surface": "cli",
317
- "invocation": "argv replay executed by the Ultragoal runtime",
318
- "verdict": "passed",
319
- "artifactRefs": ["cli-replay"]
320
- },
321
- {
322
- "id": "surface-api",
323
- "contractRef": "API/package public interface under test",
324
- "surface": "api/package",
325
- "invocation": "real endpoint call, package consumer call, or schema contract check",
326
- "verdict": "passed",
327
- "artifactRefs": ["api-package-report"]
328
- },
329
- {
330
- "id": "surface-algorithm",
331
- "contractRef": "algorithm/math invariant under test",
332
- "surface": "algorithm/math",
333
- "invocation": "property, boundary, or invariant test run",
334
- "verdict": "passed",
335
- "artifactRefs": ["algorithm-report"]
336
- },
337
- {
338
- "id": "surface-out-of-scope",
339
- "contractRef": "surface intentionally outside this story",
340
- "surface": "gui|web|cli|api|package|algorithm|math|native|desktop|tui",
341
- "status": "not_applicable",
342
- "reason": "why this surface does not apply to the current story"
343
- }
273
+ { "id": "<surface-id>", "contractRef": "<surface under test>", "surface": "gui|web|cli|api|package|algorithm|math|native|desktop|tui", "invocation": "<real invocation>", "verdict": "passed", "artifactRefs": ["<ref-id>"] }
344
274
  ],
345
275
  "adversarialCases": [
346
- {
347
- "id": "case-invalid-input",
348
- "contractRef": "approved plan/spec/acceptance criterion or user-facing contract id",
349
- "scenario": "boundary/property/adversarial/failure-mode input or user action",
350
- "expectedBehavior": "contract-required rejection, handling, or invariant preservation",
351
- "verdict": "passed",
352
- "artifactRefs": ["adversarial-report"]
353
- }
276
+ { "id": "<case-id>", "contractRef": "<approved contract id>", "scenario": "<boundary/adversarial input>", "expectedBehavior": "<required handling>", "verdict": "passed", "artifactRefs": ["<ref-id>"] }
354
277
  ],
355
278
  "blockers": []
356
279
  },
357
280
  "iteration": {
358
281
  "status": "passed",
359
- "evidence": "blockers were absent or resolved and the full verification loop was rerun cleanly",
282
+ "evidence": "blockers absent or resolved and the full loop was rerun cleanly",
360
283
  "fullRerun": true,
361
284
  "rerunCommands": ["bun test:e2e", "bun test:red-team"],
362
285
  "blockers": []
@@ -364,6 +287,8 @@ The native `checkpoint --status complete` command rejects missing or shallow gat
364
287
  }
365
288
  ```
366
289
 
290
+ Provide one `artifactRefs` entry per live surface actually exercised, using the surface-appropriate `kind` and evidence rules from steps 6–7 above; the CLI rejects missing or shallow gates. `status: "not_applicable"` rows are allowed only in `contractCoverage` and `surfaceEvidence` and each requires `contractRef` plus `reason`.
291
+
367
292
  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\")"],"recordedStdout":"ultragoal-cli-ok\n"}`. Use `replayExempt` only for audited unsafe/non-deterministic invocations, with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs`. `reason` must be substantive and audited, `approvedBy` must identify the verifier, and `fallbackArtifactRefs` must reference same-surface structurally valid fallback artifacts. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
368
293
 
369
294
  ## Review mode
@@ -392,6 +317,6 @@ The skill tool then dispatches `/skill:ralplan` or `/skill:deep-interview` same-
392
317
  - For back-to-back ultragoal runs in the same session/thread, when `goal({"op":"get"})` still reports an active aggregate, call `goal({"op":"drop"})` before `goal({"op":"create"})`; when no active goal exists or the prior aggregate is already complete or dropped, call `goal({"op":"create"})` directly. The goal tool remains callable across drop; no slash-command cleanup exists or is required.
393
318
  - Never call `goal({"op":"create"})` when `goal({"op":"get"})` reports a different active goal.
394
319
  - Never call `goal({"op":"complete"})` unless the aggregate run or legacy per-story goal is actually complete.
395
- - In aggregate mode, intermediate and final story checkpoints require a matching `active` GJC goal snapshot; omitted complete-checkpoint `--gjc-goal-json` reads that snapshot from current session state, and the final story checkpoint creates the final aggregate receipt before `goal({"op":"complete"})` may reconcile the inline goal state.
396
- - Completion checkpoints require read-only goal snapshot reconciliation: omit `--gjc-goal-json` to use current session state, or pass an explicit JSON/path override that remains strictly validated; shell commands and hooks must not mutate goal state.
320
+ - 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"})`.
321
+ - 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.
397
322
  - Treat `ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
@@ -44,6 +44,13 @@ export type DefaultGjcDefinition = DefaultGjcSkillDefinition | DefaultGjcSkillFr
44
44
  export interface InstallDefaultGjcDefinitionsOptions {
45
45
  check?: boolean;
46
46
  force?: boolean;
47
+ /**
48
+ * Only rewrite default definition files that already exist on disk but whose
49
+ * content differs from the embedded defaults. Files that are absent are left
50
+ * absent (status "missing"). Used by `gjc update` to refresh opted-in copies
51
+ * without materializing new on-disk copies for users who never installed them.
52
+ */
53
+ refreshOnly?: boolean;
47
54
  targetRoot?: string;
48
55
  }
49
56
 
@@ -160,6 +167,15 @@ export async function installDefaultGjcDefinitions(
160
167
 
161
168
  if (options.check) {
162
169
  status = existing === undefined ? "missing" : existing === definition.content ? "matching" : "different";
170
+ } else if (options.refreshOnly) {
171
+ if (existing === undefined) {
172
+ status = "missing";
173
+ } else if (existing === definition.content) {
174
+ status = "matching";
175
+ } else {
176
+ await Bun.write(destination, definition.content);
177
+ status = "written";
178
+ }
163
179
  } else if (existing !== undefined && !options.force) {
164
180
  status = "skipped";
165
181
  } else {
@@ -347,7 +347,8 @@ export class ExtensionRunner {
347
347
  "ctrl+t",
348
348
  "ctrl+g",
349
349
  "shift+tab",
350
- "shift+ctrl+p",
350
+ "alt+n",
351
+ "alt+shift+n",
351
352
  "ctrl+enter",
352
353
  "alt+enter",
353
354
  "escape",
@@ -136,6 +136,17 @@ export interface ExtensionUIDialogOptions {
136
136
  optionLabel: string;
137
137
  onSubmit: (text: string) => void;
138
138
  };
139
+ /**
140
+ * Inline free-text input for a non-answer clarification action. It is
141
+ * separate from `customInput` so callers can distinguish "ask about these
142
+ * choices" from "provide my own answer". Empty/whitespace-only submissions
143
+ * may be rejected by the selector when `allowEmpty` is false.
144
+ */
145
+ clarificationInput?: {
146
+ optionLabel: string;
147
+ onSubmit: (text: string) => void;
148
+ allowEmpty?: boolean;
149
+ };
139
150
  }
140
151
 
141
152
  /** Raw terminal input listener for extensions. */
@@ -37,6 +37,7 @@ export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
37
37
  export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
38
38
  export const GJC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
39
39
  export const GJC_PSMUX_PROFILE_FORCE_ENV = "GJC_PSMUX_PROFILE_FORCE";
40
+ const WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS = 100;
40
41
  const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
41
42
 
42
43
  type LaunchPolicy = "direct" | "tmux";
@@ -44,6 +45,8 @@ type LaunchPolicy = "direct" | "tmux";
44
45
  interface TtyState {
45
46
  stdin: boolean;
46
47
  stdout: boolean;
48
+ columns?: number;
49
+ rows?: number;
47
50
  }
48
51
 
49
52
  export interface TmuxLaunchContext {
@@ -57,6 +60,7 @@ export interface TmuxLaunchContext {
57
60
  tty?: TtyState;
58
61
  spawnSync?: TmuxSpawnSync;
59
62
  tmuxAvailable?: boolean;
63
+ tmuxStatusLines?: number;
60
64
  worktreeBranch?: string | null;
61
65
  currentBranch?: string | null;
62
66
  existingBranchSessionName?: string | null;
@@ -69,6 +73,10 @@ export interface TmuxSpawnResult {
69
73
  signalCode?: string | null;
70
74
  stderr?: string;
71
75
  }
76
+ export interface TmuxTerminalSize {
77
+ columns: number;
78
+ rows: number;
79
+ }
72
80
 
73
81
  export type TmuxSpawnSync = (command: string, args: string[], options: TmuxSpawnOptions) => TmuxSpawnResult;
74
82
 
@@ -95,11 +103,14 @@ export interface TmuxLaunchPlan {
95
103
  cwd: string;
96
104
  innerCommand: string;
97
105
  newSessionArgs: string[];
106
+ initialSize?: TmuxTerminalSize;
98
107
  branch?: string | null;
99
108
  attachSessionName?: string;
100
109
  project?: string | null;
101
110
  sessionId?: string | null;
102
111
  sessionStateFile?: string | null;
112
+ isPsmux: boolean;
113
+ platform: NodeJS.Platform;
103
114
  }
104
115
 
105
116
  function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
@@ -110,7 +121,9 @@ function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolea
110
121
  }
111
122
 
112
123
  function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
113
- return Boolean(parsed.continue || parsed.resume || explicitTmuxSessionName(env));
124
+ // `--resume` belongs to the inner GJC session resolver. Let it reach main.ts so
125
+ // value-less resume can show the session picker and valued resume can honor the target.
126
+ return Boolean(parsed.continue || explicitTmuxSessionName(env));
114
127
  }
115
128
 
116
129
  function findExistingSessionForLaunch(context: {
@@ -537,6 +550,89 @@ function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
537
550
  const stderr = result.stderr?.toLowerCase() ?? "";
538
551
  return stderr.includes("eio") || stderr.includes("input/output error");
539
552
  }
553
+ function isWindowsPsmuxAttachConnectionRefused(plan: TmuxLaunchPlan, result: TmuxSpawnResult): boolean {
554
+ if (plan.platform !== "win32" || !plan.isPsmux) return false;
555
+ return result.stderr?.toLowerCase().includes("os error 10061") === true;
556
+ }
557
+ function waitForWindowsPsmuxAttachRetry(): void {
558
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS);
559
+ }
560
+ function normalizeTmuxTerminalDimension(value: number | undefined): number | undefined {
561
+ if (value === undefined || !Number.isSafeInteger(value) || value <= 0) return undefined;
562
+ return value;
563
+ }
564
+
565
+ function normalizeTmuxStatusLineCount(value: number | undefined): number {
566
+ if (value === undefined || !Number.isSafeInteger(value) || value <= 0) return 0;
567
+ return value;
568
+ }
569
+
570
+ function parseTmuxStatusLineCount(value: string): number {
571
+ const normalized = value.trim().toLowerCase();
572
+ if (normalized.length === 0 || normalized === "off" || normalized === "0") return 0;
573
+ if (normalized === "on") return 1;
574
+ const parsed = Number.parseInt(normalized, 10);
575
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1;
576
+ }
577
+
578
+ function readTmuxStatusLineCount(tmuxCommand: string, cwd: string, env: NodeJS.ProcessEnv): number {
579
+ const result = Bun.spawnSync([tmuxCommand, "show-options", "-gqv", "status"], {
580
+ cwd,
581
+ env,
582
+ stdin: "pipe",
583
+ stdout: "pipe",
584
+ stderr: "pipe",
585
+ });
586
+ if (result.exitCode !== 0) return 0;
587
+ return parseTmuxStatusLineCount(new TextDecoder().decode(result.stdout));
588
+ }
589
+
590
+ function resolveCallerTmuxTerminalSize(tty: TtyState, tmuxStatusLines = 0): TmuxTerminalSize | undefined {
591
+ if (!tty.stdout) return undefined;
592
+ const columns = normalizeTmuxTerminalDimension(tty.columns);
593
+ const rows = normalizeTmuxTerminalDimension(tty.rows);
594
+ if (columns === undefined || rows === undefined) return undefined;
595
+ const adjustedRows = Math.max(1, rows - normalizeTmuxStatusLineCount(tmuxStatusLines));
596
+ return { columns, rows: adjustedRows };
597
+ }
598
+
599
+ function buildTmuxNewSessionSizeArgs(size: TmuxTerminalSize | undefined): string[] {
600
+ return size ? ["-x", String(size.columns), "-y", String(size.rows)] : [];
601
+ }
602
+
603
+ // Ensure the freshly created window fits the terminal that ultimately attaches.
604
+ // `new-session` already starts the window (and the inner TUI) at the caller's
605
+ // captured `-x/-y` size; this step governs what happens on `attach-session`.
606
+ //
607
+ // On native tmux we must NOT reassert with `resize-window`: that command flips
608
+ // the window's `window-size` option to `manual`, pinning it to the capture-time
609
+ // dimensions and stopping `attach-session` from resizing the window to the real
610
+ // client. When the attaching terminal is larger than the capture — e.g. a GUI
611
+ // terminal that reports a smaller size before it finishes sizing — the pinned
612
+ // window stays small and tmux paints the uncovered client area with `·` fill
613
+ // (the "window smaller than client" symptom). Keeping `window-size` on `latest`
614
+ // lets tmux size the window to the attaching client (status line included).
615
+ //
616
+ // psmux (Windows) does not share tmux's `window-size` semantics, so preserve the
617
+ // historical explicit `resize-window` reassert there rather than sending an
618
+ // option its server may reject and echo into the user's pane.
619
+ function ensureCreatedTmuxWindowTracksCallerTerminal(
620
+ plan: TmuxLaunchPlan,
621
+ spawnSync: TmuxSpawnSync,
622
+ options: TmuxSpawnOptions,
623
+ ): void {
624
+ if (!plan.initialSize) return;
625
+ const target = buildGjcTmuxExactOptionTarget(plan.sessionName, { env: options.env });
626
+ if (plan.isPsmux) {
627
+ spawnSync(
628
+ plan.tmuxCommand,
629
+ ["resize-window", "-t", target, "-x", String(plan.initialSize.columns), "-y", String(plan.initialSize.rows)],
630
+ options,
631
+ );
632
+ return;
633
+ }
634
+ spawnSync(plan.tmuxCommand, ["set-window-option", "-t", target, "window-size", "latest"], options);
635
+ }
540
636
 
541
637
  export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
542
638
  const env = context.env ?? process.env;
@@ -544,7 +640,12 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
544
640
  if (!context.parsed.tmux || policy === "direct") return undefined;
545
641
  if (env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return undefined;
546
642
  const platform = context.platform ?? process.platform;
547
- const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
643
+ const tty = context.tty ?? {
644
+ stdin: Boolean(process.stdin.isTTY),
645
+ stdout: Boolean(process.stdout.isTTY),
646
+ columns: process.stdout.columns,
647
+ rows: process.stdout.rows,
648
+ };
548
649
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
549
650
 
550
651
  const cwd = context.cwd ?? process.cwd();
@@ -592,12 +693,28 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
592
693
  },
593
694
  context.rawArgs,
594
695
  );
696
+ const tmuxStatusLines =
697
+ context.tmuxStatusLines ??
698
+ (context.tmuxAvailable === undefined ? readTmuxStatusLineCount(tmuxCommand, cwd, env) : 0);
699
+ const initialSize = resolveCallerTmuxTerminalSize(tty, tmuxStatusLines);
595
700
  return {
596
701
  tmuxCommand,
702
+ isPsmux: resolvedBinary.isPsmux,
703
+ platform,
597
704
  sessionName,
598
705
  cwd,
599
706
  innerCommand,
600
- newSessionArgs: ["new-session", "-d", "-s", sessionName, "-c", cwd, innerCommand],
707
+ newSessionArgs: [
708
+ "new-session",
709
+ "-d",
710
+ ...buildTmuxNewSessionSizeArgs(initialSize),
711
+ "-s",
712
+ sessionName,
713
+ "-c",
714
+ cwd,
715
+ innerCommand,
716
+ ],
717
+ initialSize,
601
718
  branch,
602
719
  project,
603
720
  sessionId,
@@ -682,6 +799,30 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
682
799
  const rootTerminalTitle = shouldSetGjcTmuxRootTerminalTitle(context.parsed, env)
683
800
  ? buildGjcTmuxRootTerminalTitle(plan.project ?? plan.cwd, plan.branch)
684
801
  : undefined;
802
+ const buildProfileInputs = (): GjcTmuxProfileContext => ({
803
+ tmuxCommand: plan.tmuxCommand,
804
+ target: plan.sessionName,
805
+ cwd: plan.cwd,
806
+ env,
807
+ spawnSync,
808
+ branch: plan.branch,
809
+ project: plan.project,
810
+ sessionId: plan.sessionId ?? null,
811
+ sessionStateFile: plan.sessionStateFile ?? null,
812
+ version: VERSION,
813
+ });
814
+ const probeHasSession = (): TmuxSpawnResult =>
815
+ spawnSync(
816
+ plan.tmuxCommand,
817
+ ["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
818
+ probeOptions,
819
+ );
820
+ const attachCreatedSession = (): TmuxSpawnResult =>
821
+ spawnSync(
822
+ plan.tmuxCommand,
823
+ ["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
824
+ attachOptions,
825
+ );
685
826
 
686
827
  if (plan.attachSessionName) {
687
828
  applyGjcTmuxRootTerminalTitleProfile({
@@ -707,24 +848,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
707
848
  // server first, and if the race fired, retry new-session before
708
849
  // we hand the session to rename-window / applyGjcTmuxProfile so the
709
850
  // profile-tagging path always sees a registered session.
710
- const buildProfileInputs = (): GjcTmuxProfileContext => ({
711
- tmuxCommand: plan.tmuxCommand,
712
- target: plan.sessionName,
713
- cwd: plan.cwd,
714
- env,
715
- spawnSync,
716
- branch: plan.branch,
717
- project: plan.project,
718
- sessionId: plan.sessionId ?? null,
719
- sessionStateFile: plan.sessionStateFile ?? null,
720
- version: VERSION,
721
- });
722
- const probeHasSession = (): TmuxSpawnResult =>
723
- spawnSync(
724
- plan.tmuxCommand,
725
- ["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
726
- probeOptions,
727
- );
728
851
  const probeResult = probeHasSession();
729
852
  if (probeResult.exitCode !== 0) {
730
853
  const retry = spawnSync(plan.tmuxCommand, plan.newSessionArgs, newSessionOptions);
@@ -791,6 +914,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
791
914
  }
792
915
  // Recovery succeeded via retry — fall through to attach-session below.
793
916
  }
917
+ ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
794
918
  applyGjcTmuxRootTerminalTitleProfile({
795
919
  tmuxCommand: plan.tmuxCommand,
796
920
  target: plan.sessionName,
@@ -814,16 +938,75 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
814
938
  return false;
815
939
  }
816
940
  // attach-session needs PTY inherit for the user-facing attach; keep it unchanged.
817
- const attached = spawnSync(
818
- plan.tmuxCommand,
819
- ["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
820
- attachOptions,
821
- );
941
+ const attached = attachCreatedSession();
822
942
  if (attached.exitCode === 0) return true;
823
943
  if (isTmuxAttachDisconnectError(attached)) {
824
944
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
825
945
  return true;
826
946
  }
947
+ if (isWindowsPsmuxAttachConnectionRefused(plan, attached)) {
948
+ waitForWindowsPsmuxAttachRetry();
949
+ const probeAfterAttach = probeHasSession();
950
+ if (probeAfterAttach.exitCode === 0) {
951
+ const retryAttached = attachCreatedSession();
952
+ if (retryAttached.exitCode === 0) return true;
953
+ if (isTmuxAttachDisconnectError(retryAttached)) {
954
+ (context.diagnosticWriter ?? safeStderrWrite)(
955
+ formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
956
+ );
957
+ return true;
958
+ }
959
+ } else {
960
+ const retry = spawnSync(plan.tmuxCommand, plan.newSessionArgs, newSessionOptions);
961
+ const retryProbe = probeHasSession();
962
+ if (retry.exitCode === 0 && retryProbe.exitCode === 0) {
963
+ renameTmuxWindow(
964
+ plan.tmuxCommand,
965
+ windowTitle,
966
+ spawnSync,
967
+ controlOptions,
968
+ buildGjcTmuxExactSessionTarget(plan.sessionName, { env }),
969
+ );
970
+ const retryProfile = applyGjcTmuxProfile(buildProfileInputs());
971
+ const retryOwnershipFailure = retryProfile.failures.find(item =>
972
+ item.command.args.includes("@gjc-profile"),
973
+ );
974
+ if (!retryOwnershipFailure) {
975
+ ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
976
+ applyGjcTmuxRootTerminalTitleProfile({
977
+ tmuxCommand: plan.tmuxCommand,
978
+ target: plan.sessionName,
979
+ title: rootTerminalTitle,
980
+ spawnSync,
981
+ options,
982
+ });
983
+ const retryAttached = attachCreatedSession();
984
+ if (retryAttached.exitCode === 0) return true;
985
+ if (isTmuxAttachDisconnectError(retryAttached)) {
986
+ (context.diagnosticWriter ?? safeStderrWrite)(
987
+ formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
988
+ );
989
+ return true;
990
+ }
991
+ }
992
+ }
993
+ }
994
+ }
995
+ // Closing an SSH/Windows Terminal tab can make `tmux attach-session`
996
+ // exit with code 1 and no captured stderr while the tmux server correctly
997
+ // keeps the just-created session alive. Preserve that live session so the
998
+ // user can reattach instead of treating the parent client teardown as a
999
+ // launch failure.
1000
+ const attachFailureStderr = attached.stderr?.trim() ?? "";
1001
+ if (attachFailureStderr.length === 0) {
1002
+ const probeAfterAttachFailure = probeHasSession();
1003
+ if (probeAfterAttachFailure.exitCode === 0) {
1004
+ (context.diagnosticWriter ?? safeStderrWrite)(
1005
+ formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr),
1006
+ );
1007
+ return true;
1008
+ }
1009
+ }
827
1010
  cleanupCreatedTmuxSession(plan, spawnSync, options);
828
1011
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
829
1012
  return true;