@gajae-code/coding-agent 0.2.5 → 0.3.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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -1,9 +1,11 @@
1
1
  import * as crypto from "node:crypto";
2
- import * as fs from "node:fs/promises";
3
2
  import * as path from "node:path";
4
3
  import type { WorkflowHudSummary } from "../skill-state/active-state";
5
4
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
5
+ import { renderCliWriteReceipt } from "./cli-write-receipt";
6
6
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
7
+ import { renderUltragoalStatusMarkdown } from "./state-renderer";
8
+ import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
7
9
 
8
10
  export type UltragoalGjcGoalMode = "aggregate" | "per-story";
9
11
  export type UltragoalGoalStatus =
@@ -103,6 +105,12 @@ const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "
103
105
  const CLEAN_ARCHITECT_STATUS = "CLEAR";
104
106
  const APPROVE_RECOMMENDATION = "APPROVE";
105
107
  const PASSED_STATUS = "passed";
108
+ const NOT_APPLICABLE_STATUS = "not_applicable";
109
+ const COVERED_STATUS = "covered";
110
+ const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
111
+ const MIN_SUBSTANTIVE_EVIDENCE_WORDS = 5;
112
+ const MIN_SUBSTANTIVE_EVIDENCE_CHARS = 32;
113
+
106
114
  const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
107
115
  const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
108
116
 
@@ -143,19 +151,17 @@ function isEnoent(error: unknown): boolean {
143
151
  );
144
152
  }
145
153
 
146
- async function ensureUltragoalDir(paths: UltragoalPaths): Promise<void> {
147
- await fs.mkdir(paths.dir, { recursive: true });
148
- }
149
-
150
154
  async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLedgerEvent> {
151
155
  const paths = getUltragoalPaths(cwd);
152
- await ensureUltragoalDir(paths);
153
156
  const entry: UltragoalLedgerEvent = {
154
157
  eventId: typeof event.eventId === "string" ? event.eventId : crypto.randomUUID(),
155
158
  ...event,
156
159
  timestamp: new Date().toISOString(),
157
160
  };
158
- await fs.appendFile(paths.ledgerPath, `${JSON.stringify(entry)}\n`);
161
+ await appendJsonl(paths.ledgerPath, entry, {
162
+ cwd,
163
+ audit: { category: "ledger", verb: "append", owner: "gjc-runtime" },
164
+ });
159
165
  return entry;
160
166
  }
161
167
 
@@ -175,9 +181,14 @@ export async function readUltragoalLedger(cwd: string): Promise<UltragoalLedgerE
175
181
 
176
182
  async function writePlan(cwd: string, plan: UltragoalPlan): Promise<void> {
177
183
  const paths = getUltragoalPaths(cwd);
178
- await ensureUltragoalDir(paths);
179
- await Bun.write(paths.briefPath, `${plan.brief.trim()}\n`);
180
- await Bun.write(paths.goalsPath, `${JSON.stringify(plan, null, 2)}\n`);
184
+ await writeArtifact(paths.briefPath, `${plan.brief.trim()}\n`, {
185
+ cwd,
186
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
187
+ });
188
+ await writeJsonAtomic(paths.goalsPath, plan, {
189
+ cwd,
190
+ audit: { category: "state", verb: "write", owner: "gjc-runtime" },
191
+ });
181
192
  }
182
193
 
183
194
  function requiredUltragoalGoals(plan: UltragoalPlan): UltragoalGoal[] {
@@ -464,13 +475,58 @@ export function buildUltragoalHudSummary(
464
475
  });
465
476
  }
466
477
 
467
- function titleFromBrief(brief: string): string {
468
- const firstLine = brief
478
+ function clampTitle(title: string): string {
479
+ return title.length > 80 ? `${title.slice(0, 77)}...` : title;
480
+ }
481
+
482
+ function firstNonEmptyLine(text: string): string | undefined {
483
+ return text
469
484
  .split(/\r?\n/)
470
485
  .map(line => line.trim())
471
486
  .find(line => line.length > 0);
487
+ }
488
+
489
+ function titleFromBrief(brief: string): string {
490
+ const firstLine = firstNonEmptyLine(brief);
472
491
  if (!firstLine) return "Complete ultragoal brief";
473
- return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
492
+ return clampTitle(firstLine);
493
+ }
494
+
495
+ // A reserved, column-0 (unindented) `@goal` line opens a story. The character
496
+ // right after `@goal` must be `:`, an ASCII space or tab, or end-of-line, so
497
+ // `@goalish`, `@goals:`, `@goal-foo`, `@goal.foo`, `@goal/foo`, a non-breaking
498
+ // space, and indented or mid-line `@goal:` are all ordinary objective text and
499
+ // never delimiters.
500
+ const GOAL_DELIMITER = /^@goal(?::|[ \t]+|$)[ \t]*(.*)$/;
501
+
502
+ interface ParsedGoal {
503
+ title: string;
504
+ objective: string;
505
+ }
506
+
507
+ function parseGoalsFromBrief(brief: string): ParsedGoal[] {
508
+ const sections: { title: string; body: string[] }[] = [];
509
+ let current: { title: string; body: string[] } | undefined;
510
+ for (const line of brief.split(/\r?\n/)) {
511
+ const match = GOAL_DELIMITER.exec(line);
512
+ if (match) {
513
+ current = { title: match[1].trim(), body: [] };
514
+ sections.push(current);
515
+ continue;
516
+ }
517
+ current?.body.push(line);
518
+ }
519
+ if (sections.length === 0) {
520
+ return [{ title: titleFromBrief(brief), objective: brief.trim() }];
521
+ }
522
+ return sections.map((section, index) => {
523
+ const body = section.body.join("\n").trim();
524
+ const title = section.title || firstNonEmptyLine(body) || "";
525
+ if (!title && !body) {
526
+ throw new Error(`ultragoal @goal block ${index + 1} has no title or objective`);
527
+ }
528
+ return { title: clampTitle(title), objective: body || title };
529
+ });
474
530
  }
475
531
 
476
532
  export async function createUltragoalPlan(input: {
@@ -481,21 +537,23 @@ export async function createUltragoalPlan(input: {
481
537
  const brief = input.brief.trim();
482
538
  if (!brief) throw new Error("ultragoal brief is required");
483
539
  const now = new Date().toISOString();
540
+ // Parse the untrimmed brief so the raw-line delimiter contract holds: a
541
+ // leading-indented `@goal` on the first line must stay objective text rather
542
+ // than being promoted to column 0 by trimming.
543
+ const goals: UltragoalGoal[] = parseGoalsFromBrief(input.brief).map((goal, index) => ({
544
+ id: `G${String(index + 1).padStart(3, "0")}`,
545
+ title: goal.title,
546
+ objective: goal.objective,
547
+ status: "pending",
548
+ createdAt: now,
549
+ updatedAt: now,
550
+ }));
484
551
  const plan: UltragoalPlan = {
485
552
  version: 1,
486
553
  brief,
487
554
  gjcGoalMode: input.gjcGoalMode ?? "aggregate",
488
555
  gjcObjective: DEFAULT_ULTRAGOAL_OBJECTIVE,
489
- goals: [
490
- {
491
- id: "G001",
492
- title: titleFromBrief(brief),
493
- objective: brief,
494
- status: "pending",
495
- createdAt: now,
496
- updatedAt: now,
497
- },
498
- ],
556
+ goals,
499
557
  createdAt: now,
500
558
  updatedAt: now,
501
559
  };
@@ -565,8 +623,330 @@ function requireEmptyBlockers(value: unknown, fieldName: string): void {
565
623
  throw new Error(`qualityGate ${fieldName} must be an empty blockers array`);
566
624
  }
567
625
  }
626
+ function requireQualityGateObject(value: unknown, fieldName: string): JsonObject {
627
+ const object = qualityGateObject(value);
628
+ if (!object) throw new Error(`qualityGate ${fieldName} must be an object`);
629
+ return object;
630
+ }
568
631
 
569
- function validateCompletionQualityGate(gate: JsonObject): void {
632
+ function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
633
+ if (!Array.isArray(value) || value.length === 0) {
634
+ throw new Error(`qualityGate ${fieldName} must be a non-empty object array`);
635
+ }
636
+ return value.map((item, index) => requireQualityGateObject(item, `${fieldName}[${index}]`));
637
+ }
638
+
639
+ function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
640
+ const value = row[key];
641
+ if (typeof value !== "string" || value.trim().length === 0) {
642
+ throw new Error(`qualityGate ${fieldName}.${key} must be a non-empty string`);
643
+ }
644
+ return value.trim();
645
+ }
646
+
647
+ function optionalStatusField(row: JsonObject, fieldName: string): string | null {
648
+ if (row.status === undefined) return null;
649
+ const status = requiredStringField(row, "status", fieldName).toLowerCase();
650
+ if (status === "todo") throw new Error(`qualityGate ${fieldName}.status must not be todo`);
651
+ return status;
652
+ }
653
+
654
+ function requireProofStatus(status: string, fieldName: string): void {
655
+ if (!ACCEPTED_PROOF_STATUSES.has(status) && status !== NOT_APPLICABLE_STATUS) {
656
+ throw new Error(`qualityGate ${fieldName}.status must be covered, passed, verified, or not_applicable`);
657
+ }
658
+ }
659
+ function requireSuccessStatus(status: string, fieldName: string): void {
660
+ requireProofStatus(status, fieldName);
661
+ if (status === NOT_APPLICABLE_STATUS) {
662
+ throw new Error(`qualityGate ${fieldName}.status must be covered, passed, or verified`);
663
+ }
664
+ }
665
+
666
+ function rowOutcomeStatuses(row: JsonObject, fieldName: string): string[] {
667
+ const statuses: string[] = [];
668
+ const status = optionalStatusField(row, fieldName);
669
+ if (status) statuses.push(status);
670
+ const verdict = row.verdict;
671
+ if (typeof verdict === "string" && verdict.trim().length > 0) statuses.push(verdict.trim().toLowerCase());
672
+ const result = row.result;
673
+ if (typeof result === "string" && result.trim().length > 0) statuses.push(result.trim().toLowerCase());
674
+ if (statuses.length === 0) throw new Error(`qualityGate ${fieldName}.verdict must be a non-empty string`);
675
+ return statuses;
676
+ }
677
+
678
+ function requireSuccessfulRowOutcome(row: JsonObject, fieldName: string): void {
679
+ for (const status of rowOutcomeStatuses(row, fieldName)) {
680
+ requireSuccessStatus(status, fieldName);
681
+ }
682
+ }
683
+
684
+ function requireStringLinks(value: unknown, fieldName: string): string[] {
685
+ const strings = nonEmptyStringArray(value);
686
+ if (!strings) throw new Error(`qualityGate ${fieldName} must be a non-empty string array`);
687
+ return strings.map(item => item.trim());
688
+ }
689
+
690
+ function optionalStringLinks(row: JsonObject, key: string, fieldName: string): string[] | null {
691
+ if (row[key] === undefined) return null;
692
+ return requireStringLinks(row[key], `${fieldName}.${key}`);
693
+ }
694
+
695
+ function buildRowIdMap(rows: JsonObject[], fieldName: string): Map<string, JsonObject> {
696
+ const ids = new Map<string, JsonObject>();
697
+ for (const [index, row] of rows.entries()) {
698
+ const id = requiredStringField(row, "id", `${fieldName}[${index}]`);
699
+ if (ids.has(id)) throw new Error(`qualityGate ${fieldName} contains duplicate id ${id}`);
700
+ ids.set(id, row);
701
+ }
702
+ return ids;
703
+ }
704
+
705
+ function requireResolvedLinks(ids: string[], map: Map<string, JsonObject>, fieldName: string): void {
706
+ for (const id of ids) {
707
+ if (!map.has(id)) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
708
+ }
709
+ }
710
+ function successfulLinkedRows(ids: string[], map: Map<string, JsonObject>, fieldName: string): JsonObject[] {
711
+ const rows: JsonObject[] = [];
712
+ for (const id of ids) {
713
+ const row = map.get(id);
714
+ if (!row) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
715
+ requireSuccessfulRowOutcome(row, `${fieldName}.${id}`);
716
+ rows.push(row);
717
+ }
718
+ return rows;
719
+ }
720
+
721
+ function normalizedEvidenceKind(row: JsonObject): string {
722
+ return requiredStringField(row, "kind", "executorQa.artifactRefs[]").toLowerCase().replaceAll("_", "-");
723
+ }
724
+
725
+ function evidenceKindMatches(kind: string, words: string[]): boolean {
726
+ return words.some(word => kind.includes(word));
727
+ }
728
+
729
+ function validateSurfaceArtifactCompatibility(
730
+ surface: string,
731
+ artifactIds: string[],
732
+ artifactRefs: Map<string, JsonObject>,
733
+ fieldName: string,
734
+ ): void {
735
+ const normalizedSurface = surface.toLowerCase().replaceAll("_", "-");
736
+ const kinds = artifactIds.map(id => normalizedEvidenceKind(artifactRefs.get(id)!));
737
+ const isGuiOrWeb = ["gui", "web", "browser", "ui", "visual"].some(word => normalizedSurface.includes(word));
738
+ if (isGuiOrWeb) {
739
+ const hasBrowser = kinds.some(kind =>
740
+ evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation"]),
741
+ );
742
+ const hasVisual = kinds.some(kind => evidenceKindMatches(kind, ["screenshot", "image", "visual"]));
743
+ if (!hasBrowser || !hasVisual) {
744
+ throw new Error(
745
+ `qualityGate ${fieldName} for GUI/web surfaces must reference browser automation plus screenshot or image-verdict artifacts`,
746
+ );
747
+ }
748
+ return;
749
+ }
750
+ const surfaceFamilies: Array<{ surface: string[]; evidence: string[]; label: string }> = [
751
+ {
752
+ surface: ["cli", "terminal", "command"],
753
+ evidence: ["cli", "log", "transcript", "terminal", "command", "test-report"],
754
+ label: "CLI",
755
+ },
756
+ {
757
+ surface: ["api", "package", "library", "sdk"],
758
+ evidence: ["api", "package", "consumer", "black-box", "test-report"],
759
+ label: "API/package",
760
+ },
761
+ {
762
+ surface: ["algorithm", "math", "mathematical", "equation"],
763
+ evidence: ["property", "boundary", "edge", "adversarial", "failure", "math", "algorithm", "test-report"],
764
+ label: "algorithm/math",
765
+ },
766
+ ];
767
+ for (const family of surfaceFamilies) {
768
+ if (family.surface.some(word => normalizedSurface.includes(word))) {
769
+ if (!kinds.some(kind => evidenceKindMatches(kind, family.evidence))) {
770
+ throw new Error(
771
+ `qualityGate ${fieldName} for ${family.label} surfaces must reference compatible artifact kinds`,
772
+ );
773
+ }
774
+ return;
775
+ }
776
+ }
777
+ }
778
+
779
+ function isSubstantiveEvidence(value: unknown): boolean {
780
+ if (typeof value !== "string") return false;
781
+ const trimmed = value.trim();
782
+ if (trimmed.length < MIN_SUBSTANTIVE_EVIDENCE_CHARS) return false;
783
+ const words = trimmed.split(/\s+/).filter(word => /[a-z0-9]/i.test(word));
784
+ if (words.length < MIN_SUBSTANTIVE_EVIDENCE_WORDS) return false;
785
+ const normalized = trimmed.toLowerCase();
786
+ return !["todo", "tbd", "n/a", "na", "none", "placeholder", "empty", "stub"].includes(normalized);
787
+ }
788
+
789
+ function hasTypedVerifiedReceipt(value: unknown): boolean {
790
+ const receipt = qualityGateObject(value);
791
+ if (!receipt) return false;
792
+ const type = nonEmptyString(receipt.type) ?? nonEmptyString(receipt.kind) ?? nonEmptyString(receipt.receiptType);
793
+ const id = nonEmptyString(receipt.id) ?? nonEmptyString(receipt.receiptId) ?? nonEmptyString(receipt.ref);
794
+ const status = (nonEmptyString(receipt.status) ?? nonEmptyString(receipt.verdict) ?? "").toLowerCase();
795
+ return Boolean(type && id && (status === "verified" || status === "passed"));
796
+ }
797
+
798
+ async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
799
+ const artifactPath = nonEmptyString(value);
800
+ if (!artifactPath) return false;
801
+ const resolved = path.resolve(cwd, artifactPath);
802
+ try {
803
+ const file = Bun.file(resolved);
804
+ return (await file.exists()) && file.size > 0;
805
+ } catch (error) {
806
+ if (isEnoent(error)) return false;
807
+ throw error;
808
+ }
809
+ }
810
+
811
+ async function requireSubstantiveArtifactEvidence(cwd: string, row: JsonObject, fieldName: string): Promise<void> {
812
+ if (isSubstantiveEvidence(row.inlineEvidence) || isSubstantiveEvidence(row.evidence)) return;
813
+ if (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt)) return;
814
+ if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
815
+ throw new Error(
816
+ `qualityGate ${fieldName} must reference an existing non-empty artifact path, substantive inlineEvidence, or a typed verifiedReceipt`,
817
+ );
818
+ }
819
+
820
+ async function validateArtifactRefs(cwd: string, executorQa: JsonObject): Promise<Map<string, JsonObject>> {
821
+ const rows = requireObjectArray(executorQa.artifactRefs, "executorQa.artifactRefs");
822
+ const idMap = buildRowIdMap(rows, "executorQa.artifactRefs");
823
+ for (const [index, row] of rows.entries()) {
824
+ const fieldName = `executorQa.artifactRefs[${index}]`;
825
+ requiredStringField(row, "kind", fieldName);
826
+ requiredStringField(row, "description", fieldName);
827
+ await requireSubstantiveArtifactEvidence(cwd, row, fieldName);
828
+ }
829
+ return idMap;
830
+ }
831
+
832
+ function validateSurfaceEvidence(
833
+ executorQa: JsonObject,
834
+ artifactRefs: Map<string, JsonObject>,
835
+ ): Map<string, JsonObject> {
836
+ const rows = requireObjectArray(executorQa.surfaceEvidence, "executorQa.surfaceEvidence");
837
+ const idMap = buildRowIdMap(rows, "executorQa.surfaceEvidence");
838
+ for (const [index, row] of rows.entries()) {
839
+ const fieldName = `executorQa.surfaceEvidence[${index}]`;
840
+ const status = optionalStatusField(row, fieldName);
841
+ requiredStringField(row, "contractRef", fieldName);
842
+ if (status === NOT_APPLICABLE_STATUS) {
843
+ requiredStringField(row, "reason", fieldName);
844
+ continue;
845
+ }
846
+ const surface = requiredStringField(row, "surface", fieldName);
847
+ requireSuccessfulRowOutcome(row, fieldName);
848
+ requiredStringField(row, "invocation", fieldName);
849
+ if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
850
+ requiredStringField(row, "result", fieldName);
851
+ }
852
+ const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
853
+ requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
854
+ validateSurfaceArtifactCompatibility(surface, artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
855
+ }
856
+ return idMap;
857
+ }
858
+
859
+ function validateAdversarialCases(
860
+ executorQa: JsonObject,
861
+ artifactRefs: Map<string, JsonObject>,
862
+ ): Map<string, JsonObject> {
863
+ const rows = requireObjectArray(executorQa.adversarialCases, "executorQa.adversarialCases");
864
+ const idMap = buildRowIdMap(rows, "executorQa.adversarialCases");
865
+ for (const [index, row] of rows.entries()) {
866
+ const fieldName = `executorQa.adversarialCases[${index}]`;
867
+ const status = optionalStatusField(row, fieldName);
868
+ if (status === NOT_APPLICABLE_STATUS) {
869
+ throw new Error(`qualityGate ${fieldName}.status must not be not_applicable`);
870
+ }
871
+ requireSuccessfulRowOutcome(row, fieldName);
872
+ requiredStringField(row, "contractRef", fieldName);
873
+ requiredStringField(row, "scenario", fieldName);
874
+ requiredStringField(row, "expectedBehavior", fieldName);
875
+ if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
876
+ requiredStringField(row, "result", fieldName);
877
+ }
878
+ const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
879
+ requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
880
+ }
881
+ return idMap;
882
+ }
883
+
884
+ function validateContractCoverage(
885
+ executorQa: JsonObject,
886
+ surfaceEvidence: Map<string, JsonObject>,
887
+ adversarialCases: Map<string, JsonObject>,
888
+ artifactRefs: Map<string, JsonObject>,
889
+ ): void {
890
+ const rows = requireObjectArray(executorQa.contractCoverage, "executorQa.contractCoverage");
891
+ buildRowIdMap(rows, "executorQa.contractCoverage");
892
+ let hasSuccessfulContractCoverage = false;
893
+ for (const [index, row] of rows.entries()) {
894
+ const fieldName = `executorQa.contractCoverage[${index}]`;
895
+ requiredStringField(row, "contractRef", fieldName);
896
+ const status = optionalStatusField(row, fieldName);
897
+ if (status === NOT_APPLICABLE_STATUS) {
898
+ requiredStringField(row, "reason", fieldName);
899
+ continue;
900
+ }
901
+ requiredStringField(row, "obligation", fieldName);
902
+ if (!status) throw new Error(`qualityGate ${fieldName}.status must be a non-empty string`);
903
+ requireSuccessStatus(status, fieldName);
904
+ hasSuccessfulContractCoverage = true;
905
+ const surfaceIds = optionalStringLinks(row, "surfaceEvidenceRefs", fieldName);
906
+ const adversarialIds = optionalStringLinks(row, "adversarialCaseRefs", fieldName);
907
+ const artifactIds = optionalStringLinks(row, "artifactRefs", fieldName);
908
+ if (!surfaceIds && !adversarialIds && !artifactIds) {
909
+ throw new Error(
910
+ `qualityGate ${fieldName} must link to surfaceEvidenceRefs, adversarialCaseRefs, or artifactRefs`,
911
+ );
912
+ }
913
+ let successfulProofLinks = 0;
914
+ if (surfaceIds)
915
+ successfulProofLinks += successfulLinkedRows(
916
+ surfaceIds,
917
+ surfaceEvidence,
918
+ `${fieldName}.surfaceEvidenceRefs`,
919
+ ).length;
920
+ if (adversarialIds) {
921
+ successfulProofLinks += successfulLinkedRows(
922
+ adversarialIds,
923
+ adversarialCases,
924
+ `${fieldName}.adversarialCaseRefs`,
925
+ ).length;
926
+ }
927
+ if (artifactIds) {
928
+ requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
929
+ successfulProofLinks += artifactIds.length;
930
+ }
931
+ if (successfulProofLinks === 0) {
932
+ throw new Error(`qualityGate ${fieldName} must link to at least one successful proof row or artifact`);
933
+ }
934
+ }
935
+ if (!hasSuccessfulContractCoverage) {
936
+ throw new Error(
937
+ "qualityGate executorQa.contractCoverage must include at least one row with status covered, passed, or verified",
938
+ );
939
+ }
940
+ }
941
+
942
+ async function validateExecutorQaRedTeamEvidence(cwd: string, executorQa: JsonObject): Promise<void> {
943
+ const artifactRefs = await validateArtifactRefs(cwd, executorQa);
944
+ const surfaceEvidence = validateSurfaceEvidence(executorQa, artifactRefs);
945
+ const adversarialCases = validateAdversarialCases(executorQa, artifactRefs);
946
+ validateContractCoverage(executorQa, surfaceEvidence, adversarialCases, artifactRefs);
947
+ }
948
+
949
+ async function validateCompletionQualityGate(cwd: string, gate: JsonObject): Promise<void> {
570
950
  const codeReview = qualityGateObject(gate.codeReview);
571
951
  if (codeReview) {
572
952
  throw new Error(
@@ -611,6 +991,7 @@ function validateCompletionQualityGate(gate: JsonObject): void {
611
991
  }
612
992
  requireNonEmptyString(executorQa.evidence, "executorQa.evidence");
613
993
  requireEmptyBlockers(executorQa.blockers, "executorQa.blockers");
994
+ await validateExecutorQaRedTeamEvidence(cwd, executorQa);
614
995
  if (iteration.status !== PASSED_STATUS || iteration.fullRerun !== true) {
615
996
  throw new Error("qualityGate iteration must be passed with fullRerun true");
616
997
  }
@@ -630,7 +1011,7 @@ async function readRequiredCompletionQualityGate(cwd: string, value: string | un
630
1011
  const gate = await readStructuredValue(cwd, value);
631
1012
  const gateObject = qualityGateObject(gate);
632
1013
  if (!gateObject) throw new Error("qualityGate must be a JSON object");
633
- validateCompletionQualityGate(gateObject);
1014
+ await validateCompletionQualityGate(cwd, gateObject);
634
1015
  return gate;
635
1016
  }
636
1017
 
@@ -896,26 +1277,32 @@ async function readBrief(cwd: string, args: readonly string[]): Promise<string>
896
1277
 
897
1278
  function renderStatus(summary: UltragoalStatusSummary, json: boolean): string {
898
1279
  if (json) return `${JSON.stringify(summary, null, 2)}\n`;
899
- if (!summary.exists) {
900
- return `No ultragoal plan found at ${summary.paths.goalsPath}. Run \`gjc ultragoal create-goals --brief "..."\` first.\n`;
901
- }
902
- const current = summary.currentGoal ? ` Current: ${summary.currentGoal.id} (${summary.currentGoal.status}).` : "";
903
- return `Ultragoal ${summary.status}: ${summary.counts.complete}/${summary.goals.length} complete.${current}\n`;
1280
+ return renderUltragoalStatusMarkdown(summary);
904
1281
  }
905
1282
 
906
1283
  function renderCompleteHandoff(
907
1284
  result: { plan: UltragoalPlan; goal?: UltragoalGoal; allComplete: boolean },
908
1285
  json: boolean,
1286
+ cwd: string,
909
1287
  ): string {
910
- if (json) return `${JSON.stringify(result, null, 2)}\n`;
911
- if (result.allComplete) return "All ultragoal goals are complete.\n";
912
- if (!result.goal) return "No schedulable ultragoal goal found.\n";
1288
+ if (json) {
1289
+ return renderCliWriteReceipt({
1290
+ ok: true,
1291
+ all_complete: result.allComplete,
1292
+ next_action: result.allComplete ? "none" : "execute-goal",
1293
+ goal_id: result.goal?.id,
1294
+ goal_status: result.goal?.status,
1295
+ gjc_objective: result.plan.gjcObjective,
1296
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1297
+ });
1298
+ }
1299
+ if (result.allComplete) return "ultragoal complete all=true\n";
1300
+ if (!result.goal) return "ultragoal next-action=none\n";
913
1301
  return [
914
- `Ultragoal handoff: ${result.goal.id} — ${result.goal.title}`,
915
- `Objective: ${result.goal.objective}`,
916
- `GJC objective: ${result.plan.gjcObjective}`,
917
- 'Call goal({"op":"get"}); call goal({"op":"create","objective":"<printed objective>"}) only if no active GJC goal exists, then complete this GJC story with goal({"op":"complete"}) after verification.',
918
- "Before checkpointing complete, obtain a passing architectReview (architecture/product/code CLEAR + APPROVE) and executorQa (e2e/red-team passed); record blockers instead of completing on any finding.",
1302
+ `ultragoal next-action=execute-goal goal-id=${result.goal.id}`,
1303
+ `objective=${result.goal.objective}`,
1304
+ `gjc-objective=${result.plan.gjcObjective}`,
1305
+ "checkpoint requires=architectReview:CLEAR+APPROVE,executorQa:passed",
919
1306
  "",
920
1307
  ].join("\n");
921
1308
  }
@@ -935,8 +1322,13 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
935
1322
  status: 0,
936
1323
  createdPlan: true,
937
1324
  stdout: json
938
- ? `${JSON.stringify(plan, null, 2)}\n`
939
- : `Created ultragoal plan with ${plan.goals.length} goal at ${getUltragoalPaths(cwd).goalsPath}.\n`,
1325
+ ? renderCliWriteReceipt({
1326
+ ok: true,
1327
+ goals_count: plan.goals.length,
1328
+ goal_ids: plan.goals.map(goal => goal.id),
1329
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1330
+ })
1331
+ : `Created ultragoal plan with ${plan.goals.length} goal${plan.goals.length === 1 ? "" : "s"} at ${getUltragoalPaths(cwd).goalsPath}.\n`,
940
1332
  };
941
1333
  }
942
1334
  case "complete-goals":
@@ -945,6 +1337,7 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
945
1337
  stdout: renderCompleteHandoff(
946
1338
  await startNextUltragoalGoal({ cwd, retryFailed: hasFlag(args, "--retry-failed") }),
947
1339
  json,
1340
+ cwd,
948
1341
  ),
949
1342
  };
950
1343
  case "checkpoint": {
@@ -959,9 +1352,19 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
959
1352
  gjcGoalJson: flagValue(args, "--gjc-goal-json"),
960
1353
  qualityGateJson: flagValue(args, "--quality-gate-json"),
961
1354
  });
1355
+ const goal = plan.goals.find(item => item.id === goalId);
962
1356
  return {
963
1357
  status: 0,
964
- stdout: json ? `${JSON.stringify(plan, null, 2)}\n` : `Checkpointed ${goalId} as ${status}.\n`,
1358
+ stdout: json
1359
+ ? renderCliWriteReceipt({
1360
+ ok: true,
1361
+ goal_id: goalId,
1362
+ status,
1363
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1364
+ completion_receipt_kind: goal?.completionVerification?.receiptKind,
1365
+ quality_gate_hash: goal?.completionVerification?.qualityGateHash,
1366
+ })
1367
+ : `ultragoal checkpoint goal-id=${goalId} status=${status}\n`,
965
1368
  };
966
1369
  }
967
1370
  case "steer": {
@@ -974,9 +1377,17 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
974
1377
  evidence: flagValue(args, "--evidence") ?? "",
975
1378
  rationale: flagValue(args, "--rationale") ?? "",
976
1379
  });
1380
+ const goal = plan.goals.at(-1);
977
1381
  return {
978
1382
  status: 0,
979
- stdout: json ? `${JSON.stringify(plan, null, 2)}\n` : "Accepted add_subgoal steering.\n",
1383
+ stdout: json
1384
+ ? renderCliWriteReceipt({
1385
+ ok: true,
1386
+ kind,
1387
+ goal_id: goal?.id,
1388
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1389
+ })
1390
+ : "Accepted add_subgoal steering.\n",
980
1391
  };
981
1392
  }
982
1393
  case "record-review-blockers": {
@@ -988,7 +1399,13 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
988
1399
  evidence: flagValue(args, "--evidence") ?? "",
989
1400
  gjcGoalJson: flagValue(args, "--gjc-goal-json"),
990
1401
  });
991
- return { status: 0, stdout: json ? `${JSON.stringify(plan, null, 2)}\n` : "Recorded review blockers.\n" };
1402
+ const goal = plan.goals.at(-1);
1403
+ return {
1404
+ status: 0,
1405
+ stdout: json
1406
+ ? renderCliWriteReceipt({ ok: true, goal_id: goal?.id, goals_path: getUltragoalPaths(cwd).goalsPath })
1407
+ : "Recorded review blockers.\n",
1408
+ };
992
1409
  }
993
1410
  default:
994
1411
  return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };