@gajae-code/coding-agent 0.12.1 → 0.12.4

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 (66) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  6. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
  7. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  8. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  10. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  11. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  12. package/dist/types/modes/interactive-mode.d.ts +9 -0
  13. package/dist/types/modes/theme/theme.d.ts +3 -1
  14. package/dist/types/modes/types.d.ts +32 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  16. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  17. package/dist/types/session/agent-session.d.ts +2 -0
  18. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  19. package/dist/types/task/index.d.ts +4 -0
  20. package/dist/types/task/output-manager.d.ts +1 -0
  21. package/dist/types/task/receipt.d.ts +2 -0
  22. package/dist/types/tools/index.d.ts +2 -0
  23. package/package.json +8 -8
  24. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  25. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  26. package/scripts/run-acp-conformance.ts +5 -0
  27. package/src/async/job-manager.ts +22 -20
  28. package/src/config/provider-ranking.ts +1 -0
  29. package/src/config/settings-schema.ts +2 -2
  30. package/src/debug/index.ts +42 -17
  31. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  33. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  34. package/src/gjc-runtime/launch-worktree.ts +103 -1
  35. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  36. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  37. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  38. package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
  39. package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
  40. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  41. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  42. package/src/internal-urls/docs-index.generated.ts +2 -2
  43. package/src/modes/DESIGN.md +53 -0
  44. package/src/modes/components/irc-sidebar.ts +30 -1
  45. package/src/modes/components/tool-status-header.ts +0 -1
  46. package/src/modes/controllers/command-controller.ts +38 -34
  47. package/src/modes/controllers/event-controller.ts +27 -16
  48. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  49. package/src/modes/controllers/selector-controller.ts +115 -22
  50. package/src/modes/interactive-mode.ts +189 -62
  51. package/src/modes/jobs-observer.ts +1 -1
  52. package/src/modes/theme/theme.ts +11 -9
  53. package/src/modes/types.ts +88 -0
  54. package/src/sdk/broker/lifecycle.ts +18 -13
  55. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  56. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  57. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  58. package/src/sdk/session.ts +1 -0
  59. package/src/session/agent-session.ts +22 -1
  60. package/src/session/internal/managed-session-scope.ts +158 -107
  61. package/src/session/internal/managed-session-storage.ts +445 -57
  62. package/src/session/session-storage.ts +29 -44
  63. package/src/task/index.ts +257 -31
  64. package/src/task/output-manager.ts +24 -14
  65. package/src/task/receipt.ts +11 -1
  66. package/src/tools/index.ts +2 -0
@@ -1,4 +1,5 @@
1
1
  import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { getConfigRootDir } from "@gajae-code/utils";
4
5
  import type { WorkflowHudSummary } from "../skill-state/active-state";
@@ -1794,27 +1795,46 @@ export function hasTypedVerifiedReceipt(value: unknown): boolean {
1794
1795
  return Boolean(type && id && (status === "verified" || status === "passed"));
1795
1796
  }
1796
1797
 
1797
- export async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
1798
+ async function resolveExistingArtifactPathUnderCwd(
1799
+ cwd: string,
1800
+ value: unknown,
1801
+ fieldName: string,
1802
+ ): Promise<string | null> {
1798
1803
  const artifactPath = nonEmptyString(value);
1799
- if (!artifactPath) return false;
1800
- const resolved = path.resolve(cwd, artifactPath);
1804
+ if (!artifactPath) return null;
1805
+ const lexicalRoot = path.resolve(cwd);
1806
+ const lexical = path.resolve(lexicalRoot, artifactPath);
1807
+ const lexicalRelative = path.relative(lexicalRoot, lexical);
1808
+ if (lexicalRelative === ".." || lexicalRelative.startsWith(`..${path.sep}`) || path.isAbsolute(lexicalRelative)) {
1809
+ throw new Error(`qualityGate ${fieldName} artifact path must resolve under the repository cwd`);
1810
+ }
1801
1811
  try {
1802
- const file = Bun.file(resolved);
1803
- return (await file.exists()) && file.size > 0;
1812
+ const [root, resolved] = await Promise.all([fs.realpath(lexicalRoot), fs.realpath(lexical)]);
1813
+ const relative = path.relative(root, resolved);
1814
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
1815
+ throw new Error(`qualityGate ${fieldName} artifact path must not escape the repository cwd through symlinks`);
1816
+ }
1817
+ if (!(await fs.stat(resolved)).isFile()) {
1818
+ throw new Error(`qualityGate ${fieldName} artifact path must reference a regular file`);
1819
+ }
1820
+ return resolved;
1804
1821
  } catch (error) {
1805
- if (isEnoent(error)) return false;
1822
+ if (isEnoent(error)) return null;
1806
1823
  throw error;
1807
1824
  }
1808
1825
  }
1809
1826
 
1827
+ export async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
1828
+ const resolved = await resolveExistingArtifactPathUnderCwd(cwd, value, "artifact");
1829
+ if (!resolved) return false;
1830
+ return (await fs.stat(resolved)).size > 0;
1831
+ }
1832
+
1810
1833
  export async function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string): Promise<Buffer | null> {
1811
- const artifactPath = nonEmptyString(row.path);
1812
- if (!artifactPath) return null;
1813
- const resolved = path.resolve(cwd, artifactPath);
1834
+ const resolved = await resolveExistingArtifactPathUnderCwd(cwd, row.path, fieldName);
1835
+ if (!resolved) return null;
1814
1836
  try {
1815
- const file = Bun.file(resolved);
1816
- if (!(await file.exists())) return null;
1817
- return Buffer.from(await file.arrayBuffer());
1837
+ return Buffer.from(await Bun.file(resolved).arrayBuffer());
1818
1838
  } catch (error) {
1819
1839
  if (isEnoent(error)) return null;
1820
1840
  throw new Error(`qualityGate ${fieldName} artifact could not be read: ${String(error)}`);
@@ -1823,6 +1843,7 @@ export async function readArtifactBytes(cwd: string, row: JsonObject, fieldName:
1823
1843
 
1824
1844
  import {
1825
1845
  readCliReplayRecord,
1846
+ resolveCliReplayCommand,
1826
1847
  validateArtifactProof,
1827
1848
  validateCliReplay,
1828
1849
  validateLiveSurfaceProofPresence,
@@ -1834,6 +1855,7 @@ import {
1834
1855
 
1835
1856
  export type { ReplayProcessHandle } from "./ultragoal-evidence";
1836
1857
  export {
1858
+ resolveCliReplayCommand,
1837
1859
  validateArtifactProof,
1838
1860
  validateCliReplay,
1839
1861
  validateLiveSurfaceProofPresence,
@@ -2230,6 +2252,169 @@ function validateDeferredLaneDeclaration(deferred: JsonObject, fieldName: string
2230
2252
  }
2231
2253
  }
2232
2254
 
2255
+ /**
2256
+ * Agent-friendly deferred-gate hydration: the runtime already stores the durable
2257
+ * batch tuple and computes the cumulative change set itself, so a deferred gate
2258
+ * only has to prove what the runtime cannot know — that targeted verification ran.
2259
+ * Every mechanical field (`kind`, the batch tuple, `deferredLanes`, and the whole
2260
+ * `changeSet` block including `paths` and `changeSetHash`) is auto-filled when
2261
+ * omitted. Explicitly supplied values are never overwritten, so a wrong
2262
+ * declaration still fails closed.
2263
+ */
2264
+ function hydrateDeferredGateDefaults(
2265
+ gate: JsonObject,
2266
+ goal: UltragoalGoal,
2267
+ changeSet: UltragoalChangeSet | undefined,
2268
+ ): JsonObject {
2269
+ const deferred = qualityGateObject(gate.deferredToBatch);
2270
+ if (!deferred) return gate;
2271
+ const metadata = goal.validationBatch;
2272
+ // The final member must carry the full strict gate; never help it defer.
2273
+ if (metadata && goal.id === metadata.finalGoalId) return gate;
2274
+ const hydrated: JsonObject = { ...deferred };
2275
+ if (hydrated.kind === undefined) hydrated.kind = "validation-batch-deferred";
2276
+ if (metadata) {
2277
+ if (hydrated.schemaVersion === undefined) hydrated.schemaVersion = metadata.schemaVersion;
2278
+ if (hydrated.batchId === undefined) hydrated.batchId = metadata.batchId;
2279
+ if (hydrated.memberIds === undefined) hydrated.memberIds = [...metadata.memberIds];
2280
+ if (hydrated.finalGoalId === undefined) hydrated.finalGoalId = metadata.finalGoalId;
2281
+ if (hydrated.metadataHash === undefined) hydrated.metadataHash = metadata.metadataHash;
2282
+ }
2283
+ if (hydrated.deferredLanes === undefined) hydrated.deferredLanes = ["architectReview", "executorQa"];
2284
+ const computedRows = (changeSet?.paths ?? []).map(row => ({
2285
+ path: row.path,
2286
+ status: row.status,
2287
+ ...(row.oldPath ? { oldPath: row.oldPath } : {}),
2288
+ }));
2289
+ const computedByPath = new Map(computedRows.map(row => [row.path, row]));
2290
+ const declared = qualityGateObject(hydrated.changeSet);
2291
+ const changeSetRecord: JsonObject = declared ? { ...declared } : {};
2292
+ if (changeSetRecord.memberGoalId === undefined) changeSetRecord.memberGoalId = goal.id;
2293
+ if (changeSetRecord.cumulativeFromBase === undefined) changeSetRecord.cumulativeFromBase = true;
2294
+ if (changeSetRecord.paths === undefined) {
2295
+ changeSetRecord.paths = computedRows;
2296
+ } else if (Array.isArray(changeSetRecord.paths)) {
2297
+ // Accept plain-string rows and rows without a status; resolve the status
2298
+ // from the computed change set instead of demanding git trivia.
2299
+ changeSetRecord.paths = changeSetRecord.paths.map(row => {
2300
+ const record = typeof row === "string" ? { path: row } : qualityGateObject(row);
2301
+ if (!record) return row;
2302
+ const pathValue = nonEmptyString(record.path);
2303
+ if (!pathValue || nonEmptyString(record.status)) return record;
2304
+ const computed = computedByPath.get(normalizeRepoPath(pathValue));
2305
+ return {
2306
+ ...record,
2307
+ status: computed?.status ?? "unknown",
2308
+ ...(computed?.oldPath && record.oldPath === undefined ? { oldPath: computed.oldPath } : {}),
2309
+ };
2310
+ });
2311
+ }
2312
+ if (changeSetRecord.changeSetHash === undefined) {
2313
+ try {
2314
+ changeSetRecord.changeSetHash = changeSetHashForPaths(
2315
+ canonicalChangeSetRows(changeSetRecord.paths, "deferredToBatch.changeSet.paths"),
2316
+ );
2317
+ } catch {
2318
+ // Malformed rows: leave the hash unset so validation reports the row defect.
2319
+ }
2320
+ }
2321
+ hydrated.changeSet = changeSetRecord;
2322
+ return { ...gate, deferredToBatch: hydrated };
2323
+ }
2324
+
2325
+ /**
2326
+ * Agent-friendly batch-close hydration, mirroring `hydrateDeferredGateDefaults`:
2327
+ * every `validationBatchClose` field except `coverageEvidence` is derivable from
2328
+ * durable state (batch metadata, member receipts) and the computed cumulative
2329
+ * change set, so it is auto-filled when omitted. Supplied values are never
2330
+ * overwritten and still fail closed when wrong. The minimal close is the full
2331
+ * strict gate plus `{"validationBatchClose":{"coverageEvidence":"..."}}`.
2332
+ */
2333
+ function hydrateBatchCloseDefaults(input: {
2334
+ gate: JsonObject;
2335
+ plan: UltragoalPlan;
2336
+ goal: UltragoalGoal;
2337
+ ledger: readonly UltragoalLedgerEvent[];
2338
+ changeSet?: UltragoalChangeSet;
2339
+ }): JsonObject {
2340
+ const metadata = input.goal.validationBatch;
2341
+ if (!metadata || input.goal.id !== metadata.finalGoalId) return input.gate;
2342
+ const requested = qualityGateObject(input.gate.validationBatchClose);
2343
+ if (!requested) return input.gate;
2344
+ // The replacement-close kind has its own dedicated hydrator.
2345
+ if (requested.kind === "review-blocker-replacement-close") return input.gate;
2346
+ const close: JsonObject = { ...requested };
2347
+ if (close.schemaVersion === undefined) close.schemaVersion = 1;
2348
+ if (close.kind === undefined) close.kind = "validation-batch-close";
2349
+ if (close.batchId === undefined) close.batchId = metadata.batchId;
2350
+ if (close.finalGoalId === undefined) close.finalGoalId = metadata.finalGoalId;
2351
+ if (close.memberIds === undefined) close.memberIds = [...metadata.memberIds];
2352
+ const derivedMetadataHashes: Record<string, string> = {};
2353
+ const derivedChangeSetHashes: Record<string, string> = {};
2354
+ const derivedReceipts: JsonObject[] = [];
2355
+ for (const memberId of metadata.memberIds) {
2356
+ const member = input.plan.goals.find(goal => goal.id === memberId);
2357
+ if (member?.validationBatch) derivedMetadataHashes[memberId] = member.validationBatch.metadataHash;
2358
+ if (!member || memberId === input.goal.id) continue;
2359
+ try {
2360
+ const receipt = requireDeferredMemberReceiptFresh(
2361
+ input.plan,
2362
+ input.ledger,
2363
+ member,
2364
+ "validationBatchClose hydration",
2365
+ );
2366
+ derivedChangeSetHashes[memberId] = receipt.validationBatch.changeSetHash;
2367
+ derivedReceipts.push({
2368
+ goalId: memberId,
2369
+ receiptId: receipt.receiptId,
2370
+ checkpointLedgerEventId: receipt.checkpointLedgerEventId,
2371
+ qualityGateHash: receipt.qualityGateHash,
2372
+ changeSetHash: receipt.validationBatch.changeSetHash,
2373
+ role: "deferred-member",
2374
+ });
2375
+ } catch {
2376
+ // Member not fresh/complete: omit it so validation reports the real defect.
2377
+ }
2378
+ }
2379
+ const suppliedMetadataHashes = qualityGateObject(close.memberMetadataHashes);
2380
+ if (close.memberMetadataHashes === undefined) close.memberMetadataHashes = derivedMetadataHashes;
2381
+ else if (suppliedMetadataHashes)
2382
+ close.memberMetadataHashes = { ...derivedMetadataHashes, ...suppliedMetadataHashes };
2383
+ if (close.memberReceipts === undefined) close.memberReceipts = derivedReceipts;
2384
+ const requestedUnion = qualityGateObject(close.unionChangeSet);
2385
+ const union: JsonObject = requestedUnion ? { ...requestedUnion } : {};
2386
+ if (union.source === undefined) union.source = "validation-batch";
2387
+ if (union.paths === undefined) {
2388
+ union.paths = (input.changeSet?.paths ?? []).map(row => ({
2389
+ path: row.path,
2390
+ status: row.status,
2391
+ ...(row.oldPath ? { oldPath: row.oldPath } : {}),
2392
+ }));
2393
+ }
2394
+ try {
2395
+ const unionRows = canonicalChangeSetRows(union.paths, "validationBatchClose.unionChangeSet.paths");
2396
+ const suppliedChangeSetHashes = qualityGateObject(union.memberChangeSetHashes);
2397
+ const memberChangeSetHashes: Record<string, unknown> = {
2398
+ ...derivedChangeSetHashes,
2399
+ [metadata.finalGoalId]: changeSetHashForPaths(unionRows),
2400
+ ...suppliedChangeSetHashes,
2401
+ };
2402
+ if (union.memberChangeSetHashes === undefined || suppliedChangeSetHashes) {
2403
+ union.memberChangeSetHashes = memberChangeSetHashes;
2404
+ }
2405
+ if (union.unionHash === undefined) {
2406
+ union.unionHash = hashStructuredValue({
2407
+ memberChangeSetHashes: qualityGateObject(union.memberChangeSetHashes) ?? memberChangeSetHashes,
2408
+ paths: unionRows.map(row => ({ path: row.path, status: row.status, oldPath: row.oldPath })),
2409
+ });
2410
+ }
2411
+ } catch {
2412
+ // Malformed rows: leave derived hashes unset so validation reports the row defect.
2413
+ }
2414
+ close.unionChangeSet = union;
2415
+ return { ...input.gate, validationBatchClose: close };
2416
+ }
2417
+
2233
2418
  function validateDeferredCompletionQualityGate(
2234
2419
  gate: JsonObject,
2235
2420
  goal: UltragoalGoal,
@@ -2251,7 +2436,9 @@ function validateDeferredCompletionQualityGate(
2251
2436
  }
2252
2437
  const deferredLanes = stringArray(deferred.deferredLanes)?.filter(Boolean).sort();
2253
2438
  if (deferredLanes?.join(",") !== "architectReview,executorQa")
2254
- throw new Error("deferredToBatch.deferredLanes must be architectReview and executorQa");
2439
+ throw new Error(
2440
+ "deferredToBatch.deferredLanes must be architectReview and executorQa (or omitted; the runtime fills it)",
2441
+ );
2255
2442
  const targeted = qualityGateObject(deferred.targetedVerification);
2256
2443
  if (!targeted || targeted.status !== PASSED_STATUS || !nonEmptyStringArray(targeted.commands))
2257
2444
  throw new Error("deferredToBatch.targetedVerification must pass with non-empty commands");
@@ -2276,13 +2463,17 @@ function validateDeferredCompletionQualityGate(
2276
2463
  const declaredChangeSet = qualityGateObject(deferred.changeSet);
2277
2464
  if (!declaredChangeSet) throw new Error("deferredToBatch.changeSet is required");
2278
2465
  if (declaredChangeSet.memberGoalId !== goal.id)
2279
- throw new Error("deferredToBatch.changeSet.memberGoalId must label the checkpointed goal");
2466
+ throw new Error(
2467
+ `deferredToBatch.changeSet.memberGoalId must label the checkpointed goal ${goal.id} (or be omitted; the runtime fills it)`,
2468
+ );
2280
2469
  if (declaredChangeSet.cumulativeFromBase !== true)
2281
- throw new Error("deferredToBatch.changeSet.cumulativeFromBase must be true");
2470
+ throw new Error("deferredToBatch.changeSet.cumulativeFromBase must be true (or omitted; the runtime fills it)");
2282
2471
  const paths = canonicalChangeSetRows(declaredChangeSet.paths, "deferredToBatch.changeSet.paths");
2283
2472
  requireChangeSetCoverage(changeSet, paths, "deferredToBatch.changeSet.paths");
2284
2473
  if (declaredChangeSet.changeSetHash !== changeSetHashForPaths(paths))
2285
- throw new Error("deferredToBatch.changeSet.changeSetHash does not match declared paths");
2474
+ throw new Error(
2475
+ "deferredToBatch.changeSet.changeSetHash does not match declared paths; omit changeSetHash and the runtime computes it",
2476
+ );
2286
2477
  }
2287
2478
  const COHORT_LANE_KEYS = ["cleaner", "architect", "qa"] as const;
2288
2479
 
@@ -2775,6 +2966,182 @@ function hydrateReviewedBatchReplacementClose(input: {
2775
2966
  },
2776
2967
  };
2777
2968
  }
2969
+ /**
2970
+ * Scaffold a schema-shaped quality-gate template for selected surfaces (#3474).
2971
+ * The template is intentionally incomplete for live artifact proofs so `quality-gate
2972
+ * validate` can report remaining evidence gaps in one pass after the author fills paths.
2973
+ */
2974
+ export function buildQualityGateInitTemplate(surfaces: readonly string[]): JsonObject {
2975
+ const normalized = surfaces.length > 0 ? surfaces.map(surface => surface.trim()).filter(Boolean) : ["web"];
2976
+ const unique: string[] = [];
2977
+ for (const surface of normalized) {
2978
+ if (!unique.includes(surface)) unique.push(surface);
2979
+ }
2980
+
2981
+ const artifactRefs: JsonObject[] = [
2982
+ {
2983
+ id: "adversarial-report",
2984
+ kind: "failure-mode-test",
2985
+ path: "artifacts/adversarial-report.txt",
2986
+ description: "Adversarial boundary and failure-mode test output",
2987
+ inlineEvidence:
2988
+ "Adversarial boundary cases exercised invalid input, missing state, and repeated submission without violating the contract.",
2989
+ },
2990
+ ];
2991
+ const surfaceEvidence: JsonObject[] = [];
2992
+ const contractCoverage: JsonObject[] = [];
2993
+ const adversarialCases: JsonObject[] = [
2994
+ {
2995
+ id: "case-invalid-input",
2996
+ contractRef: "approved-plan:goal",
2997
+ scenario: "Submit invalid or boundary input through the user-facing surface",
2998
+ expectedBehavior: "The implementation rejects or handles the case according to the approved contract",
2999
+ verdict: "passed",
3000
+ artifactRefs: ["adversarial-report"],
3001
+ },
3002
+ ];
3003
+
3004
+ unique.forEach((surface, index) => {
3005
+ const surfaceId = `surface-${index + 1}`;
3006
+ const family = surface.toLowerCase();
3007
+ const linked: string[] = [];
3008
+ if (family.includes("web") || family.includes("gui")) {
3009
+ const browserId = `browser-run-${index + 1}`;
3010
+ const shotId = `gui-screenshot-${index + 1}`;
3011
+ linked.push(browserId, shotId);
3012
+ artifactRefs.push(
3013
+ {
3014
+ id: browserId,
3015
+ kind: "browser-automation",
3016
+ path: `artifacts/${browserId}.json`,
3017
+ description: "Browser automation transcript for the approved user-facing flow",
3018
+ inlineEvidence:
3019
+ "Browser automation executed the approved flow, asserted the expected visible result, and captured the final DOM state.",
3020
+ },
3021
+ {
3022
+ id: shotId,
3023
+ kind: "screenshot",
3024
+ path: `artifacts/${shotId}.png`,
3025
+ description: "Screenshot evidence for the GUI/web surface verdict",
3026
+ inlineEvidence:
3027
+ "Screenshot review confirmed the approved screen state, including the success message and absence of regression indicators.",
3028
+ },
3029
+ );
3030
+ } else if (family.includes("cli")) {
3031
+ const replayId = `cli-replay-${index + 1}`;
3032
+ linked.push(replayId);
3033
+ artifactRefs.push({
3034
+ id: replayId,
3035
+ kind: "cli-replay",
3036
+ path: `artifacts/${replayId}.json`,
3037
+ description: "CLI argv replay transcript for the approved command surface",
3038
+ inlineEvidence:
3039
+ "CLI replay executed the allowlisted command and verified recorded stdout against the approved contract.",
3040
+ });
3041
+ } else if (family.includes("api") || family.includes("package")) {
3042
+ const reportId = `api-report-${index + 1}`;
3043
+ linked.push(reportId);
3044
+ artifactRefs.push({
3045
+ id: reportId,
3046
+ kind: "test-report",
3047
+ path: `artifacts/${reportId}.xml`,
3048
+ description: "API/package black-box test report",
3049
+ inlineEvidence:
3050
+ "API/package suite covered happy path, auth failure, and contract-breaking payloads with non-empty report output.",
3051
+ });
3052
+ } else {
3053
+ const evidenceId = `surface-evidence-${index + 1}`;
3054
+ linked.push(evidenceId);
3055
+ artifactRefs.push({
3056
+ id: evidenceId,
3057
+ kind: "transcript",
3058
+ path: `artifacts/${evidenceId}.txt`,
3059
+ description: `Evidence transcript for surface ${surface}`,
3060
+ inlineEvidence: `Surface ${surface} was exercised against the approved contract with recorded transcript evidence.`,
3061
+ });
3062
+ }
3063
+
3064
+ surfaceEvidence.push({
3065
+ id: surfaceId,
3066
+ surface: family.includes("web") || family.includes("gui") ? "gui/web" : surface,
3067
+ contractRef: "approved-plan:goal",
3068
+ invocation: `Exercise surface ${surface} against the approved contract`,
3069
+ verdict: "passed",
3070
+ artifactRefs: linked,
3071
+ });
3072
+ contractCoverage.push({
3073
+ id: `contract-${index + 1}`,
3074
+ contractRef: "approved-plan:goal",
3075
+ obligation: `The completed story satisfies the approved contract on surface ${surface}`,
3076
+ status: "covered",
3077
+ surfaceEvidenceRefs: [surfaceId],
3078
+ adversarialCaseRefs: ["case-invalid-input"],
3079
+ });
3080
+ });
3081
+
3082
+ return {
3083
+ architectReview: {
3084
+ architectureStatus: "CLEAR",
3085
+ productStatus: "CLEAR",
3086
+ codeStatus: "CLEAR",
3087
+ recommendation: "APPROVE",
3088
+ evidence: "architect reviewed architecture, product behavior, and code changes",
3089
+ commands: ["architect-review"],
3090
+ blockers: [],
3091
+ },
3092
+ executorQa: {
3093
+ status: "passed",
3094
+ e2eStatus: "passed",
3095
+ redTeamStatus: "passed",
3096
+ evidence: "executor built and ran e2e plus red-team QA suite",
3097
+ e2eCommands: ["bun test:e2e"],
3098
+ redTeamCommands: ["bun test:red-team"],
3099
+ artifactRefs,
3100
+ contractCoverage,
3101
+ surfaceEvidence,
3102
+ adversarialCases,
3103
+ blockers: [],
3104
+ },
3105
+ criticReview: {
3106
+ verdict: "OKAY",
3107
+ evidence: "critic approved final aggregate",
3108
+ blockers: [],
3109
+ },
3110
+ iteration: {
3111
+ status: "passed",
3112
+ evidence: "no verification findings remain after steering iterations",
3113
+ fullRerun: true,
3114
+ reviewCohort: {
3115
+ reviewGeneration: 1,
3116
+ sourceHash: "sha256:replace-with-frozen-source-hash",
3117
+ joined: true,
3118
+ lanes: {
3119
+ cleaner: {
3120
+ status: "passed",
3121
+ sourceHash: "sha256:replace-with-frozen-source-hash",
3122
+ evidence: "cleaner clean",
3123
+ blockers: [],
3124
+ },
3125
+ architect: {
3126
+ status: "CLEAR",
3127
+ sourceHash: "sha256:replace-with-frozen-source-hash",
3128
+ evidence: "architect clear",
3129
+ blockers: [],
3130
+ },
3131
+ qa: {
3132
+ status: "passed",
3133
+ sourceHash: "sha256:replace-with-frozen-source-hash",
3134
+ evidence: "qa passed",
3135
+ blockers: [],
3136
+ },
3137
+ },
3138
+ },
3139
+ rerunCommands: ["bun test:e2e", "bun test:red-team"],
3140
+ blockers: [],
3141
+ },
3142
+ };
3143
+ }
3144
+
2778
3145
  /**
2779
3146
  * Read-only quality-gate validation (#3474). Applies exactly the same rules as
2780
3147
  * `checkpoint --status complete` — including deferred-vs-boundary gate selection and
@@ -2807,8 +3174,17 @@ export async function validateUltragoalQualityGateReadOnly(input: {
2807
3174
  }
2808
3175
  const ledger = plan ? await readUltragoalLedger(input.cwd, sessionId) : undefined;
2809
3176
  const changeSet = await computeCheckpointChangeSet(input.cwd);
3177
+ let hydratedGate = goal ? hydrateDeferredGateDefaults(gate, goal, changeSet) : gate;
3178
+ if (goal && plan && ledger) {
3179
+ hydratedGate = hydrateBatchCloseDefaults({ gate: hydratedGate, plan, goal, ledger, changeSet });
3180
+ }
2810
3181
  try {
2811
- await validateCompletionQualityGate(input.cwd, gate, { changeSet, plan: plan ?? undefined, goal, ledger });
3182
+ await validateCompletionQualityGate(input.cwd, hydratedGate, {
3183
+ changeSet,
3184
+ plan: plan ?? undefined,
3185
+ goal,
3186
+ ledger,
3187
+ });
2812
3188
  return { valid: true, errors: [] };
2813
3189
  } catch (error) {
2814
3190
  if (error instanceof UltragoalQualityGateError) return { valid: false, errors: error.diagnostics };
@@ -2855,13 +3231,25 @@ async function readRequiredCompletionQualityGate(
2855
3231
  changeSet: options.changeSet,
2856
3232
  })
2857
3233
  : gateObject;
2858
- await validateCompletionQualityGate(cwd, hydratedGate, {
3234
+ let completionGate = options.goal
3235
+ ? hydrateDeferredGateDefaults(hydratedGate, options.goal, options.changeSet)
3236
+ : hydratedGate;
3237
+ if (options.goal && options.plan && options.ledger) {
3238
+ completionGate = hydrateBatchCloseDefaults({
3239
+ gate: completionGate,
3240
+ plan: options.plan,
3241
+ goal: options.goal,
3242
+ ledger: options.ledger,
3243
+ changeSet: options.changeSet,
3244
+ });
3245
+ }
3246
+ await validateCompletionQualityGate(cwd, completionGate, {
2859
3247
  changeSet: options.changeSet,
2860
3248
  plan: options.plan,
2861
3249
  goal: options.goal,
2862
3250
  ledger: options.ledger,
2863
3251
  });
2864
- return hydratedGate;
3252
+ return completionGate;
2865
3253
  }
2866
3254
 
2867
3255
  function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
@@ -3957,6 +4345,18 @@ function flagValue(args: readonly string[], flag: string): string | undefined {
3957
4345
  return args[index + 1];
3958
4346
  }
3959
4347
 
4348
+ function flagValues(args: readonly string[], flag: string): string[] {
4349
+ const values: string[] = [];
4350
+ for (let index = 0; index < args.length; index += 1) {
4351
+ if (args[index] !== flag) continue;
4352
+ const value = args[index + 1];
4353
+ if (value === undefined || value.startsWith("-")) continue;
4354
+ values.push(value);
4355
+ index += 1;
4356
+ }
4357
+ return values;
4358
+ }
4359
+
3960
4360
  function hasFlag(args: readonly string[], flag: string): boolean {
3961
4361
  return args.includes(flag);
3962
4362
  }
@@ -3985,6 +4385,8 @@ const FLAGS_WITH_VALUES = new Set([
3985
4385
  "--order-json",
3986
4386
  "--classification",
3987
4387
  "--validation-batch-json",
4388
+ "--out",
4389
+ "--surface",
3988
4390
  ]);
3989
4391
 
3990
4392
  function isHelpArg(arg: string): boolean {
@@ -4114,6 +4516,28 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
4114
4516
  ].join("\n");
4115
4517
  }
4116
4518
 
4519
+ if (subject === "quality-gate") {
4520
+ return [
4521
+ "Run native GJC Ultragoal workflow commands",
4522
+ "",
4523
+ "USAGE",
4524
+ " $ gjc ultragoal quality-gate init [--surface <name> ...] --out <path>",
4525
+ " $ gjc ultragoal quality-gate validate --quality-gate-json <json-or-path> [--goal-id <id>] [--json]",
4526
+ "",
4527
+ "FLAGS",
4528
+ " --surface=<value> Surface to scaffold (repeatable; default web)",
4529
+ " --out=<value> Output path for quality-gate init",
4530
+ " --quality-gate-json=<value> JSON string or path for quality-gate validate",
4531
+ " --goal-id=<value> Optional durable goal id for rule-identical validation",
4532
+ " --json Machine-readable output",
4533
+ "",
4534
+ "EXAMPLES",
4535
+ " $ gjc ultragoal quality-gate init --surface web --surface api --out ./quality-gate.json",
4536
+ " $ gjc ultragoal quality-gate validate --quality-gate-json ./quality-gate.json --json",
4537
+ "",
4538
+ ].join("\n");
4539
+ }
4540
+
4117
4541
  return [
4118
4542
  "Run native GJC Ultragoal workflow commands",
4119
4543
  "",
@@ -4131,10 +4555,11 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
4131
4555
  " classify-blocker",
4132
4556
  " record-critic-verdict",
4133
4557
  " record-critic-gate-override",
4558
+ " quality-gate init",
4134
4559
  " quality-gate validate",
4135
4560
 
4136
4561
  "",
4137
- "Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, `gjc ultragoal classify-blocker --help`, `gjc ultragoal record-critic-verdict --help`, or `gjc ultragoal record-critic-gate-override --help` for command-specific requirements.",
4562
+ "Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, `gjc ultragoal classify-blocker --help`, `gjc ultragoal record-critic-verdict --help`, or `gjc ultragoal record-critic-gate-override --help`, or `gjc ultragoal quality-gate --help` for command-specific requirements.",
4138
4563
  "",
4139
4564
  ].join("\n");
4140
4565
  }
@@ -4510,12 +4935,32 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
4510
4935
  };
4511
4936
  }
4512
4937
  case "quality-gate": {
4513
- const positional = args.filter(arg => !arg.startsWith("--"));
4938
+ const positional = args.filter(arg => !arg.startsWith("-"));
4514
4939
  const subcommand = positional[1];
4940
+ if (subcommand === "init") {
4941
+ const out = flagValue(args, "--out");
4942
+ if (!out?.trim()) {
4943
+ return { status: 1, stderr: "quality-gate init requires --out <path>\n" };
4944
+ }
4945
+ const surfaces = flagValues(args, "--surface");
4946
+ const template = buildQualityGateInitTemplate(surfaces);
4947
+ const resolved = path.resolve(cwd, out);
4948
+ await Bun.write(resolved, `${JSON.stringify(template, null, 2)}\n`);
4949
+ if (json) {
4950
+ return {
4951
+ status: 0,
4952
+ stdout: `${JSON.stringify({ ok: true, out: resolved, surfaces: surfaces.length > 0 ? surfaces : ["web"] }, null, 2)}\n`,
4953
+ };
4954
+ }
4955
+ return {
4956
+ status: 0,
4957
+ stdout: `Wrote quality-gate template to ${resolved}\n`,
4958
+ };
4959
+ }
4515
4960
  if (subcommand !== "validate") {
4516
4961
  return {
4517
4962
  status: 1,
4518
- stderr: `Unknown gjc ultragoal quality-gate subcommand: ${subcommand ?? "(missing)"}; only validate is supported\n`,
4963
+ stderr: `Unknown gjc ultragoal quality-gate subcommand: ${subcommand ?? "(missing)"}; supported: init, validate\n`,
4519
4964
  };
4520
4965
  }
4521
4966
  const qualityGateJson = flagValue(args, "--quality-gate-json");
@@ -466,6 +466,9 @@
466
466
  {
467
467
  "id": "critic"
468
468
  },
469
+ {
470
+ "id": "disposition"
471
+ },
469
472
  {
470
473
  "id": "revision"
471
474
  },
@@ -507,6 +510,21 @@
507
510
  "to": "critic",
508
511
  "verb": "write-artifact"
509
512
  },
513
+ {
514
+ "from": "critic",
515
+ "to": "disposition",
516
+ "verb": "write-artifact"
517
+ },
518
+ {
519
+ "from": "architect",
520
+ "to": "disposition",
521
+ "verb": "write-artifact"
522
+ },
523
+ {
524
+ "from": "disposition",
525
+ "to": "revision",
526
+ "verb": "write-artifact"
527
+ },
510
528
  {
511
529
  "from": "critic",
512
530
  "to": "revision",
@@ -522,6 +540,11 @@
522
540
  "to": "post-interview",
523
541
  "verb": "write-artifact"
524
542
  },
543
+ {
544
+ "from": "disposition",
545
+ "to": "post-interview",
546
+ "verb": "write-artifact"
547
+ },
525
548
  {
526
549
  "from": "post-interview",
527
550
  "to": "revision",
@@ -557,6 +580,11 @@
557
580
  "to": "handoff",
558
581
  "verb": "handoff"
559
582
  },
583
+ {
584
+ "from": "disposition",
585
+ "to": "handoff",
586
+ "verb": "handoff"
587
+ },
560
588
  {
561
589
  "from": "revision",
562
590
  "to": "handoff",
@@ -727,6 +755,7 @@
727
755
  "planner",
728
756
  "architect",
729
757
  "critic",
758
+ "disposition",
730
759
  "revision",
731
760
  "post-interview",
732
761
  "adr",