@gajae-code/coding-agent 0.2.4 → 0.3.0

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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
@@ -1,9 +1,10 @@
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";
6
5
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
6
+ import { renderUltragoalStatusMarkdown } from "./state-renderer";
7
+ import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
7
8
 
8
9
  export type UltragoalGjcGoalMode = "aggregate" | "per-story";
9
10
  export type UltragoalGoalStatus =
@@ -103,6 +104,12 @@ const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "
103
104
  const CLEAN_ARCHITECT_STATUS = "CLEAR";
104
105
  const APPROVE_RECOMMENDATION = "APPROVE";
105
106
  const PASSED_STATUS = "passed";
107
+ const NOT_APPLICABLE_STATUS = "not_applicable";
108
+ const COVERED_STATUS = "covered";
109
+ const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
110
+ const MIN_SUBSTANTIVE_EVIDENCE_WORDS = 5;
111
+ const MIN_SUBSTANTIVE_EVIDENCE_CHARS = 32;
112
+
106
113
  const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
107
114
  const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
108
115
 
@@ -143,19 +150,17 @@ function isEnoent(error: unknown): boolean {
143
150
  );
144
151
  }
145
152
 
146
- async function ensureUltragoalDir(paths: UltragoalPaths): Promise<void> {
147
- await fs.mkdir(paths.dir, { recursive: true });
148
- }
149
-
150
153
  async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLedgerEvent> {
151
154
  const paths = getUltragoalPaths(cwd);
152
- await ensureUltragoalDir(paths);
153
155
  const entry: UltragoalLedgerEvent = {
154
156
  eventId: typeof event.eventId === "string" ? event.eventId : crypto.randomUUID(),
155
157
  ...event,
156
158
  timestamp: new Date().toISOString(),
157
159
  };
158
- await fs.appendFile(paths.ledgerPath, `${JSON.stringify(entry)}\n`);
160
+ await appendJsonl(paths.ledgerPath, entry, {
161
+ cwd,
162
+ audit: { category: "ledger", verb: "append", owner: "gjc-runtime" },
163
+ });
159
164
  return entry;
160
165
  }
161
166
 
@@ -175,9 +180,14 @@ export async function readUltragoalLedger(cwd: string): Promise<UltragoalLedgerE
175
180
 
176
181
  async function writePlan(cwd: string, plan: UltragoalPlan): Promise<void> {
177
182
  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`);
183
+ await writeArtifact(paths.briefPath, `${plan.brief.trim()}\n`, {
184
+ cwd,
185
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
186
+ });
187
+ await writeJsonAtomic(paths.goalsPath, plan, {
188
+ cwd,
189
+ audit: { category: "state", verb: "write", owner: "gjc-runtime" },
190
+ });
181
191
  }
182
192
 
183
193
  function requiredUltragoalGoals(plan: UltragoalPlan): UltragoalGoal[] {
@@ -565,8 +575,330 @@ function requireEmptyBlockers(value: unknown, fieldName: string): void {
565
575
  throw new Error(`qualityGate ${fieldName} must be an empty blockers array`);
566
576
  }
567
577
  }
578
+ function requireQualityGateObject(value: unknown, fieldName: string): JsonObject {
579
+ const object = qualityGateObject(value);
580
+ if (!object) throw new Error(`qualityGate ${fieldName} must be an object`);
581
+ return object;
582
+ }
583
+
584
+ function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
585
+ if (!Array.isArray(value) || value.length === 0) {
586
+ throw new Error(`qualityGate ${fieldName} must be a non-empty object array`);
587
+ }
588
+ return value.map((item, index) => requireQualityGateObject(item, `${fieldName}[${index}]`));
589
+ }
590
+
591
+ function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
592
+ const value = row[key];
593
+ if (typeof value !== "string" || value.trim().length === 0) {
594
+ throw new Error(`qualityGate ${fieldName}.${key} must be a non-empty string`);
595
+ }
596
+ return value.trim();
597
+ }
598
+
599
+ function optionalStatusField(row: JsonObject, fieldName: string): string | null {
600
+ if (row.status === undefined) return null;
601
+ const status = requiredStringField(row, "status", fieldName).toLowerCase();
602
+ if (status === "todo") throw new Error(`qualityGate ${fieldName}.status must not be todo`);
603
+ return status;
604
+ }
605
+
606
+ function requireProofStatus(status: string, fieldName: string): void {
607
+ if (!ACCEPTED_PROOF_STATUSES.has(status) && status !== NOT_APPLICABLE_STATUS) {
608
+ throw new Error(`qualityGate ${fieldName}.status must be covered, passed, verified, or not_applicable`);
609
+ }
610
+ }
611
+ function requireSuccessStatus(status: string, fieldName: string): void {
612
+ requireProofStatus(status, fieldName);
613
+ if (status === NOT_APPLICABLE_STATUS) {
614
+ throw new Error(`qualityGate ${fieldName}.status must be covered, passed, or verified`);
615
+ }
616
+ }
617
+
618
+ function rowOutcomeStatuses(row: JsonObject, fieldName: string): string[] {
619
+ const statuses: string[] = [];
620
+ const status = optionalStatusField(row, fieldName);
621
+ if (status) statuses.push(status);
622
+ const verdict = row.verdict;
623
+ if (typeof verdict === "string" && verdict.trim().length > 0) statuses.push(verdict.trim().toLowerCase());
624
+ const result = row.result;
625
+ if (typeof result === "string" && result.trim().length > 0) statuses.push(result.trim().toLowerCase());
626
+ if (statuses.length === 0) throw new Error(`qualityGate ${fieldName}.verdict must be a non-empty string`);
627
+ return statuses;
628
+ }
629
+
630
+ function requireSuccessfulRowOutcome(row: JsonObject, fieldName: string): void {
631
+ for (const status of rowOutcomeStatuses(row, fieldName)) {
632
+ requireSuccessStatus(status, fieldName);
633
+ }
634
+ }
635
+
636
+ function requireStringLinks(value: unknown, fieldName: string): string[] {
637
+ const strings = nonEmptyStringArray(value);
638
+ if (!strings) throw new Error(`qualityGate ${fieldName} must be a non-empty string array`);
639
+ return strings.map(item => item.trim());
640
+ }
641
+
642
+ function optionalStringLinks(row: JsonObject, key: string, fieldName: string): string[] | null {
643
+ if (row[key] === undefined) return null;
644
+ return requireStringLinks(row[key], `${fieldName}.${key}`);
645
+ }
646
+
647
+ function buildRowIdMap(rows: JsonObject[], fieldName: string): Map<string, JsonObject> {
648
+ const ids = new Map<string, JsonObject>();
649
+ for (const [index, row] of rows.entries()) {
650
+ const id = requiredStringField(row, "id", `${fieldName}[${index}]`);
651
+ if (ids.has(id)) throw new Error(`qualityGate ${fieldName} contains duplicate id ${id}`);
652
+ ids.set(id, row);
653
+ }
654
+ return ids;
655
+ }
656
+
657
+ function requireResolvedLinks(ids: string[], map: Map<string, JsonObject>, fieldName: string): void {
658
+ for (const id of ids) {
659
+ if (!map.has(id)) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
660
+ }
661
+ }
662
+ function successfulLinkedRows(ids: string[], map: Map<string, JsonObject>, fieldName: string): JsonObject[] {
663
+ const rows: JsonObject[] = [];
664
+ for (const id of ids) {
665
+ const row = map.get(id);
666
+ if (!row) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
667
+ requireSuccessfulRowOutcome(row, `${fieldName}.${id}`);
668
+ rows.push(row);
669
+ }
670
+ return rows;
671
+ }
672
+
673
+ function normalizedEvidenceKind(row: JsonObject): string {
674
+ return requiredStringField(row, "kind", "executorQa.artifactRefs[]").toLowerCase().replaceAll("_", "-");
675
+ }
676
+
677
+ function evidenceKindMatches(kind: string, words: string[]): boolean {
678
+ return words.some(word => kind.includes(word));
679
+ }
680
+
681
+ function validateSurfaceArtifactCompatibility(
682
+ surface: string,
683
+ artifactIds: string[],
684
+ artifactRefs: Map<string, JsonObject>,
685
+ fieldName: string,
686
+ ): void {
687
+ const normalizedSurface = surface.toLowerCase().replaceAll("_", "-");
688
+ const kinds = artifactIds.map(id => normalizedEvidenceKind(artifactRefs.get(id)!));
689
+ const isGuiOrWeb = ["gui", "web", "browser", "ui", "visual"].some(word => normalizedSurface.includes(word));
690
+ if (isGuiOrWeb) {
691
+ const hasBrowser = kinds.some(kind =>
692
+ evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation"]),
693
+ );
694
+ const hasVisual = kinds.some(kind => evidenceKindMatches(kind, ["screenshot", "image", "visual"]));
695
+ if (!hasBrowser || !hasVisual) {
696
+ throw new Error(
697
+ `qualityGate ${fieldName} for GUI/web surfaces must reference browser automation plus screenshot or image-verdict artifacts`,
698
+ );
699
+ }
700
+ return;
701
+ }
702
+ const surfaceFamilies: Array<{ surface: string[]; evidence: string[]; label: string }> = [
703
+ {
704
+ surface: ["cli", "terminal", "command"],
705
+ evidence: ["cli", "log", "transcript", "terminal", "command", "test-report"],
706
+ label: "CLI",
707
+ },
708
+ {
709
+ surface: ["api", "package", "library", "sdk"],
710
+ evidence: ["api", "package", "consumer", "black-box", "test-report"],
711
+ label: "API/package",
712
+ },
713
+ {
714
+ surface: ["algorithm", "math", "mathematical", "equation"],
715
+ evidence: ["property", "boundary", "edge", "adversarial", "failure", "math", "algorithm", "test-report"],
716
+ label: "algorithm/math",
717
+ },
718
+ ];
719
+ for (const family of surfaceFamilies) {
720
+ if (family.surface.some(word => normalizedSurface.includes(word))) {
721
+ if (!kinds.some(kind => evidenceKindMatches(kind, family.evidence))) {
722
+ throw new Error(
723
+ `qualityGate ${fieldName} for ${family.label} surfaces must reference compatible artifact kinds`,
724
+ );
725
+ }
726
+ return;
727
+ }
728
+ }
729
+ }
730
+
731
+ function isSubstantiveEvidence(value: unknown): boolean {
732
+ if (typeof value !== "string") return false;
733
+ const trimmed = value.trim();
734
+ if (trimmed.length < MIN_SUBSTANTIVE_EVIDENCE_CHARS) return false;
735
+ const words = trimmed.split(/\s+/).filter(word => /[a-z0-9]/i.test(word));
736
+ if (words.length < MIN_SUBSTANTIVE_EVIDENCE_WORDS) return false;
737
+ const normalized = trimmed.toLowerCase();
738
+ return !["todo", "tbd", "n/a", "na", "none", "placeholder", "empty", "stub"].includes(normalized);
739
+ }
740
+
741
+ function hasTypedVerifiedReceipt(value: unknown): boolean {
742
+ const receipt = qualityGateObject(value);
743
+ if (!receipt) return false;
744
+ const type = nonEmptyString(receipt.type) ?? nonEmptyString(receipt.kind) ?? nonEmptyString(receipt.receiptType);
745
+ const id = nonEmptyString(receipt.id) ?? nonEmptyString(receipt.receiptId) ?? nonEmptyString(receipt.ref);
746
+ const status = (nonEmptyString(receipt.status) ?? nonEmptyString(receipt.verdict) ?? "").toLowerCase();
747
+ return Boolean(type && id && (status === "verified" || status === "passed"));
748
+ }
749
+
750
+ async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
751
+ const artifactPath = nonEmptyString(value);
752
+ if (!artifactPath) return false;
753
+ const resolved = path.resolve(cwd, artifactPath);
754
+ try {
755
+ const file = Bun.file(resolved);
756
+ return (await file.exists()) && file.size > 0;
757
+ } catch (error) {
758
+ if (isEnoent(error)) return false;
759
+ throw error;
760
+ }
761
+ }
762
+
763
+ async function requireSubstantiveArtifactEvidence(cwd: string, row: JsonObject, fieldName: string): Promise<void> {
764
+ if (isSubstantiveEvidence(row.inlineEvidence) || isSubstantiveEvidence(row.evidence)) return;
765
+ if (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt)) return;
766
+ if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
767
+ throw new Error(
768
+ `qualityGate ${fieldName} must reference an existing non-empty artifact path, substantive inlineEvidence, or a typed verifiedReceipt`,
769
+ );
770
+ }
771
+
772
+ async function validateArtifactRefs(cwd: string, executorQa: JsonObject): Promise<Map<string, JsonObject>> {
773
+ const rows = requireObjectArray(executorQa.artifactRefs, "executorQa.artifactRefs");
774
+ const idMap = buildRowIdMap(rows, "executorQa.artifactRefs");
775
+ for (const [index, row] of rows.entries()) {
776
+ const fieldName = `executorQa.artifactRefs[${index}]`;
777
+ requiredStringField(row, "kind", fieldName);
778
+ requiredStringField(row, "description", fieldName);
779
+ await requireSubstantiveArtifactEvidence(cwd, row, fieldName);
780
+ }
781
+ return idMap;
782
+ }
568
783
 
569
- function validateCompletionQualityGate(gate: JsonObject): void {
784
+ function validateSurfaceEvidence(
785
+ executorQa: JsonObject,
786
+ artifactRefs: Map<string, JsonObject>,
787
+ ): Map<string, JsonObject> {
788
+ const rows = requireObjectArray(executorQa.surfaceEvidence, "executorQa.surfaceEvidence");
789
+ const idMap = buildRowIdMap(rows, "executorQa.surfaceEvidence");
790
+ for (const [index, row] of rows.entries()) {
791
+ const fieldName = `executorQa.surfaceEvidence[${index}]`;
792
+ const status = optionalStatusField(row, fieldName);
793
+ requiredStringField(row, "contractRef", fieldName);
794
+ if (status === NOT_APPLICABLE_STATUS) {
795
+ requiredStringField(row, "reason", fieldName);
796
+ continue;
797
+ }
798
+ const surface = requiredStringField(row, "surface", fieldName);
799
+ requireSuccessfulRowOutcome(row, fieldName);
800
+ requiredStringField(row, "invocation", fieldName);
801
+ if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
802
+ requiredStringField(row, "result", fieldName);
803
+ }
804
+ const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
805
+ requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
806
+ validateSurfaceArtifactCompatibility(surface, artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
807
+ }
808
+ return idMap;
809
+ }
810
+
811
+ function validateAdversarialCases(
812
+ executorQa: JsonObject,
813
+ artifactRefs: Map<string, JsonObject>,
814
+ ): Map<string, JsonObject> {
815
+ const rows = requireObjectArray(executorQa.adversarialCases, "executorQa.adversarialCases");
816
+ const idMap = buildRowIdMap(rows, "executorQa.adversarialCases");
817
+ for (const [index, row] of rows.entries()) {
818
+ const fieldName = `executorQa.adversarialCases[${index}]`;
819
+ const status = optionalStatusField(row, fieldName);
820
+ if (status === NOT_APPLICABLE_STATUS) {
821
+ throw new Error(`qualityGate ${fieldName}.status must not be not_applicable`);
822
+ }
823
+ requireSuccessfulRowOutcome(row, fieldName);
824
+ requiredStringField(row, "contractRef", fieldName);
825
+ requiredStringField(row, "scenario", fieldName);
826
+ requiredStringField(row, "expectedBehavior", fieldName);
827
+ if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
828
+ requiredStringField(row, "result", fieldName);
829
+ }
830
+ const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
831
+ requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
832
+ }
833
+ return idMap;
834
+ }
835
+
836
+ function validateContractCoverage(
837
+ executorQa: JsonObject,
838
+ surfaceEvidence: Map<string, JsonObject>,
839
+ adversarialCases: Map<string, JsonObject>,
840
+ artifactRefs: Map<string, JsonObject>,
841
+ ): void {
842
+ const rows = requireObjectArray(executorQa.contractCoverage, "executorQa.contractCoverage");
843
+ buildRowIdMap(rows, "executorQa.contractCoverage");
844
+ let hasSuccessfulContractCoverage = false;
845
+ for (const [index, row] of rows.entries()) {
846
+ const fieldName = `executorQa.contractCoverage[${index}]`;
847
+ requiredStringField(row, "contractRef", fieldName);
848
+ const status = optionalStatusField(row, fieldName);
849
+ if (status === NOT_APPLICABLE_STATUS) {
850
+ requiredStringField(row, "reason", fieldName);
851
+ continue;
852
+ }
853
+ requiredStringField(row, "obligation", fieldName);
854
+ if (!status) throw new Error(`qualityGate ${fieldName}.status must be a non-empty string`);
855
+ requireSuccessStatus(status, fieldName);
856
+ hasSuccessfulContractCoverage = true;
857
+ const surfaceIds = optionalStringLinks(row, "surfaceEvidenceRefs", fieldName);
858
+ const adversarialIds = optionalStringLinks(row, "adversarialCaseRefs", fieldName);
859
+ const artifactIds = optionalStringLinks(row, "artifactRefs", fieldName);
860
+ if (!surfaceIds && !adversarialIds && !artifactIds) {
861
+ throw new Error(
862
+ `qualityGate ${fieldName} must link to surfaceEvidenceRefs, adversarialCaseRefs, or artifactRefs`,
863
+ );
864
+ }
865
+ let successfulProofLinks = 0;
866
+ if (surfaceIds)
867
+ successfulProofLinks += successfulLinkedRows(
868
+ surfaceIds,
869
+ surfaceEvidence,
870
+ `${fieldName}.surfaceEvidenceRefs`,
871
+ ).length;
872
+ if (adversarialIds) {
873
+ successfulProofLinks += successfulLinkedRows(
874
+ adversarialIds,
875
+ adversarialCases,
876
+ `${fieldName}.adversarialCaseRefs`,
877
+ ).length;
878
+ }
879
+ if (artifactIds) {
880
+ requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
881
+ successfulProofLinks += artifactIds.length;
882
+ }
883
+ if (successfulProofLinks === 0) {
884
+ throw new Error(`qualityGate ${fieldName} must link to at least one successful proof row or artifact`);
885
+ }
886
+ }
887
+ if (!hasSuccessfulContractCoverage) {
888
+ throw new Error(
889
+ "qualityGate executorQa.contractCoverage must include at least one row with status covered, passed, or verified",
890
+ );
891
+ }
892
+ }
893
+
894
+ async function validateExecutorQaRedTeamEvidence(cwd: string, executorQa: JsonObject): Promise<void> {
895
+ const artifactRefs = await validateArtifactRefs(cwd, executorQa);
896
+ const surfaceEvidence = validateSurfaceEvidence(executorQa, artifactRefs);
897
+ const adversarialCases = validateAdversarialCases(executorQa, artifactRefs);
898
+ validateContractCoverage(executorQa, surfaceEvidence, adversarialCases, artifactRefs);
899
+ }
900
+
901
+ async function validateCompletionQualityGate(cwd: string, gate: JsonObject): Promise<void> {
570
902
  const codeReview = qualityGateObject(gate.codeReview);
571
903
  if (codeReview) {
572
904
  throw new Error(
@@ -611,6 +943,7 @@ function validateCompletionQualityGate(gate: JsonObject): void {
611
943
  }
612
944
  requireNonEmptyString(executorQa.evidence, "executorQa.evidence");
613
945
  requireEmptyBlockers(executorQa.blockers, "executorQa.blockers");
946
+ await validateExecutorQaRedTeamEvidence(cwd, executorQa);
614
947
  if (iteration.status !== PASSED_STATUS || iteration.fullRerun !== true) {
615
948
  throw new Error("qualityGate iteration must be passed with fullRerun true");
616
949
  }
@@ -630,7 +963,7 @@ async function readRequiredCompletionQualityGate(cwd: string, value: string | un
630
963
  const gate = await readStructuredValue(cwd, value);
631
964
  const gateObject = qualityGateObject(gate);
632
965
  if (!gateObject) throw new Error("qualityGate must be a JSON object");
633
- validateCompletionQualityGate(gateObject);
966
+ await validateCompletionQualityGate(cwd, gateObject);
634
967
  return gate;
635
968
  }
636
969
 
@@ -896,11 +1229,7 @@ async function readBrief(cwd: string, args: readonly string[]): Promise<string>
896
1229
 
897
1230
  function renderStatus(summary: UltragoalStatusSummary, json: boolean): string {
898
1231
  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`;
1232
+ return renderUltragoalStatusMarkdown(summary);
904
1233
  }
905
1234
 
906
1235
  function renderCompleteHandoff(
@@ -914,8 +1243,8 @@ function renderCompleteHandoff(
914
1243
  `Ultragoal handoff: ${result.goal.id} — ${result.goal.title}`,
915
1244
  `Objective: ${result.goal.objective}`,
916
1245
  `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.",
1246
+ 'Call goal({"op":"get"}); call goal({"op":"create","objective":"<printed objective>"}) only if no active GJC goal exists, then keep the GJC goal active while this Ultragoal story is verified and checkpointed.',
1247
+ 'Before checkpointing complete, obtain a passing architectReview (architecture/product/code CLEAR + APPROVE) and executorQa (e2e/red-team passed with contractCoverage, surfaceEvidence, adversarialCases, and artifactRefs matrix evidence), then checkpoint with --quality-gate-json and a fresh active goal snapshot; record blockers instead of completing on any finding, plan/code mismatch, shallow evidence, or missing artifact link; call goal({"op":"complete"}) only after the final aggregate receipt exists.',
919
1248
  "",
920
1249
  ].join("\n");
921
1250
  }