@gajae-code/coding-agent 0.5.0 → 0.5.2

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 (194) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +26 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/list-models.d.ts +6 -0
  6. package/dist/types/cli/setup-cli.d.ts +8 -1
  7. package/dist/types/commands/gc.d.ts +26 -0
  8. package/dist/types/commands/setup.d.ts +7 -0
  9. package/dist/types/config/file-lock-gc.d.ts +5 -0
  10. package/dist/types/config/file-lock.d.ts +29 -0
  11. package/dist/types/config/model-registry.d.ts +4 -0
  12. package/dist/types/config/models-config-schema.d.ts +5 -0
  13. package/dist/types/config/settings-schema.d.ts +62 -0
  14. package/dist/types/coordinator/contract.d.ts +1 -1
  15. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  25. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  26. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  27. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  28. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  29. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  31. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  32. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  33. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  34. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  42. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  43. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  46. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -1
  49. package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
  50. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  51. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  52. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  53. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  54. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  55. package/dist/types/modes/theme/theme.d.ts +1 -0
  56. package/dist/types/modes/types.d.ts +1 -1
  57. package/dist/types/session/agent-session.d.ts +1 -1
  58. package/dist/types/session/blob-store.d.ts +39 -3
  59. package/dist/types/session/history-storage.d.ts +2 -2
  60. package/dist/types/session/session-manager.d.ts +10 -1
  61. package/dist/types/setup/credential-import.d.ts +79 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/executor.d.ts +1 -0
  64. package/dist/types/task/render.d.ts +1 -1
  65. package/dist/types/tools/ask.d.ts +15 -1
  66. package/dist/types/tools/subagent-render.d.ts +7 -1
  67. package/dist/types/tools/subagent.d.ts +27 -0
  68. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  69. package/dist/types/web/search/index.d.ts +4 -4
  70. package/dist/types/web/search/provider.d.ts +16 -20
  71. package/dist/types/web/search/providers/base.d.ts +2 -1
  72. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  73. package/dist/types/web/search/types.d.ts +14 -2
  74. package/package.json +7 -7
  75. package/scripts/build-binary.ts +7 -0
  76. package/src/async/job-manager.ts +52 -0
  77. package/src/cli/args.ts +5 -0
  78. package/src/cli/auth-broker-cli.ts +1 -0
  79. package/src/cli/fast-help.ts +2 -0
  80. package/src/cli/list-models.ts +13 -1
  81. package/src/cli/setup-cli.ts +138 -3
  82. package/src/cli.ts +1 -0
  83. package/src/commands/gc.ts +22 -0
  84. package/src/commands/harness.ts +7 -3
  85. package/src/commands/setup.ts +5 -1
  86. package/src/commands/ultragoal.ts +3 -1
  87. package/src/config/file-lock-gc.ts +193 -0
  88. package/src/config/file-lock.ts +66 -10
  89. package/src/config/model-profile-activation.ts +15 -3
  90. package/src/config/model-profiles.ts +39 -30
  91. package/src/config/model-registry.ts +21 -1
  92. package/src/config/models-config-schema.ts +1 -0
  93. package/src/config/settings-schema.ts +62 -0
  94. package/src/coordinator/contract.ts +1 -0
  95. package/src/coordinator-mcp/server.ts +459 -3
  96. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  97. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  106. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  107. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  108. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  109. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  110. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  111. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  113. package/src/defaults/gjc-defaults.ts +7 -0
  114. package/src/defaults/gjc-grok-cli.ts +22 -0
  115. package/src/extensibility/extensions/index.ts +1 -0
  116. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  117. package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
  118. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  119. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  120. package/src/gjc-runtime/gc-render.ts +70 -0
  121. package/src/gjc-runtime/gc-runtime.ts +403 -0
  122. package/src/gjc-runtime/launch-tmux.ts +3 -4
  123. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  124. package/src/gjc-runtime/ralplan-runtime.ts +232 -19
  125. package/src/gjc-runtime/state-renderer.ts +12 -3
  126. package/src/gjc-runtime/state-runtime.ts +48 -30
  127. package/src/gjc-runtime/state-writer.ts +254 -7
  128. package/src/gjc-runtime/team-gc.ts +49 -0
  129. package/src/gjc-runtime/team-runtime.ts +179 -2
  130. package/src/gjc-runtime/tmux-common.ts +14 -0
  131. package/src/gjc-runtime/tmux-gc.ts +177 -0
  132. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  133. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  134. package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
  135. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  136. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  137. package/src/harness-control-plane/gc-adapter.ts +184 -0
  138. package/src/harness-control-plane/owner.ts +14 -2
  139. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  140. package/src/harness-control-plane/storage.ts +70 -0
  141. package/src/hooks/skill-state.ts +121 -2
  142. package/src/internal-urls/docs-index.generated.ts +22 -12
  143. package/src/lsp/defaults.json +1 -0
  144. package/src/main.ts +18 -3
  145. package/src/modes/acp/acp-agent.ts +4 -2
  146. package/src/modes/bridge/bridge-mode.ts +2 -1
  147. package/src/modes/components/history-search.ts +5 -2
  148. package/src/modes/components/hook-selector.ts +19 -0
  149. package/src/modes/components/model-selector.ts +51 -8
  150. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  151. package/src/modes/components/status-line/segments.ts +1 -1
  152. package/src/modes/controllers/command-controller.ts +25 -6
  153. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  154. package/src/modes/controllers/selector-controller.ts +81 -1
  155. package/src/modes/interactive-mode.ts +11 -1
  156. package/src/modes/rpc/rpc-mode.ts +266 -34
  157. package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
  158. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  159. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  160. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  161. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  162. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  163. package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
  164. package/src/modes/theme/defaults/claude-code.json +100 -0
  165. package/src/modes/theme/defaults/codex.json +100 -0
  166. package/src/modes/theme/defaults/index.ts +6 -0
  167. package/src/modes/theme/defaults/opencode.json +102 -0
  168. package/src/modes/theme/theme.ts +2 -2
  169. package/src/modes/types.ts +1 -1
  170. package/src/prompts/agents/executor.md +5 -2
  171. package/src/sdk.ts +29 -4
  172. package/src/session/agent-session.ts +99 -19
  173. package/src/session/blob-store.ts +59 -3
  174. package/src/session/history-storage.ts +32 -11
  175. package/src/session/session-manager.ts +72 -20
  176. package/src/setup/credential-import.ts +429 -0
  177. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  178. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  179. package/src/skill-state/workflow-hud.ts +106 -10
  180. package/src/slash-commands/builtin-registry.ts +3 -2
  181. package/src/task/executor.ts +16 -1
  182. package/src/task/render.ts +18 -7
  183. package/src/tools/ask.ts +59 -2
  184. package/src/tools/cron.ts +1 -1
  185. package/src/tools/job.ts +3 -2
  186. package/src/tools/monitor.ts +36 -1
  187. package/src/tools/subagent-render.ts +128 -29
  188. package/src/tools/subagent.ts +173 -9
  189. package/src/tools/ultragoal-ask-guard.ts +39 -0
  190. package/src/web/search/index.ts +25 -25
  191. package/src/web/search/provider.ts +178 -87
  192. package/src/web/search/providers/base.ts +2 -1
  193. package/src/web/search/providers/openai-compatible.ts +151 -0
  194. package/src/web/search/types.ts +47 -22
@@ -1,9 +1,12 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as path from "node:path";
3
+ import { inflateSync } from "node:zlib";
4
+
3
5
  import type { WorkflowHudSummary } from "../skill-state/active-state";
4
6
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
5
7
  import { renderCliWriteReceipt } from "./cli-write-receipt";
6
8
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
9
+ import { latestUltragoalLedgerEventFromText } from "./ledger-event-renderer";
7
10
  import { renderUltragoalStatusMarkdown } from "./state-renderer";
8
11
  import { reconcileWorkflowSkillState } from "./state-runtime";
9
12
  import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
@@ -92,6 +95,8 @@ export interface UltragoalStatusSummary {
92
95
  }
93
96
 
94
97
  export interface UltragoalCommandResult {
98
+ reviewBlockerGoalIds?: string[];
99
+ createdReviewPlan?: boolean;
95
100
  status: number;
96
101
  stdout?: string;
97
102
  stderr?: string;
@@ -794,16 +799,37 @@ function evidenceKindMatches(kind: string, words: string[]): boolean {
794
799
  return words.some(word => kind.includes(word));
795
800
  }
796
801
 
802
+ type SurfaceFamily = "web" | "cli" | "native" | "api-package" | "algorithm-math" | "unknown";
803
+
804
+ export function normalizeSurfaceToken(value: string): string {
805
+ return value.toLowerCase().replaceAll("_", "-").trim();
806
+ }
807
+
808
+ export function surfaceFamily(value: string): SurfaceFamily {
809
+ const normalized = normalizeSurfaceToken(value);
810
+ if (["native", "desktop", "tui"].some(word => normalized.includes(word))) return "native";
811
+ if (["gui", "web", "browser", "ui", "visual"].some(word => normalized.includes(word))) return "web";
812
+ if (["cli", "terminal", "command"].some(word => normalized.includes(word))) return "cli";
813
+ if (["api", "package", "library", "sdk"].some(word => normalized.includes(word))) return "api-package";
814
+ if (["algorithm", "math", "mathematical", "equation"].some(word => normalized.includes(word))) {
815
+ return "algorithm-math";
816
+ }
817
+ return "unknown";
818
+ }
819
+
820
+ function isLiveSurfaceFamily(family: SurfaceFamily): boolean {
821
+ return family === "web" || family === "cli" || family === "native";
822
+ }
823
+
797
824
  function validateSurfaceArtifactCompatibility(
798
825
  surface: string,
799
826
  artifactIds: string[],
800
827
  artifactRefs: Map<string, JsonObject>,
801
828
  fieldName: string,
802
829
  ): void {
803
- const normalizedSurface = surface.toLowerCase().replaceAll("_", "-");
830
+ const family = surfaceFamily(surface);
804
831
  const kinds = artifactIds.map(id => normalizedEvidenceKind(artifactRefs.get(id)!));
805
- const isGuiOrWeb = ["gui", "web", "browser", "ui", "visual"].some(word => normalizedSurface.includes(word));
806
- if (isGuiOrWeb) {
832
+ if (family === "web") {
807
833
  const hasBrowser = kinds.some(kind =>
808
834
  evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation"]),
809
835
  );
@@ -815,31 +841,30 @@ function validateSurfaceArtifactCompatibility(
815
841
  }
816
842
  return;
817
843
  }
818
- const surfaceFamilies: Array<{ surface: string[]; evidence: string[]; label: string }> = [
819
- {
820
- surface: ["cli", "terminal", "command"],
844
+ const surfaceFamilies: Record<Exclude<SurfaceFamily, "web" | "unknown">, { evidence: string[]; label: string }> = {
845
+ cli: {
821
846
  evidence: ["cli", "log", "transcript", "terminal", "command", "test-report"],
822
847
  label: "CLI",
823
848
  },
824
- {
825
- surface: ["api", "package", "library", "sdk"],
849
+ native: {
850
+ evidence: ["native", "desktop", "tui", "terminal", "pty", "transcript", "screenshot", "image", "automation"],
851
+ label: "native",
852
+ },
853
+ "api-package": {
826
854
  evidence: ["api", "package", "consumer", "black-box", "test-report"],
827
855
  label: "API/package",
828
856
  },
829
- {
830
- surface: ["algorithm", "math", "mathematical", "equation"],
857
+ "algorithm-math": {
831
858
  evidence: ["property", "boundary", "edge", "adversarial", "failure", "math", "algorithm", "test-report"],
832
859
  label: "algorithm/math",
833
860
  },
834
- ];
835
- for (const family of surfaceFamilies) {
836
- if (family.surface.some(word => normalizedSurface.includes(word))) {
837
- if (!kinds.some(kind => evidenceKindMatches(kind, family.evidence))) {
838
- throw new Error(
839
- `qualityGate ${fieldName} for ${family.label} surfaces must reference compatible artifact kinds`,
840
- );
841
- }
842
- return;
861
+ };
862
+ if (family !== "unknown") {
863
+ const expected = surfaceFamilies[family];
864
+ if (!kinds.some(kind => evidenceKindMatches(kind, expected.evidence))) {
865
+ throw new Error(
866
+ `qualityGate ${fieldName} for ${expected.label} surfaces must reference compatible artifact kinds`,
867
+ );
843
868
  }
844
869
  }
845
870
  }
@@ -876,31 +901,850 @@ async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise
876
901
  }
877
902
  }
878
903
 
879
- async function requireSubstantiveArtifactEvidence(cwd: string, row: JsonObject, fieldName: string): Promise<void> {
880
- if (isSubstantiveEvidence(row.inlineEvidence) || isSubstantiveEvidence(row.evidence)) return;
881
- if (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt)) return;
882
- if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
904
+ async function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string): Promise<Buffer | null> {
905
+ const artifactPath = nonEmptyString(row.path);
906
+ if (!artifactPath) return null;
907
+ const resolved = path.resolve(cwd, artifactPath);
908
+ try {
909
+ const file = Bun.file(resolved);
910
+ if (!(await file.exists())) return null;
911
+ return Buffer.from(await file.arrayBuffer());
912
+ } catch (error) {
913
+ if (isEnoent(error)) return null;
914
+ throw new Error(`qualityGate ${fieldName} artifact could not be read: ${String(error)}`);
915
+ }
916
+ }
917
+
918
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
919
+ const JPEG_START_OF_IMAGE = 0xd8;
920
+ const JPEG_END_OF_IMAGE = 0xd9;
921
+ const JPEG_START_OF_SCAN = 0xda;
922
+ const JPEG_STANDALONE_MARKERS = new Set([0x01, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7]);
923
+ const PNG_CRC_TABLE = new Uint32Array(256).map((_, index) => {
924
+ let crc = index;
925
+ for (let bit = 0; bit < 8; bit++) crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
926
+ return crc >>> 0;
927
+ });
928
+
929
+ function pngCrc32(bytes: Buffer): number {
930
+ let crc = 0xffffffff;
931
+ for (const byte of bytes) crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
932
+ return (crc ^ 0xffffffff) >>> 0;
933
+ }
934
+
935
+ function parsePngDimensions(
936
+ bytes: Buffer,
937
+ ): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
938
+ if (bytes.length < 45) return null;
939
+ if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
940
+ let offset = 8;
941
+ let width = 0;
942
+ let height = 0;
943
+ let sawIhdr = false;
944
+ let sawIdat = false;
945
+ const idatChunks: Buffer[] = [];
946
+ while (offset + 12 <= bytes.length) {
947
+ const chunkStart = offset;
948
+ const length = bytes.readUInt32BE(offset);
949
+ offset += 4;
950
+ const type = bytes.toString("ascii", offset, offset + 4);
951
+ offset += 4;
952
+ if (offset + length + 4 > bytes.length) return null;
953
+ const data = bytes.subarray(offset, offset + length);
954
+ offset += length;
955
+ const expectedCrc = bytes.readUInt32BE(offset);
956
+ offset += 4;
957
+ if (pngCrc32(bytes.subarray(chunkStart + 4, offset - 4)) !== expectedCrc) return null;
958
+ if (!sawIhdr) {
959
+ if (type !== "IHDR" || length !== 13) return null;
960
+ width = data.readUInt32BE(0);
961
+ height = data.readUInt32BE(4);
962
+ if (
963
+ width === 0 ||
964
+ height === 0 ||
965
+ data[8] !== 8 ||
966
+ ![2, 6].includes(data[9]!) ||
967
+ data[10] !== 0 ||
968
+ data[11] !== 0 ||
969
+ data[12] !== 0
970
+ )
971
+ return null;
972
+ sawIhdr = true;
973
+ } else if (type === "IHDR") return null;
974
+ if (type === "IDAT") {
975
+ if (!sawIhdr || length === 0) return null;
976
+ sawIdat = true;
977
+ idatChunks.push(data);
978
+ }
979
+ if (type === "IEND") {
980
+ if (length !== 0 || !sawIhdr || !sawIdat || offset !== bytes.length) return null;
981
+ try {
982
+ return { width, height, headerBytes: 8, sampleBytes: inflateSync(Buffer.concat(idatChunks)) };
983
+ } catch {
984
+ return null;
985
+ }
986
+ }
987
+ }
988
+ return null;
989
+ }
990
+
991
+ function parseJpegDimensions(
992
+ bytes: Buffer,
993
+ ): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
994
+ if (bytes.length < 8 || bytes[0] !== 0xff || bytes[1] !== JPEG_START_OF_IMAGE) return null;
995
+ let offset = 2;
996
+ let dimensions: { width: number; height: number; headerBytes: number } | null = null;
997
+ let sawStartOfScan = false;
998
+ let scanStart = -1;
999
+ while (offset < bytes.length) {
1000
+ if (bytes[offset] !== 0xff) return null;
1001
+ while (offset < bytes.length && bytes[offset] === 0xff) offset++;
1002
+ if (offset >= bytes.length) return null;
1003
+ const marker = bytes[offset++];
1004
+ if (marker === 0x00) return null;
1005
+ if (marker === JPEG_END_OF_IMAGE) return null;
1006
+ if (JPEG_STANDALONE_MARKERS.has(marker)) continue;
1007
+ if (offset + 2 > bytes.length) return null;
1008
+ const segmentLength = bytes.readUInt16BE(offset);
1009
+ if (segmentLength < 2 || offset + segmentLength > bytes.length) return null;
1010
+ const segmentDataEnd = offset + segmentLength;
1011
+ if (marker === 0xc0 || marker === 0xc1 || marker === 0xc2) {
1012
+ if (segmentLength < 8) return null;
1013
+ dimensions = {
1014
+ width: bytes.readUInt16BE(offset + 5),
1015
+ height: bytes.readUInt16BE(offset + 3),
1016
+ headerBytes: offset + segmentLength,
1017
+ };
1018
+ }
1019
+ if (marker === JPEG_START_OF_SCAN) {
1020
+ if (!dimensions || segmentDataEnd >= bytes.length) return null;
1021
+ sawStartOfScan = true;
1022
+ scanStart = segmentDataEnd;
1023
+ break;
1024
+ }
1025
+ offset += segmentLength;
1026
+ }
1027
+ if (!dimensions || !sawStartOfScan || scanStart < 0) return null;
1028
+ let scanOffset = scanStart;
1029
+ let entropyBytes = 0;
1030
+ while (scanOffset < bytes.length) {
1031
+ const byte = bytes[scanOffset++]!;
1032
+ if (byte !== 0xff) {
1033
+ entropyBytes++;
1034
+ continue;
1035
+ }
1036
+ if (scanOffset >= bytes.length) return null;
1037
+ const marker = bytes[scanOffset++]!;
1038
+ if (marker === 0x00) {
1039
+ entropyBytes++;
1040
+ continue;
1041
+ }
1042
+ if (JPEG_STANDALONE_MARKERS.has(marker)) continue;
1043
+ if (marker === JPEG_END_OF_IMAGE) {
1044
+ if (scanOffset !== bytes.length || entropyBytes < 32) return null;
1045
+ return { ...dimensions, sampleBytes: bytes.subarray(scanStart, scanOffset - 2) };
1046
+ }
1047
+ return null;
1048
+ }
1049
+ return null;
1050
+ }
1051
+
1052
+ function unsupportedScreenshotFormat(bytes: Buffer): string | null {
1053
+ if (bytes.toString("ascii", 0, 6) === "GIF87a" || bytes.toString("ascii", 0, 6) === "GIF89a") return "GIF";
1054
+ if (bytes.toString("ascii", 0, 2) === "BM") return "BMP";
1055
+ if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
1056
+ return "WebP";
1057
+ return null;
1058
+ }
1059
+
1060
+ function parseImageDimensions(
1061
+ bytes: Buffer,
1062
+ ): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
1063
+ return parsePngDimensions(bytes) ?? parseJpegDimensions(bytes);
1064
+ }
1065
+
1066
+ function hasNonUniformImageBytes(bytes: Buffer, headerBytes: number, sampleBytes?: Buffer): boolean {
1067
+ const source = sampleBytes ?? bytes;
1068
+ const sampleStart = sampleBytes ? 0 : Math.min(Math.max(headerBytes, 0), source.length);
1069
+ const sampleLength = source.length - sampleStart;
1070
+ if (sampleLength < 32) return false;
1071
+ const windows: Buffer[] = [];
1072
+ for (let index = 0; index < 64; index++) {
1073
+ const offset = sampleStart + Math.floor(((sampleLength - 32) * index) / 63);
1074
+ windows.push(source.subarray(offset, offset + 32));
1075
+ }
1076
+ const byteCounts = new Map<number, number>();
1077
+ let total = 0;
1078
+ for (const window of windows) {
1079
+ for (const byte of window) {
1080
+ byteCounts.set(byte, (byteCounts.get(byte) ?? 0) + 1);
1081
+ total++;
1082
+ }
1083
+ }
1084
+ const first = windows[0]!;
1085
+ const differingWindows = windows.slice(1).filter(window => !window.equals(first)).length;
1086
+ const maxCount = Math.max(...byteCounts.values());
1087
+ return byteCounts.size >= 16 && differingWindows >= 8 && maxCount / total <= 0.95;
1088
+ }
1089
+
1090
+ async function validateScreenshotArtifact(cwd: string, row: JsonObject, fieldName: string): Promise<boolean> {
1091
+ const bytes = await readArtifactBytes(cwd, row, fieldName);
1092
+ if (!bytes) throw new Error(`qualityGate ${fieldName} screenshot artifact path must resolve to an existing file`);
1093
+ if (bytes.length < 4096) throw new Error(`qualityGate ${fieldName} screenshot artifact must be at least 4096 bytes`);
1094
+ const unsupportedFormat = unsupportedScreenshotFormat(bytes);
1095
+ if (unsupportedFormat) {
1096
+ throw new Error(
1097
+ `qualityGate ${fieldName} unsupported/undecodable screenshot format ${unsupportedFormat}; use PNG or fully marker-validated JPEG`,
1098
+ );
1099
+ }
1100
+ const dimensions = parseImageDimensions(bytes);
1101
+ if (!dimensions)
1102
+ throw new Error(`qualityGate ${fieldName} screenshot artifact must be a decodable PNG or JPEG image`);
1103
+ if (dimensions.width < 320 || dimensions.height < 180) {
1104
+ throw new Error(`qualityGate ${fieldName} screenshot artifact must be at least 320x180 pixels`);
1105
+ }
1106
+ if (!hasNonUniformImageBytes(bytes, dimensions.headerBytes, dimensions.sampleBytes)) {
1107
+ throw new Error(
1108
+ `qualityGate ${fieldName} screenshot artifact must be non-uniform, not blank, solid, tiny, or placeholder imagery`,
1109
+ );
1110
+ }
1111
+ return true;
1112
+ }
1113
+
1114
+ function normalizeTranscriptTimestamp(value: unknown): number | null {
1115
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1116
+ if (typeof value !== "string" || value.trim().length === 0) return null;
1117
+ const numeric = Number(value);
1118
+ if (Number.isFinite(numeric)) return numeric;
1119
+ const parsed = Date.parse(value);
1120
+ return Number.isFinite(parsed) ? parsed : null;
1121
+ }
1122
+
1123
+ function transcriptSurfaceCompatible(value: unknown, family: SurfaceFamily): boolean {
1124
+ const surface = nonEmptyString(value);
1125
+ return !surface || family === "unknown" || surfaceFamily(surface) === family;
1126
+ }
1127
+
1128
+ function actionSelectorRequired(type: string): boolean {
1129
+ return ["click", "fill", "press", "assert", "screenshot", "observe"].includes(type);
1130
+ }
1131
+
1132
+ async function validateAutomationTranscriptArtifact(
1133
+ cwd: string,
1134
+ row: JsonObject,
1135
+ fieldName: string,
1136
+ options: { surfaceFamily: SurfaceFamily },
1137
+ ): Promise<boolean> {
1138
+ const bytes = await readArtifactBytes(cwd, row, fieldName);
1139
+ if (!bytes) throw new Error(`qualityGate ${fieldName} automation transcript path must resolve to an existing file`);
1140
+ let transcript: JsonObject;
1141
+ try {
1142
+ const parsed = JSON.parse(bytes.toString("utf8"));
1143
+ transcript = requireQualityGateObject(parsed, `${fieldName}.transcript`);
1144
+ } catch (error) {
1145
+ throw new Error(`qualityGate ${fieldName} automation transcript must be valid JSON: ${String(error)}`);
1146
+ }
1147
+ if (transcript.schemaVersion !== 1)
1148
+ throw new Error(`qualityGate ${fieldName} automation transcript schemaVersion must be 1`);
1149
+ if (!transcriptSurfaceCompatible(transcript.surface, options.surfaceFamily)) {
1150
+ throw new Error(
1151
+ `qualityGate ${fieldName} automation transcript surface is not compatible with ${options.surfaceFamily}`,
1152
+ );
1153
+ }
1154
+ if (!nonEmptyString(transcript.tool))
1155
+ throw new Error(`qualityGate ${fieldName} automation transcript tool must be non-empty`);
1156
+ const actions = requireObjectArray(transcript.actions, `${fieldName}.actions`);
1157
+ if (actions.length < 1) throw new Error(`qualityGate ${fieldName} automation transcript actions must be non-empty`);
1158
+ const assertionsValue = transcript.assertions;
1159
+ const assertions =
1160
+ assertionsValue === undefined ? [] : requireObjectArray(assertionsValue, `${fieldName}.assertions`);
1161
+ const timestamps: number[] = [];
1162
+ let hasSelectorBearingEntry = false;
1163
+ for (const [index, action] of actions.entries()) {
1164
+ const actionField = `${fieldName}.actions[${index}]`;
1165
+ const type = requiredStringField(action, "type", actionField).toLowerCase();
1166
+ const timestamp = normalizeTranscriptTimestamp(action.timestamp);
1167
+ if (timestamp === null) throw new Error(`qualityGate ${actionField}.timestamp must be present and parseable`);
1168
+ timestamps.push(timestamp);
1169
+ const selector = nonEmptyString(action.selector);
1170
+ if (actionSelectorRequired(type) && !selector)
1171
+ throw new Error(`qualityGate ${actionField}.selector must be non-empty`);
1172
+ if (type === "goto" && !nonEmptyString(action.url))
1173
+ throw new Error(`qualityGate ${actionField}.url must be non-empty`);
1174
+ if (type === "custom" && !selector && !nonEmptyString(action.target)) {
1175
+ throw new Error(`qualityGate ${actionField}.selector or target must be non-empty`);
1176
+ }
1177
+ if (selector) hasSelectorBearingEntry = true;
1178
+ }
1179
+ for (const [index, assertion] of assertions.entries()) {
1180
+ const assertionField = `${fieldName}.assertions[${index}]`;
1181
+ const timestamp = normalizeTranscriptTimestamp(assertion.timestamp);
1182
+ if (timestamp === null) throw new Error(`qualityGate ${assertionField}.timestamp must be present and parseable`);
1183
+ timestamps.push(timestamp);
1184
+ if (nonEmptyString(assertion.status)?.toLowerCase() !== PASSED_STATUS) {
1185
+ throw new Error(`qualityGate ${assertionField}.status must be passed`);
1186
+ }
1187
+ if (nonEmptyString(assertion.selector)) hasSelectorBearingEntry = true;
1188
+ }
1189
+ for (let index = 1; index < timestamps.length; index++) {
1190
+ if (timestamps[index]! < timestamps[index - 1]!) {
1191
+ throw new Error(`qualityGate ${fieldName} automation transcript timestamps must be monotonic non-decreasing`);
1192
+ }
1193
+ }
1194
+ if (!hasSelectorBearingEntry) {
1195
+ throw new Error(
1196
+ `qualityGate ${fieldName} automation transcript must include at least one selector-bearing action or assertion`,
1197
+ );
1198
+ }
1199
+ return true;
1200
+ }
1201
+
1202
+ async function validatePtyCaptureArtifact(cwd: string, row: JsonObject, fieldName: string): Promise<boolean> {
1203
+ const bytes = await readArtifactBytes(cwd, row, fieldName);
1204
+ if (!bytes) throw new Error(`qualityGate ${fieldName} PTY capture path must resolve to an existing file`);
1205
+ if (bytes.length < 512) throw new Error(`qualityGate ${fieldName} PTY capture must be at least 512 bytes`);
1206
+ const text = bytes.toString("utf8");
1207
+ const hasCsi = /\x1b\[[0-?]*[ -/]*[@-~]/.test(text);
1208
+ const hasOsc = /\x1b\][^\x07]*(?:\x07|\x1b\\)/.test(text);
1209
+ const hasAltOrCursor = /\x1b\[\?1049[hl]|\x1b\[H|\x1b\[2J/.test(text);
1210
+ const hasRedraw = /[\r\b]/.test(text) && hasCsi;
1211
+ if (!hasCsi && !hasOsc && !hasAltOrCursor && !hasRedraw) {
1212
+ throw new Error(`qualityGate ${fieldName} PTY capture must contain terminal control sequences`);
1213
+ }
1214
+ if (!/[\x20-\x7e]{10,}/.test(text)) {
1215
+ throw new Error(
1216
+ `qualityGate ${fieldName} PTY capture must contain a printable text run of at least 10 characters`,
1217
+ );
1218
+ }
1219
+ return true;
1220
+ }
1221
+
1222
+ function structuralArtifactKind(row: JsonObject): "screenshot" | "automation" | "pty" | null {
1223
+ const kind = normalizedEvidenceKind(row);
1224
+ if (evidenceKindMatches(kind, ["screenshot", "image", "visual"])) return "screenshot";
1225
+ if (evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation", "app-automation"]))
1226
+ return "automation";
1227
+ if (evidenceKindMatches(kind, ["pty", "tui", "terminal-capture"])) return "pty";
1228
+ return null;
1229
+ }
1230
+
1231
+ async function validateStructuralArtifact(
1232
+ cwd: string,
1233
+ row: JsonObject,
1234
+ fieldName: string,
1235
+ options: { surfaceFamily: SurfaceFamily; live: boolean },
1236
+ ): Promise<boolean> {
1237
+ void options.live;
1238
+ const kind = structuralArtifactKind(row);
1239
+ if (!kind) return false;
1240
+ if (kind === "screenshot") return validateScreenshotArtifact(cwd, row, fieldName);
1241
+ if (kind === "automation") return validateAutomationTranscriptArtifact(cwd, row, fieldName, options);
1242
+ if (kind === "pty") return validatePtyCaptureArtifact(cwd, row, fieldName);
1243
+ return false;
1244
+ }
1245
+
1246
+ const CLI_REPLAY_MAX_OUTPUT_BYTES = 1024 * 1024;
1247
+ const CLI_REPLAY_DEFAULT_TIMEOUT_MS = 10_000;
1248
+ const CLI_REPLAY_MIN_TIMEOUT_MS = 1_000;
1249
+ const CLI_REPLAY_MAX_TIMEOUT_MS = 30_000;
1250
+ const CLI_REPLAY_EXEMPT_REASON_CODES = new Set([
1251
+ "unsafe_side_effect",
1252
+ "requires_credentials",
1253
+ "requires_network",
1254
+ "non_deterministic_external",
1255
+ "destructive",
1256
+ "interactive_only",
1257
+ "platform_unavailable",
1258
+ ]);
1259
+ const CLI_REPLAY_ENV_BASE: Record<string, string> = { CI: "1", NO_COLOR: "1", GJC_ULTRAGOAL_REPLAY: "1" };
1260
+ const CLI_REPLAY_SAFE_ENV_NAMES = new Set(["LANG", "LC_ALL", "LC_CTYPE", "TZ"]);
1261
+ const CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN =
1262
+ /^(?:NODE_OPTIONS|GIT_EXTERNAL_DIFF|GIT_SSH|GIT_SSH_COMMAND|GIT_PAGER|PATH|LD_PRELOAD|LD_LIBRARY_PATH)$|^(?:GIT_CONFIG|DYLD_|BUN_|NPM_CONFIG_)|(?:^|_)OPTIONS$|PRELOAD$/;
1263
+ const ANSI_ESCAPE_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
1264
+
1265
+ function clampCliReplayTimeout(value: unknown): number {
1266
+ if (value === undefined) return CLI_REPLAY_DEFAULT_TIMEOUT_MS;
1267
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1268
+ throw new Error("qualityGate CLI replay timeoutMs must be a finite number");
1269
+ }
1270
+ return Math.min(CLI_REPLAY_MAX_TIMEOUT_MS, Math.max(CLI_REPLAY_MIN_TIMEOUT_MS, Math.trunc(value)));
1271
+ }
1272
+
1273
+ function basenameCommand(value: string): string {
1274
+ return path.basename(value).toLowerCase();
1275
+ }
1276
+
1277
+ function isDeterministicConsoleLogReplay(code: string): boolean {
1278
+ let remaining = code.trim();
1279
+ if (remaining.length === 0) return false;
1280
+ let matched = false;
1281
+ while (remaining.length > 0) {
1282
+ const match =
1283
+ /^console\.log\(\s*("(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\$])*`)\s*\)\s*;?\s*/.exec(
1284
+ remaining,
1285
+ );
1286
+ if (!match) return false;
1287
+ const statement = match[0]!;
1288
+ const literal = match[1]!;
1289
+ if (literal.startsWith("`") && literal.includes("${")) return false;
1290
+ matched = true;
1291
+ remaining = remaining.slice(statement.length);
1292
+ }
1293
+ return matched;
1294
+ }
1295
+
1296
+ function hasShellRedirectionToken(value: string): boolean {
1297
+ return /^(?:[<>]|\d?[<>]|\d?>&\d|\|\|?|&&|;)$/.test(value) || /(?:^|[^\w])-?>/.test(value);
1298
+ }
1299
+
1300
+ function isSafeRefOrPathspec(value: string): boolean {
1301
+ return value.length > 0 && !value.startsWith("-") && !/[\0\n\r]/.test(value) && !hasShellRedirectionToken(value);
1302
+ }
1303
+
1304
+ function isAllowedGitReplayCommand(args: readonly string[]): boolean {
1305
+ const subcommand = args[0];
1306
+ const rest = args.slice(1);
1307
+ if (subcommand === "status") return rest.every(arg => ["--short", "--porcelain", "--branch"].includes(arg));
1308
+ if (subcommand === "rev-parse" || subcommand === "merge-base")
1309
+ return rest.length > 0 && rest.every(isSafeRefOrPathspec);
1310
+ if (subcommand !== "diff" && subcommand !== "show" && subcommand !== "log") return false;
1311
+ let pathspecMode = false;
1312
+ for (const arg of rest) {
1313
+ if (arg === "--") {
1314
+ pathspecMode = true;
1315
+ continue;
1316
+ }
1317
+ if (pathspecMode) {
1318
+ if (!isSafeRefOrPathspec(arg)) return false;
1319
+ continue;
1320
+ }
1321
+ if (["--stat", "--name-only", "--oneline", "--no-ext-diff"].includes(arg)) continue;
1322
+ if (!isSafeRefOrPathspec(arg)) return false;
1323
+ }
1324
+ return true;
1325
+ }
1326
+
1327
+ function isBareExecutableName(value: string): boolean {
1328
+ // The allowlist is keyed on the basename, but the raw command[0] is what gets spawned.
1329
+ // Reject path-qualified or case-spoofed executables (e.g. ./git, /tmp/npm, scripts/node, GIT)
1330
+ // so an attacker-controlled binary cannot impersonate a trusted tool.
1331
+ return (
1332
+ value.length > 0 &&
1333
+ !value.includes("/") &&
1334
+ !value.includes("\\") &&
1335
+ value === path.basename(value) &&
1336
+ value === value.toLowerCase()
1337
+ );
1338
+ }
1339
+
1340
+ function isAllowedCliReplayCommand(command: readonly string[]): boolean {
1341
+ if (
1342
+ command.length === 0 ||
1343
+ command.some(arg => arg.trim() !== arg || arg.length === 0 || hasShellRedirectionToken(arg))
1344
+ )
1345
+ return false;
1346
+ if (!isBareExecutableName(command[0]!)) return false;
1347
+ const executable = basenameCommand(command[0]!);
1348
+ const args = command.slice(1);
1349
+ if (executable === "bun" || executable === "node") {
1350
+ if (args.length === 1 && args[0] === "--version") return true;
1351
+ return args.length === 2 && args[0] === "-e" && isDeterministicConsoleLogReplay(args[1]!);
1352
+ }
1353
+ if (executable === "npm" || executable === "pnpm" || executable === "yarn") {
1354
+ return (args.length === 1 && args[0] === "--version") || (args.length === 1 && args[0] === "list");
1355
+ }
1356
+ if (executable === "git") return isAllowedGitReplayCommand(args);
1357
+ if (executable === "gjc") return args.length === 1 && ["read", "status"].includes(args[0] ?? "");
1358
+ return false;
1359
+ }
1360
+
1361
+ function resolveCliReplayCommand(command: string[]): string[] {
1362
+ if (basenameCommand(command[0]!) === "bun") return [process.execPath, ...command.slice(1)];
1363
+ return command;
1364
+ }
1365
+
1366
+ function resolveUnderCwd(cwd: string, replayCwd: unknown, fieldName: string): string {
1367
+ const relative = replayCwd === undefined ? "." : nonEmptyString(replayCwd);
1368
+ if (!relative) throw new Error(`qualityGate ${fieldName}.cwd must be a non-empty string when provided`);
1369
+ const root = path.resolve(cwd);
1370
+ const resolved = path.resolve(root, relative);
1371
+ const relativeToRoot = path.relative(root, resolved);
1372
+ if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot)) {
1373
+ throw new Error(`qualityGate ${fieldName}.cwd must resolve under the repository cwd`);
1374
+ }
1375
+ return resolved;
1376
+ }
1377
+
1378
+ function buildCliReplayEnv(value: unknown, fieldName: string): Record<string, string> {
1379
+ const env: Record<string, string> = { ...CLI_REPLAY_ENV_BASE };
1380
+ if (value === undefined) return env;
1381
+ const object = requireQualityGateObject(value, `${fieldName}.env`);
1382
+ for (const [key, envValue] of Object.entries(object)) {
1383
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(key))
1384
+ throw new Error(`qualityGate ${fieldName}.env.${key} must be an uppercase environment key`);
1385
+ if (CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN.test(key) || !CLI_REPLAY_SAFE_ENV_NAMES.has(key)) {
1386
+ throw new Error(`qualityGate ${fieldName}.env.${key} is not in the CLI replay safe environment allowlist`);
1387
+ }
1388
+ if (typeof envValue !== "string") throw new Error(`qualityGate ${fieldName}.env.${key} must be a string`);
1389
+ env[key] = envValue;
1390
+ }
1391
+ return env;
1392
+ }
1393
+
1394
+ function normalizeCliReplayOutput(value: string, cwd: string): string {
1395
+ let normalized = value.replace(ANSI_ESCAPE_PATTERN, "").replace(/\r\n?/g, "\n");
1396
+ const home = process.env.HOME;
1397
+ const replacements: Array<[RegExp, string]> = [
1398
+ [/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g, "<TIMESTAMP>"],
1399
+ [/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, "<UUID>"],
1400
+ [/\b[0-9a-f]{7,}\b/gi, "<HASH>"],
1401
+ [/(?:\/private)?\/var\/folders\/[^\s"']+|\/tmp\/[^\s"']+|\/var\/tmp\/[^\s"']+/g, "<TMP>"],
1402
+ ];
1403
+ for (const candidate of [path.resolve(cwd), home]) {
1404
+ if (!candidate) continue;
1405
+ const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1406
+ normalized = normalized.replace(new RegExp(escaped, "g"), candidate === home ? "<HOME>" : "<CWD>");
1407
+ }
1408
+ for (const [pattern, replacement] of replacements) normalized = normalized.replace(pattern, replacement);
1409
+ const lines = normalized.split("\n").map(line => line.replace(/[ \t]+$/g, ""));
1410
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
1411
+ return lines.join("\n");
1412
+ }
1413
+
1414
+ async function readCliReplayRecord(cwd: string, row: JsonObject, fieldName: string): Promise<JsonObject | null> {
1415
+ const inline = qualityGateObject(row.replay) ?? (row.kind === "cli-replay" ? row : null);
1416
+ if (inline) return inline;
1417
+ if (!evidenceKindMatches(normalizedEvidenceKind(row), ["cli-replay", "command-replay"])) return null;
1418
+ const bytes = await readArtifactBytes(cwd, row, fieldName);
1419
+ if (!bytes) return null;
1420
+ try {
1421
+ return requireQualityGateObject(JSON.parse(bytes.toString("utf8")), `${fieldName}.replay`);
1422
+ } catch (error) {
1423
+ throw new Error(`qualityGate ${fieldName} CLI replay artifact must be valid JSON: ${String(error)}`);
1424
+ }
1425
+ }
1426
+
1427
+ function parseCliReplayRecord(
1428
+ record: JsonObject,
1429
+ fieldName: string,
1430
+ ): {
1431
+ command: string[];
1432
+ replayCwd: unknown;
1433
+ env: Record<string, string>;
1434
+ timeoutMs: number;
1435
+ expectedExitCode: number;
1436
+ recordedStdout: string;
1437
+ invariants: JsonObject[];
1438
+ } {
1439
+ if (record.schemaVersion !== 1) throw new Error(`qualityGate ${fieldName}.schemaVersion must be 1`);
1440
+ if (record.kind !== "cli-replay") throw new Error(`qualityGate ${fieldName}.kind must be cli-replay`);
1441
+ if (record.command !== undefined && typeof record.command === "string") {
1442
+ throw new Error(`qualityGate ${fieldName}.command must be an argv string array, not a shell string`);
1443
+ }
1444
+ const command = nonEmptyStringArray(record.command);
1445
+ if (!command) throw new Error(`qualityGate ${fieldName}.command must be a non-empty string array`);
1446
+ if (record.replaySafe !== true)
1447
+ throw new Error(`qualityGate ${fieldName}.replaySafe must be true before CLI replay executes`);
1448
+ if (!isAllowedCliReplayCommand(command))
1449
+ throw new Error(`qualityGate ${fieldName}.command is not in the conservative CLI replay allowlist`);
1450
+ if (record.normalization !== undefined && record.normalization !== "default") {
1451
+ throw new Error(`qualityGate ${fieldName}.normalization must be default when provided`);
1452
+ }
1453
+ if (typeof record.recordedStdout !== "string")
1454
+ throw new Error(`qualityGate ${fieldName}.recordedStdout must be a string`);
1455
+ if (record.recordedStderr !== undefined && typeof record.recordedStderr !== "string") {
1456
+ throw new Error(`qualityGate ${fieldName}.recordedStderr must be a string when provided`);
1457
+ }
1458
+ const expectedExitCode = record.expectedExitCode === undefined ? 0 : record.expectedExitCode;
1459
+ if (typeof expectedExitCode !== "number" || !Number.isInteger(expectedExitCode)) {
1460
+ throw new Error(`qualityGate ${fieldName}.expectedExitCode must be an integer`);
1461
+ }
1462
+ const invariants =
1463
+ record.invariants === undefined ? [] : requireObjectArray(record.invariants, `${fieldName}.invariants`);
1464
+ return {
1465
+ command: command.map(item => item.trim()),
1466
+ replayCwd: record.cwd,
1467
+ env: buildCliReplayEnv(record.env, fieldName),
1468
+ timeoutMs: clampCliReplayTimeout(record.timeoutMs),
1469
+ expectedExitCode,
1470
+ recordedStdout: record.recordedStdout,
1471
+ invariants,
1472
+ };
1473
+ }
1474
+
1475
+ function validateCliReplayInvariants(invariants: JsonObject[], stdout: string, fieldName: string): void {
1476
+ for (const [index, invariant] of invariants.entries()) {
1477
+ const invariantField = `${fieldName}.invariants[${index}]`;
1478
+ const type = requiredStringField(invariant, "type", invariantField);
1479
+ const value = requiredStringField(invariant, "value", invariantField);
1480
+ if (type === "substring" && !stdout.includes(value))
1481
+ throw new Error(`qualityGate ${invariantField} substring invariant did not match stdout`);
1482
+ else if (type === "not_substring" && stdout.includes(value))
1483
+ throw new Error(`qualityGate ${invariantField} not_substring invariant matched stdout`);
1484
+ else if (type === "regex") {
1485
+ const flags = invariant.flags === undefined ? "" : requiredStringField(invariant, "flags", invariantField);
1486
+ if (!/^[im]*$/.test(flags)) throw new Error(`qualityGate ${invariantField}.flags may only contain i and m`);
1487
+ if (!new RegExp(value, flags).test(stdout))
1488
+ throw new Error(`qualityGate ${invariantField} regex invariant did not match stdout`);
1489
+ } else if (type !== "substring" && type !== "not_substring") {
1490
+ throw new Error(`qualityGate ${invariantField}.type must be substring, regex, or not_substring`);
1491
+ }
1492
+ }
1493
+ }
1494
+
1495
+ async function collectCliReplayOutput(
1496
+ stream: ReadableStream<Uint8Array> | null,
1497
+ ): Promise<{ text: string; truncated: boolean }> {
1498
+ if (!stream) return { text: "", truncated: false };
1499
+ const reader = stream.getReader();
1500
+ const chunks: Buffer[] = [];
1501
+ let size = 0;
1502
+ let truncated = false;
1503
+ try {
1504
+ while (true) {
1505
+ const { done, value } = await reader.read();
1506
+ if (done) break;
1507
+ if (size < CLI_REPLAY_MAX_OUTPUT_BYTES) {
1508
+ const remaining = CLI_REPLAY_MAX_OUTPUT_BYTES - size;
1509
+ const chunk = Buffer.from(value.subarray(0, remaining));
1510
+ chunks.push(chunk);
1511
+ size += chunk.length;
1512
+ }
1513
+ if (value.length > 0 && size >= CLI_REPLAY_MAX_OUTPUT_BYTES) {
1514
+ truncated = true;
1515
+ await reader.cancel().catch(() => undefined);
1516
+ break;
1517
+ }
1518
+ }
1519
+ } finally {
1520
+ reader.releaseLock();
1521
+ }
1522
+ return { text: Buffer.concat(chunks).toString("utf8"), truncated };
1523
+ }
1524
+
1525
+ export interface ReplayProcessHandle {
1526
+ readonly exited: Promise<number>;
1527
+ kill(signal?: number | NodeJS.Signals): void;
1528
+ }
1529
+
1530
+ export async function waitForReplayProcessWithTimeout(
1531
+ process: ReplayProcessHandle,
1532
+ timeoutMs: number,
1533
+ graceMs = 2000,
1534
+ ): Promise<number> {
1535
+ let timeoutTimer: NodeJS.Timeout | undefined;
1536
+ let graceTimer: NodeJS.Timeout | undefined;
1537
+ const timedOut = Symbol("timedOut");
1538
+ const timeout = new Promise<typeof timedOut>(resolve => {
1539
+ timeoutTimer = setTimeout(() => resolve(timedOut), timeoutMs);
1540
+ });
1541
+ const first = await Promise.race([process.exited, timeout]);
1542
+ if (first !== timedOut) {
1543
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1544
+ return first;
1545
+ }
1546
+ process.kill("SIGTERM");
1547
+ const killed = Symbol("killed");
1548
+ const grace = new Promise<typeof killed>(resolve => {
1549
+ graceTimer = setTimeout(() => {
1550
+ process.kill("SIGKILL");
1551
+ resolve(killed);
1552
+ }, graceMs);
1553
+ });
1554
+ await Promise.race([process.exited, grace]);
1555
+ await process.exited.catch(() => undefined);
1556
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1557
+ if (graceTimer) clearTimeout(graceTimer);
1558
+ throw new Error("timeout");
1559
+ }
1560
+
1561
+ async function validateReplayExemptFallback(
1562
+ cwd: string,
1563
+ record: JsonObject,
1564
+ fieldName: string,
1565
+ artifactRefs: Map<string, JsonObject>,
1566
+ options: { surfaceFamily: SurfaceFamily; live: boolean },
1567
+ ): Promise<boolean> {
1568
+ const exempt = qualityGateObject(record.replayExempt);
1569
+ if (!exempt) return false;
1570
+ const reasonCode = requiredStringField(exempt, "reasonCode", `${fieldName}.replayExempt`);
1571
+ if (!CLI_REPLAY_EXEMPT_REASON_CODES.has(reasonCode))
1572
+ throw new Error(`qualityGate ${fieldName}.replayExempt.reasonCode is not recognized`);
1573
+ const reason = requiredStringField(exempt, "reason", `${fieldName}.replayExempt`);
1574
+ if (!isSubstantiveEvidence(reason) || reason.length < 30)
1575
+ throw new Error(`qualityGate ${fieldName}.replayExempt.reason must be audited and substantive`);
1576
+ requiredStringField(exempt, "approvedBy", `${fieldName}.replayExempt`);
1577
+ const fallbackRefs = requireStringLinks(
1578
+ exempt.fallbackArtifactRefs,
1579
+ `${fieldName}.replayExempt.fallbackArtifactRefs`,
1580
+ );
1581
+ requireResolvedLinks(fallbackRefs, artifactRefs, `${fieldName}.replayExempt.fallbackArtifactRefs`);
1582
+ let validFallback = false;
1583
+ for (const fallbackRef of fallbackRefs) {
1584
+ if (fallbackRef === requiredStringField(record, "id", fieldName)) {
1585
+ throw new Error(`qualityGate ${fieldName}.replayExempt fallback must not reference the replay record itself`);
1586
+ }
1587
+ const fallback = artifactRefs.get(fallbackRef)!;
1588
+ if (await validateStructuralArtifact(cwd, fallback, `executorQa.artifactRefs.${fallbackRef}`, options))
1589
+ validFallback = true;
1590
+ }
1591
+ if (!validFallback)
1592
+ throw new Error(
1593
+ `qualityGate ${fieldName}.replayExempt requires at least one structurally-valid fallback artifact`,
1594
+ );
1595
+ return true;
1596
+ }
1597
+ async function validateCliReplay(
1598
+ cwd: string,
1599
+ row: JsonObject,
1600
+ fieldName: string,
1601
+ options: { live: boolean },
1602
+ ): Promise<boolean> {
1603
+ const record = await readCliReplayRecord(cwd, row, fieldName);
1604
+ if (!record) return false;
1605
+ if (record.replayExempt !== undefined) {
1606
+ throw new Error(
1607
+ `qualityGate ${fieldName}.replayExempt can only be validated from surfaceEvidence with fallback context`,
1608
+ );
1609
+ }
1610
+ void options.live;
1611
+ const replay = parseCliReplayRecord(record, fieldName);
1612
+ const replayCwd = resolveUnderCwd(cwd, replay.replayCwd, fieldName);
1613
+ const process = Bun.spawn(resolveCliReplayCommand(replay.command), {
1614
+ cwd: replayCwd,
1615
+ env: replay.env,
1616
+ stdout: "pipe",
1617
+ stderr: "pipe",
1618
+ });
1619
+ try {
1620
+ const [stdout, stderr, exitCode] = await Promise.all([
1621
+ collectCliReplayOutput(process.stdout),
1622
+ collectCliReplayOutput(process.stderr),
1623
+ waitForReplayProcessWithTimeout(process, replay.timeoutMs),
1624
+ ]);
1625
+ if (stdout.truncated || stderr.truncated)
1626
+ throw new Error(`qualityGate ${fieldName} CLI replay output exceeded 1 MiB buffer cap`);
1627
+ if (exitCode !== replay.expectedExitCode) {
1628
+ throw new Error(
1629
+ `qualityGate ${fieldName} CLI replay exit code ${exitCode} did not match expected ${replay.expectedExitCode}`,
1630
+ );
1631
+ }
1632
+ const actualStdout = normalizeCliReplayOutput(stdout.text, cwd);
1633
+ const recordedStdout = normalizeCliReplayOutput(replay.recordedStdout, cwd);
1634
+ if (replay.invariants.length > 0) {
1635
+ validateCliReplayInvariants(replay.invariants, actualStdout, fieldName);
1636
+ } else if (actualStdout !== recordedStdout) {
1637
+ throw new Error(`qualityGate ${fieldName} CLI replay stdout did not match recordedStdout after normalization`);
1638
+ }
1639
+ return true;
1640
+ } catch (error) {
1641
+ if (error instanceof Error && error.message === "timeout") {
1642
+ throw new Error(`qualityGate ${fieldName} CLI replay timed out after ${replay.timeoutMs}ms`);
1643
+ }
1644
+ throw error;
1645
+ }
1646
+ }
1647
+
1648
+ async function hasLiveProofPresence(
1649
+ cwd: string,
1650
+ row: JsonObject,
1651
+ fieldName: string,
1652
+ family: SurfaceFamily,
1653
+ ): Promise<boolean> {
1654
+ if (await hasExistingNonEmptyArtifact(cwd, row.path)) return true;
1655
+ if (family === "cli") {
1656
+ const record = await readCliReplayRecord(cwd, row, fieldName);
1657
+ if (record) return true;
1658
+ }
1659
+ return false;
1660
+ }
1661
+
1662
+ async function validateLiveSurfaceProofPresence(
1663
+ cwd: string,
1664
+ family: SurfaceFamily,
1665
+ artifactIds: string[],
1666
+ artifactRefs: Map<string, JsonObject>,
1667
+ ): Promise<void> {
1668
+ if (!isLiveSurfaceFamily(family)) return;
1669
+ for (const artifactId of artifactIds) {
1670
+ if (
1671
+ await hasLiveProofPresence(cwd, artifactRefs.get(artifactId)!, `executorQa.artifactRefs.${artifactId}`, family)
1672
+ )
1673
+ return;
1674
+ }
883
1675
  throw new Error(
884
- `qualityGate ${fieldName} must reference an existing non-empty artifact path, substantive inlineEvidence, or a typed verifiedReceipt`,
1676
+ `qualityGate ${artifactIds.map(id => `executorQa.artifactRefs.${id}`).join(", ")} must reference a live proof artifact, structural capture, or CLI replay; inlineEvidence and typed verifiedReceipt do not prove live surfaces`,
885
1677
  );
886
1678
  }
1679
+ async function validateSurfaceStructuralRequirement(
1680
+ cwd: string,
1681
+ family: SurfaceFamily,
1682
+ artifactIds: string[],
1683
+ artifactRefs: Map<string, JsonObject>,
1684
+ fieldName: string,
1685
+ ): Promise<void> {
1686
+ if (family !== "web" && family !== "native") return;
1687
+ let hasScreenshot = false;
1688
+ let hasAutomation = false;
1689
+ let hasPty = false;
1690
+ for (const artifactId of artifactIds) {
1691
+ const artifact = artifactRefs.get(artifactId)!;
1692
+ const kind = structuralArtifactKind(artifact);
1693
+ if (!kind) continue;
1694
+ const valid = await validateStructuralArtifact(cwd, artifact, `executorQa.artifactRefs.${artifactId}`, {
1695
+ surfaceFamily: family,
1696
+ live: true,
1697
+ });
1698
+ if (kind === "screenshot" && valid) hasScreenshot = true;
1699
+ if (kind === "automation" && valid) hasAutomation = true;
1700
+ if (kind === "pty" && valid) hasPty = true;
1701
+ }
1702
+ if (family === "web" && (!hasScreenshot || !hasAutomation)) {
1703
+ throw new Error(
1704
+ `qualityGate ${fieldName} for GUI/web surfaces must include a valid automation transcript and non-uniform screenshot`,
1705
+ );
1706
+ }
1707
+ if (family === "native" && !hasScreenshot && !hasAutomation && !hasPty) {
1708
+ throw new Error(
1709
+ `qualityGate ${fieldName} for native surfaces must include a valid screenshot, PTY capture, or app-automation transcript`,
1710
+ );
1711
+ }
1712
+ }
1713
+
1714
+ async function validateArtifactProof(
1715
+ cwd: string,
1716
+ row: JsonObject,
1717
+ fieldName: string,
1718
+ options: { surfaceFamily: SurfaceFamily; live: boolean },
1719
+ ): Promise<void> {
1720
+ if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
1721
+ if (await validateStructuralArtifact(cwd, row, fieldName, options)) return;
1722
+ if (options.surfaceFamily === "cli" && (await validateCliReplay(cwd, row, fieldName, { live: options.live })))
1723
+ return;
1724
+ if (!options.live && (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt))) return;
1725
+ const proofLabel = options.live
1726
+ ? "a live proof artifact, structural capture, or CLI replay; inlineEvidence and typed verifiedReceipt do not prove live surfaces"
1727
+ : "an existing non-empty artifact path or a typed verifiedReceipt; inlineEvidence alone is not sufficient";
1728
+ throw new Error(`qualityGate ${fieldName} must reference ${proofLabel}`);
1729
+ }
887
1730
 
888
1731
  async function validateArtifactRefs(cwd: string, executorQa: JsonObject): Promise<Map<string, JsonObject>> {
1732
+ void cwd;
889
1733
  const rows = requireObjectArray(executorQa.artifactRefs, "executorQa.artifactRefs");
890
1734
  const idMap = buildRowIdMap(rows, "executorQa.artifactRefs");
891
1735
  for (const [index, row] of rows.entries()) {
892
1736
  const fieldName = `executorQa.artifactRefs[${index}]`;
893
1737
  requiredStringField(row, "kind", fieldName);
894
1738
  requiredStringField(row, "description", fieldName);
895
- await requireSubstantiveArtifactEvidence(cwd, row, fieldName);
896
1739
  }
897
1740
  return idMap;
898
1741
  }
899
1742
 
900
- function validateSurfaceEvidence(
1743
+ async function validateSurfaceEvidence(
1744
+ cwd: string,
901
1745
  executorQa: JsonObject,
902
1746
  artifactRefs: Map<string, JsonObject>,
903
- ): Map<string, JsonObject> {
1747
+ ): Promise<Map<string, JsonObject>> {
904
1748
  const rows = requireObjectArray(executorQa.surfaceEvidence, "executorQa.surfaceEvidence");
905
1749
  const idMap = buildRowIdMap(rows, "executorQa.surfaceEvidence");
906
1750
  for (const [index, row] of rows.entries()) {
@@ -912,6 +1756,7 @@ function validateSurfaceEvidence(
912
1756
  continue;
913
1757
  }
914
1758
  const surface = requiredStringField(row, "surface", fieldName);
1759
+ const family = surfaceFamily(surface);
915
1760
  requireSuccessfulRowOutcome(row, fieldName);
916
1761
  requiredStringField(row, "invocation", fieldName);
917
1762
  if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
@@ -919,7 +1764,49 @@ function validateSurfaceEvidence(
919
1764
  }
920
1765
  const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
921
1766
  requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
1767
+ await validateLiveSurfaceProofPresence(cwd, family, artifactIds, artifactRefs);
922
1768
  validateSurfaceArtifactCompatibility(surface, artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
1769
+ await validateSurfaceStructuralRequirement(cwd, family, artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
1770
+ if (family === "cli") {
1771
+ let hasPassingReplay = false;
1772
+ for (const artifactId of artifactIds) {
1773
+ const artifact = artifactRefs.get(artifactId)!;
1774
+ const artifactField = `executorQa.artifactRefs.${artifactId}`;
1775
+ const record = await readCliReplayRecord(cwd, artifact, artifactField);
1776
+ if (!record) continue;
1777
+ if (record.replayExempt !== undefined) {
1778
+ if (
1779
+ await validateReplayExemptFallback(cwd, { ...record, id: artifactId }, artifactField, artifactRefs, {
1780
+ surfaceFamily: family,
1781
+ live: true,
1782
+ })
1783
+ ) {
1784
+ hasPassingReplay = true;
1785
+ }
1786
+ } else if (await validateCliReplay(cwd, artifact, artifactField, { live: true })) {
1787
+ hasPassingReplay = true;
1788
+ }
1789
+ }
1790
+ if (!hasPassingReplay) {
1791
+ throw new Error(
1792
+ `qualityGate ${fieldName} for CLI surfaces must include a passing argv CLI replay or valid replayExempt fallback`,
1793
+ );
1794
+ }
1795
+ }
1796
+ for (const artifactId of artifactIds) {
1797
+ if (family === "cli") {
1798
+ const record = await readCliReplayRecord(
1799
+ cwd,
1800
+ artifactRefs.get(artifactId)!,
1801
+ `executorQa.artifactRefs.${artifactId}`,
1802
+ );
1803
+ if (record?.replayExempt !== undefined) continue;
1804
+ }
1805
+ await validateArtifactProof(cwd, artifactRefs.get(artifactId)!, `executorQa.artifactRefs.${artifactId}`, {
1806
+ surfaceFamily: family,
1807
+ live: isLiveSurfaceFamily(family),
1808
+ });
1809
+ }
923
1810
  }
924
1811
  return idMap;
925
1812
  }
@@ -1007,13 +1894,29 @@ function validateContractCoverage(
1007
1894
  }
1008
1895
  }
1009
1896
 
1010
- async function validateExecutorQaRedTeamEvidence(cwd: string, executorQa: JsonObject): Promise<void> {
1897
+ async function validateExecutorQaRedTeamEvidenceInternal(
1898
+ cwd: string,
1899
+ executorQa: JsonObject,
1900
+ _options: { mode?: "checkpoint" | "review" } = {},
1901
+ ): Promise<void> {
1011
1902
  const artifactRefs = await validateArtifactRefs(cwd, executorQa);
1012
- const surfaceEvidence = validateSurfaceEvidence(executorQa, artifactRefs);
1903
+ const surfaceEvidence = await validateSurfaceEvidence(cwd, executorQa, artifactRefs);
1013
1904
  const adversarialCases = validateAdversarialCases(executorQa, artifactRefs);
1014
1905
  validateContractCoverage(executorQa, surfaceEvidence, adversarialCases, artifactRefs);
1015
1906
  }
1016
1907
 
1908
+ async function validateExecutorQaRedTeamEvidence(cwd: string, executorQa: JsonObject): Promise<void> {
1909
+ await validateExecutorQaRedTeamEvidenceInternal(cwd, executorQa, { mode: "checkpoint" });
1910
+ }
1911
+
1912
+ export async function validateExecutorQaRedTeamEvidenceForReview(
1913
+ cwd: string,
1914
+ executorQa: Record<string, unknown>,
1915
+ options: { mode?: "review" } = {},
1916
+ ): Promise<void> {
1917
+ await validateExecutorQaRedTeamEvidenceInternal(cwd, executorQa as JsonObject, options);
1918
+ }
1919
+
1017
1920
  async function validateCompletionQualityGate(cwd: string, gate: JsonObject): Promise<void> {
1018
1921
  const codeReview = qualityGateObject(gate.codeReview);
1019
1922
  if (codeReview) {
@@ -1162,6 +2065,26 @@ export async function checkpointUltragoalGoal(input: {
1162
2065
  if (!goal) throw new Error(`No ultragoal goal found for ${input.goalId}.`);
1163
2066
  const evidence = input.evidence.trim();
1164
2067
  if (!evidence) throw new Error("checkpoint evidence is required");
2068
+ const ledgerBefore = await readUltragoalLedger(input.cwd);
2069
+ if (
2070
+ goal.status === input.status &&
2071
+ goal.evidence === evidence &&
2072
+ ledgerBefore.some(
2073
+ event =>
2074
+ event.event === "goal_checkpointed" &&
2075
+ event.goalId === goal.id &&
2076
+ event.status === input.status &&
2077
+ event.evidence === evidence,
2078
+ )
2079
+ ) {
2080
+ // Idempotent re-checkpoint: this goal is already recorded in the target status with the same
2081
+ // evidence, so skip the plan rewrite and ledger append to avoid duplicate goal_checkpointed
2082
+ // events. The ledger is the dedup source of truth because it is exactly what a duplicate write
2083
+ // would corrupt (mirrors the ralplan #638 guard). Requiring a matching ledger row means an
2084
+ // interrupted prior write (plan persisted, ledger append lost) still re-appends the event
2085
+ // instead of silently dropping it.
2086
+ return plan;
2087
+ }
1165
2088
  const qualityGateJson =
1166
2089
  input.status === "complete"
1167
2090
  ? await readRequiredCompletionQualityGate(input.cwd, input.qualityGateJson)
@@ -1169,7 +2092,6 @@ export async function checkpointUltragoalGoal(input: {
1169
2092
  ? await readStructuredValue(input.cwd, input.qualityGateJson)
1170
2093
  : undefined;
1171
2094
  const now = new Date().toISOString();
1172
- const ledgerBefore = await readUltragoalLedger(input.cwd);
1173
2095
  const beforeStatus = goal.status;
1174
2096
  if (input.status === "complete") {
1175
2097
  const blockedGoalId =
@@ -1689,6 +2611,244 @@ export async function recordUltragoalReviewBlockers(input: {
1689
2611
  return plan;
1690
2612
  }
1691
2613
 
2614
+ type UltragoalReviewMode = "review-only" | "review-start";
2615
+ type UltragoalReviewContractStrength = "strong" | "thin-derived";
2616
+
2617
+ interface UltragoalReviewFinding extends JsonObject {
2618
+ severity: "blocker";
2619
+ message: string;
2620
+ }
2621
+
2622
+ interface UltragoalReviewResult extends JsonObject {
2623
+ verdict: "pass" | "fail" | "inconclusive: weak-contract";
2624
+ contractStrength: UltragoalReviewContractStrength;
2625
+ cleanPassEligible: boolean;
2626
+ source: JsonObject;
2627
+ findings: UltragoalReviewFinding[];
2628
+ artifactValidationSummary: JsonObject;
2629
+ weakContractCapApplied: boolean;
2630
+ blockerGoalIds?: string[];
2631
+ }
2632
+
2633
+ function parseReviewMode(value: string | undefined): UltragoalReviewMode {
2634
+ if (value === undefined || value === "review-only") return "review-only";
2635
+ if (value === "review-start") return "review-start";
2636
+ throw new Error("review --mode must be review-only or review-start");
2637
+ }
2638
+
2639
+ async function readOptionalExecutorQa(cwd: string, value: string | undefined): Promise<JsonObject> {
2640
+ if (!value) {
2641
+ return {
2642
+ status: "passed",
2643
+ e2eStatus: "passed",
2644
+ redTeamStatus: "passed",
2645
+ evidence: "review evidence bundle was not supplied; runtime reports this as a finding",
2646
+ e2eCommands: ["gjc ultragoal review"],
2647
+ redTeamCommands: ["gjc ultragoal review"],
2648
+ artifactRefs: [],
2649
+ contractCoverage: [],
2650
+ surfaceEvidence: [],
2651
+ adversarialCases: [],
2652
+ blockers: [],
2653
+ };
2654
+ }
2655
+ const structured = await readStructuredValue(cwd, value);
2656
+ if (typeof structured !== "object" || structured === null || Array.isArray(structured)) {
2657
+ throw new Error("review --executor-qa-json must resolve to an executorQa object");
2658
+ }
2659
+ return structured as JsonObject;
2660
+ }
2661
+
2662
+ async function spawnText(
2663
+ command: string[],
2664
+ options: { cwd: string; timeoutMs?: number },
2665
+ ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
2666
+ try {
2667
+ const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
2668
+ const timeout = setTimeout(() => proc.kill(), options.timeoutMs ?? 5000);
2669
+ const [stdout, stderr, exitCode] = await Promise.all([
2670
+ new Response(proc.stdout).text(),
2671
+ new Response(proc.stderr).text(),
2672
+ proc.exited,
2673
+ ]);
2674
+ clearTimeout(timeout);
2675
+ return { ok: exitCode === 0, stdout, stderr };
2676
+ } catch (error) {
2677
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
2678
+ }
2679
+ }
2680
+
2681
+ async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
2682
+ const candidates = branch ? [branch] : ["origin/main", "origin/master", "main", "master"];
2683
+ for (const candidate of candidates) {
2684
+ const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
2685
+ if (exists.ok) return candidate;
2686
+ }
2687
+ const mergeBase = await spawnText(["git", "merge-base", "HEAD", "origin/main"], { cwd, timeoutMs: 3000 });
2688
+ if (mergeBase.ok && mergeBase.stdout.trim()) return mergeBase.stdout.trim();
2689
+ return "HEAD";
2690
+ }
2691
+
2692
+ async function localDiffSource(cwd: string, sourceKind: string, branch?: string): Promise<JsonObject> {
2693
+ if (sourceKind === "worktree") {
2694
+ const [status, diff] = await Promise.all([
2695
+ spawnText(["git", "status", "--short"], { cwd, timeoutMs: 5000 }),
2696
+ spawnText(["git", "diff", "--stat"], { cwd, timeoutMs: 5000 }),
2697
+ ]);
2698
+ return { kind: "worktree", status: status.stdout, diffStat: diff.stdout };
2699
+ }
2700
+ const base = await resolveGitBase(cwd, branch);
2701
+ const diff = await spawnText(["git", "diff", "--stat", `${base}...HEAD`], { cwd, timeoutMs: 5000 });
2702
+ return { kind: sourceKind, base, branch, diffStat: diff.stdout };
2703
+ }
2704
+
2705
+ async function resolveReviewSource(
2706
+ cwd: string,
2707
+ args: readonly string[],
2708
+ specPath: string | undefined,
2709
+ ): Promise<{ contractStrength: UltragoalReviewContractStrength; source: JsonObject }> {
2710
+ if (specPath) {
2711
+ const absolute = path.resolve(cwd, specPath);
2712
+ return {
2713
+ contractStrength: "strong",
2714
+ source: { kind: "spec", path: specPath, contract: await Bun.file(absolute).text() },
2715
+ };
2716
+ }
2717
+ const pr = flagValue(args, "--pr");
2718
+ if (pr) {
2719
+ const [view, diff] = await Promise.all([
2720
+ spawnText(["gh", "pr", "view", pr, "--json", "title,body,baseRefName"], { cwd, timeoutMs: 5000 }),
2721
+ spawnText(["gh", "pr", "diff", pr], { cwd, timeoutMs: 5000 }),
2722
+ ]);
2723
+ if (view.ok && diff.ok)
2724
+ return {
2725
+ contractStrength: "thin-derived",
2726
+ source: { kind: "pr", pr, prSource: "gh", metadata: view.stdout, diff: diff.stdout },
2727
+ };
2728
+ return {
2729
+ contractStrength: "thin-derived",
2730
+ source: {
2731
+ kind: "pr",
2732
+ pr,
2733
+ prSource: "gh-unavailable",
2734
+ ghError: `${view.stderr}${diff.stderr}`.trim(),
2735
+ local: await localDiffSource(cwd, "pr-fallback"),
2736
+ },
2737
+ };
2738
+ }
2739
+ const branch = flagValue(args, "--branch");
2740
+ if (branch) return { contractStrength: "thin-derived", source: await localDiffSource(cwd, "branch", branch) };
2741
+ return { contractStrength: "thin-derived", source: await localDiffSource(cwd, "worktree") };
2742
+ }
2743
+
2744
+ function findingFromError(error: unknown): UltragoalReviewFinding {
2745
+ return { severity: "blocker", message: error instanceof Error ? error.message : String(error) };
2746
+ }
2747
+
2748
+ function executorQaBlockers(executorQa: JsonObject): UltragoalReviewFinding[] {
2749
+ const blockers = nonEmptyStringArray(executorQa.blockers);
2750
+ return (blockers ?? []).map(message => ({ severity: "blocker", message: `executorQa.blockers: ${message}` }));
2751
+ }
2752
+
2753
+ const RESOLVED_REVIEW_BLOCKER_STATUSES = new Set<UltragoalGoalStatus>(["complete", "superseded"]);
2754
+
2755
+ function findOpenReviewBlockerGoal(plan: UltragoalPlan, message: string): UltragoalGoal | undefined {
2756
+ const objective = message.trim();
2757
+ return plan.goals.find(
2758
+ goal =>
2759
+ goal.steering?.kind === "review_blocker" &&
2760
+ goal.objective.trim() === objective &&
2761
+ !RESOLVED_REVIEW_BLOCKER_STATUSES.has(goal.status),
2762
+ );
2763
+ }
2764
+
2765
+ async function recordReviewFindingGoals(cwd: string, findings: readonly UltragoalReviewFinding[]): Promise<string[]> {
2766
+ let plan = await readUltragoalPlan(cwd);
2767
+ const now = new Date().toISOString();
2768
+ if (!plan) {
2769
+ plan = {
2770
+ version: 1,
2771
+ gjcObjective: DEFAULT_ULTRAGOAL_OBJECTIVE,
2772
+ brief: "Ultragoal review-start findings",
2773
+ gjcGoalMode: "aggregate",
2774
+ createdAt: now,
2775
+ updatedAt: now,
2776
+ goals: [],
2777
+ };
2778
+ }
2779
+ const blockerGoalIds: string[] = [];
2780
+ const createdGoalIds: string[] = [];
2781
+ for (const finding of findings) {
2782
+ const existing = findOpenReviewBlockerGoal(plan, finding.message);
2783
+ if (existing) {
2784
+ if (!blockerGoalIds.includes(existing.id)) blockerGoalIds.push(existing.id);
2785
+ continue;
2786
+ }
2787
+ const id = nextUltragoalGoalId(plan);
2788
+ plan.goals.push({
2789
+ id,
2790
+ title: "Resolve ultragoal review finding",
2791
+ objective: finding.message,
2792
+ status: "pending",
2793
+ createdAt: now,
2794
+ updatedAt: now,
2795
+ steering: { kind: "review_blocker" },
2796
+ });
2797
+ blockerGoalIds.push(id);
2798
+ createdGoalIds.push(id);
2799
+ }
2800
+ if (createdGoalIds.length > 0) {
2801
+ plan.updatedAt = now;
2802
+ await writePlan(cwd, plan);
2803
+ await appendLedger(cwd, {
2804
+ event: "review_blockers_recorded",
2805
+ blockerGoalIds: createdGoalIds,
2806
+ findings: findings.map(finding => finding.message),
2807
+ });
2808
+ }
2809
+ return blockerGoalIds;
2810
+ }
2811
+
2812
+ export async function runUltragoalReview(cwd: string, args: readonly string[]): Promise<UltragoalReviewResult> {
2813
+ const mode = parseReviewMode(flagValue(args, "--mode"));
2814
+ const specPath = flagValue(args, "--spec");
2815
+ const { contractStrength, source } = await resolveReviewSource(cwd, args, specPath);
2816
+ const executorQa = await readOptionalExecutorQa(
2817
+ cwd,
2818
+ flagValue(args, "--executor-qa-json") ?? flagValue(args, "--executor-qa"),
2819
+ );
2820
+ const findings: UltragoalReviewFinding[] = [];
2821
+ try {
2822
+ await validateExecutorQaRedTeamEvidenceForReview(cwd, executorQa, { mode: "review" });
2823
+ } catch (error) {
2824
+ findings.push(findingFromError(error));
2825
+ }
2826
+ findings.push(...executorQaBlockers(executorQa));
2827
+ const weakContractCapApplied = contractStrength === "thin-derived";
2828
+ const cleanPassEligible = contractStrength === "strong" && findings.length === 0;
2829
+ const result: UltragoalReviewResult = {
2830
+ verdict: cleanPassEligible
2831
+ ? "pass"
2832
+ : weakContractCapApplied && findings.length === 0
2833
+ ? "inconclusive: weak-contract"
2834
+ : "fail",
2835
+ contractStrength,
2836
+ cleanPassEligible,
2837
+ source,
2838
+ findings,
2839
+ artifactValidationSummary: {
2840
+ validator: "validateExecutorQaRedTeamEvidenceForReview",
2841
+ mode: "review",
2842
+ passed: findings.length === 0,
2843
+ findingCount: findings.length,
2844
+ },
2845
+ weakContractCapApplied,
2846
+ };
2847
+ if (mode === "review-start" && findings.length > 0)
2848
+ result.blockerGoalIds = await recordReviewFindingGoals(cwd, findings);
2849
+ return result;
2850
+ }
2851
+
1692
2852
  function flagValue(args: readonly string[], flag: string): string | undefined {
1693
2853
  const index = args.indexOf(flag);
1694
2854
  if (index < 0) return undefined;
@@ -1710,6 +2870,12 @@ const FLAGS_WITH_VALUES = new Set([
1710
2870
  "--evidence",
1711
2871
  "--gjc-goal-json",
1712
2872
  "--quality-gate-json",
2873
+ "--executor-qa-json",
2874
+ "--executor-qa",
2875
+ "--pr",
2876
+ "--branch",
2877
+ "--spec",
2878
+ "--mode",
1713
2879
  "--kind",
1714
2880
  "--title",
1715
2881
  "--objective",
@@ -1770,6 +2936,26 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
1770
2936
  "",
1771
2937
  ].join("\n");
1772
2938
  }
2939
+ if (subject === "review") {
2940
+ return [
2941
+ "Run native GJC Ultragoal workflow commands",
2942
+ "",
2943
+ "USAGE",
2944
+ " $ gjc ultragoal review [--pr <n> | --branch <ref>] [--spec <path>] [--executor-qa-json <json-or-path>] [FLAGS]",
2945
+ "",
2946
+ "FLAGS",
2947
+ " --pr=<value> Review a GitHub PR; falls back to local diff when gh is unavailable",
2948
+ " --branch=<value> Review the current branch against a base ref",
2949
+ " --spec=<value> Contract/spec override; enables strong-contract clean PASS eligibility",
2950
+ " --executor-qa-json=<value> executorQa JSON string or path using checkpoint qualityGate.executorQa shape",
2951
+ " --mode=<value> review-only|review-start (default review-only)",
2952
+ " --json Output the machine-readable verdict report",
2953
+ "",
2954
+ "OUTPUT",
2955
+ " JSON includes verdict, contractStrength, cleanPassEligible, source, findings, artifactValidationSummary, and weakContractCapApplied.",
2956
+ "",
2957
+ ].join("\n");
2958
+ }
1773
2959
  return [
1774
2960
  "Run native GJC Ultragoal workflow commands",
1775
2961
  "",
@@ -1781,10 +2967,11 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
1781
2967
  " create-goals",
1782
2968
  " complete-goals",
1783
2969
  " checkpoint",
2970
+ " review",
1784
2971
  " steer",
1785
2972
  " record-review-blockers",
1786
2973
  "",
1787
- "Run `gjc ultragoal checkpoint --help` for complete checkpoint receipt requirements.",
2974
+ "Run `gjc ultragoal checkpoint --help` or `gjc ultragoal review --help` for command-specific requirements.",
1788
2975
  "",
1789
2976
  ].join("\n");
1790
2977
  }
@@ -2056,6 +3243,15 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
2056
3243
  stdout: renderCheckpointContinuation(result, status, json, cwd),
2057
3244
  };
2058
3245
  }
3246
+ case "review": {
3247
+ const result = await runUltragoalReview(cwd, args);
3248
+ return {
3249
+ status: 0,
3250
+ stdout: json ? `${JSON.stringify(result, null, 2)}\n` : `${result.verdict}\n`,
3251
+ reviewBlockerGoalIds: result.blockerGoalIds,
3252
+ createdReviewPlan: (result.blockerGoalIds?.length ?? 0) > 0,
3253
+ };
3254
+ }
2059
3255
  case "steer": {
2060
3256
  const result = await executeUltragoalSteeringCommand(args, cwd);
2061
3257
  return {
@@ -2096,6 +3292,7 @@ const RECONCILE_COMMANDS = new Set([
2096
3292
  "checkpoint",
2097
3293
  "steer",
2098
3294
  "record-review-blockers",
3295
+ "review",
2099
3296
  ]);
2100
3297
 
2101
3298
  /**
@@ -2127,6 +3324,17 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
2127
3324
  goals_path: summary.paths.goalsPath,
2128
3325
  };
2129
3326
  if (summary.gjcObjective) payload.gjc_objective = summary.gjcObjective;
3327
+ const ledgerText = await Bun.file(summary.paths.ledgerPath)
3328
+ .text()
3329
+ .catch(() => "");
3330
+ const latestLedger = latestUltragoalLedgerEventFromText(ledgerText);
3331
+ if (latestLedger) {
3332
+ payload.latestLedgerEvent = {
3333
+ event: latestLedger.event,
3334
+ ...(latestLedger.goalId ? { goalId: latestLedger.goalId } : {}),
3335
+ ...(latestLedger.timestamp ? { timestamp: latestLedger.timestamp } : {}),
3336
+ };
3337
+ }
2130
3338
  await reconcileWorkflowSkillState({ cwd, mode: "ultragoal", sessionId, active, phase: status, payload });
2131
3339
  } catch (error) {
2132
3340
  const message = error instanceof Error ? error.message : String(error);