@gajae-code/coding-agent 0.4.1 → 0.4.3

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 (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/types/async/job-manager.d.ts +25 -0
  3. package/dist/types/commands/ultragoal.d.ts +1 -0
  4. package/dist/types/commit/model-selection.d.ts +1 -1
  5. package/dist/types/config/model-registry.d.ts +3 -1
  6. package/dist/types/config/model-resolver.d.ts +1 -19
  7. package/dist/types/config/models-config-schema.d.ts +12 -0
  8. package/dist/types/config/settings-schema.d.ts +26 -4
  9. package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  12. package/dist/types/harness-control-plane/finalize.d.ts +8 -0
  13. package/dist/types/harness-control-plane/receipts.d.ts +16 -1
  14. package/dist/types/harness-control-plane/types.d.ts +16 -3
  15. package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
  16. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  17. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
  18. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
  19. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
  20. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
  21. package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
  22. package/dist/types/reminders/star-reminder.d.ts +115 -0
  23. package/dist/types/session/agent-session.d.ts +30 -1
  24. package/dist/types/session/session-manager.d.ts +1 -1
  25. package/dist/types/tools/bash.d.ts +2 -0
  26. package/dist/types/tools/browser/actions.d.ts +54 -0
  27. package/dist/types/tools/browser.d.ts +80 -0
  28. package/dist/types/tools/image-gen.d.ts +1 -0
  29. package/dist/types/tools/index.d.ts +3 -1
  30. package/dist/types/tools/job.d.ts +1 -1
  31. package/examples/extensions/README.md +20 -41
  32. package/package.json +7 -7
  33. package/src/async/job-manager.ts +120 -1
  34. package/src/cli/grep-cli.ts +1 -1
  35. package/src/commands/harness.ts +42 -3
  36. package/src/commands/ultragoal.ts +8 -1
  37. package/src/commit/agentic/index.ts +2 -2
  38. package/src/commit/model-selection.ts +7 -22
  39. package/src/commit/pipeline.ts +2 -2
  40. package/src/config/model-registry.ts +17 -9
  41. package/src/config/model-resolver.ts +14 -84
  42. package/src/config/models-config-schema.ts +2 -0
  43. package/src/config/settings-schema.ts +27 -4
  44. package/src/defaults/gjc/skills/team/SKILL.md +10 -1
  45. package/src/defaults/gjc/skills/ultragoal/SKILL.md +3 -2
  46. package/src/gjc-runtime/goal-mode-request.ts +21 -1
  47. package/src/gjc-runtime/launch-tmux.ts +25 -2
  48. package/src/gjc-runtime/team-runtime.ts +78 -3
  49. package/src/gjc-runtime/ultragoal-guard.ts +18 -2
  50. package/src/gjc-runtime/ultragoal-runtime.ts +240 -30
  51. package/src/harness-control-plane/finalize.ts +84 -0
  52. package/src/harness-control-plane/owner.ts +16 -3
  53. package/src/harness-control-plane/receipts.ts +39 -1
  54. package/src/harness-control-plane/rpc-adapter.ts +7 -1
  55. package/src/harness-control-plane/types.ts +33 -12
  56. package/src/internal-urls/docs-index.generated.ts +3 -3
  57. package/src/memories/index.ts +1 -1
  58. package/src/modes/acp/acp-agent.ts +17 -9
  59. package/src/modes/acp/acp-event-mapper.ts +33 -1
  60. package/src/modes/components/custom-editor.ts +19 -3
  61. package/src/modes/controllers/input-controller.ts +27 -7
  62. package/src/modes/controllers/selector-controller.ts +7 -1
  63. package/src/modes/interactive-mode.ts +29 -1
  64. package/src/modes/rpc/rpc-client.ts +16 -3
  65. package/src/modes/rpc/rpc-mode.ts +5 -2
  66. package/src/modes/shared/agent-wire/command-contract.ts +18 -0
  67. package/src/modes/shared/agent-wire/event-contract.ts +147 -0
  68. package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
  69. package/src/modes/shared/agent-wire/event-observation.ts +397 -0
  70. package/src/modes/shared/agent-wire/protocol.ts +24 -81
  71. package/src/modes/utils/context-usage.ts +2 -2
  72. package/src/prompts/agents/explore.md +1 -1
  73. package/src/prompts/agents/plan.md +1 -1
  74. package/src/prompts/agents/reviewer.md +1 -1
  75. package/src/prompts/tools/browser.md +3 -2
  76. package/src/reminders/star-reminder.ts +422 -0
  77. package/src/runtime-mcp/manager.ts +15 -2
  78. package/src/sdk.ts +3 -1
  79. package/src/session/agent-session.ts +139 -17
  80. package/src/session/session-manager.ts +1 -1
  81. package/src/task/agents.ts +1 -1
  82. package/src/tools/bash.ts +6 -1
  83. package/src/tools/browser/actions.ts +189 -0
  84. package/src/tools/browser.ts +91 -1
  85. package/src/tools/image-gen.ts +42 -15
  86. package/src/tools/index.ts +7 -1
  87. package/src/tools/inspect-image.ts +10 -8
  88. package/src/tools/job.ts +12 -2
  89. package/src/tools/monitor.ts +98 -17
  90. package/src/utils/commit-message-generator.ts +6 -13
  91. package/src/utils/title-generator.ts +1 -1
  92. package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
  93. package/src/harness-control-plane/frame-mapper.ts +0 -286
  94. package/src/priority.json +0 -37
@@ -228,18 +228,35 @@ function planSnapshotForReceipt(input: {
228
228
  goal: UltragoalGoal;
229
229
  beforeStatus: UltragoalGoalStatus;
230
230
  targetGoalUpdatedAt: string;
231
+ receiptKind: UltragoalReceiptKind;
231
232
  }): unknown {
233
+ const targetGoalSnapshot = {
234
+ ...input.goal,
235
+ status: input.beforeStatus,
236
+ updatedAt: input.targetGoalUpdatedAt,
237
+ evidence: undefined,
238
+ completedAt: undefined,
239
+ completionVerification: undefined,
240
+ };
241
+ const goals =
242
+ input.receiptKind === "final-aggregate"
243
+ ? input.plan.goals.map(goal => ({
244
+ ...goal,
245
+ status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
246
+ updatedAt: goal.id === input.goal.id ? input.targetGoalUpdatedAt : goal.updatedAt,
247
+ evidence: goal.id === input.goal.id ? undefined : goal.evidence,
248
+ completedAt: goal.id === input.goal.id ? undefined : goal.completedAt,
249
+ completionVerification: undefined,
250
+ }))
251
+ : [targetGoalSnapshot];
232
252
  return {
233
- ...input.plan,
234
- updatedAt: undefined,
235
- goals: input.plan.goals.map(goal => ({
236
- ...goal,
237
- status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
238
- updatedAt: goal.id === input.goal.id ? input.targetGoalUpdatedAt : goal.updatedAt,
239
- evidence: goal.id === input.goal.id ? undefined : goal.evidence,
240
- completedAt: goal.id === input.goal.id ? undefined : goal.completedAt,
241
- completionVerification: undefined,
242
- })),
253
+ version: input.plan.version,
254
+ brief: input.plan.brief,
255
+ gjcGoalMode: input.plan.gjcGoalMode,
256
+ gjcObjective: input.plan.gjcObjective,
257
+ gjcObjectiveAliases: input.plan.gjcObjectiveAliases,
258
+ createdAt: input.plan.createdAt,
259
+ goals,
243
260
  };
244
261
  }
245
262
 
@@ -264,6 +281,7 @@ export function computeUltragoalPlanGeneration(input: {
264
281
  goal: input.goal,
265
282
  beforeStatus: input.beforeStatus,
266
283
  targetGoalUpdatedAt,
284
+ receiptKind: input.receiptKind,
267
285
  }),
268
286
  );
269
287
  const requiredGoalSetHashBeforeCheckpoint = hashStructuredValue(
@@ -569,6 +587,31 @@ function chooseNextGoal(plan: UltragoalPlan, retryFailed: boolean): UltragoalGoa
569
587
  (retryFailed ? plan.goals.find(goal => goal.status === "failed") : undefined)
570
588
  );
571
589
  }
590
+ export interface UltragoalRunCompletionState {
591
+ requiredGoals: UltragoalGoal[];
592
+ incompleteGoals: UltragoalGoal[];
593
+ nextGoal?: UltragoalGoal;
594
+ allComplete: boolean;
595
+ hasBlockers: boolean;
596
+ needsFinalAggregateReceipt: boolean;
597
+ }
598
+
599
+ export function getUltragoalRunCompletionState(
600
+ plan: UltragoalPlan,
601
+ options: { retryFailed?: boolean } = {},
602
+ ): UltragoalRunCompletionState {
603
+ const requiredGoals = requiredUltragoalGoals(plan);
604
+ const incompleteGoals = requiredGoals.filter(goal => !TERMINAL_OR_SKIPPED_STATUSES.has(goal.status));
605
+ const nextGoal = chooseNextGoal(plan, options.retryFailed === true);
606
+ return {
607
+ requiredGoals,
608
+ incompleteGoals,
609
+ nextGoal,
610
+ allComplete: requiredGoals.length > 0 && incompleteGoals.length === 0,
611
+ hasBlockers: incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "review_blocked"),
612
+ needsFinalAggregateReceipt: plan.gjcGoalMode === "aggregate" && incompleteGoals.length === 0,
613
+ };
614
+ }
572
615
 
573
616
  export async function startNextUltragoalGoal(input: { cwd: string; retryFailed?: boolean }): Promise<{
574
617
  plan: UltragoalPlan;
@@ -578,7 +621,7 @@ export async function startNextUltragoalGoal(input: { cwd: string; retryFailed?:
578
621
  const plan = await readUltragoalPlan(input.cwd);
579
622
  if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
580
623
  const goal = chooseNextGoal(plan, input.retryFailed === true);
581
- if (!goal) return { plan, allComplete: plan.goals.every(item => TERMINAL_OR_SKIPPED_STATUSES.has(item.status)) };
624
+ if (!goal) return { plan, allComplete: getUltragoalRunCompletionState(plan).allComplete };
582
625
  if (goal.status !== "active") {
583
626
  const now = new Date().toISOString();
584
627
  goal.status = "active";
@@ -639,7 +682,11 @@ function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
639
682
  function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
640
683
  const value = row[key];
641
684
  if (typeof value !== "string" || value.trim().length === 0) {
642
- throw new Error(`qualityGate ${fieldName}.${key} must be a non-empty string`);
685
+ const hint =
686
+ key === "obligation" && typeof row.description === "string" && row.description.trim().length > 0
687
+ ? "; found description, but complete-checkpoint contractCoverage rows require obligation"
688
+ : "";
689
+ throw new Error(`qualityGate ${fieldName}.${key} must be a non-empty string${hint}`);
643
690
  }
644
691
  return value.trim();
645
692
  }
@@ -1015,6 +1062,17 @@ async function readRequiredCompletionQualityGate(cwd: string, value: string | un
1015
1062
  return gate;
1016
1063
  }
1017
1064
 
1065
+ function snapshotUpdatedAtMilliseconds(value: unknown): number | null {
1066
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1067
+ if (typeof value !== "string" || value.trim().length === 0) return null;
1068
+ const trimmed = value.trim();
1069
+ if (/^\d+$/.test(trimmed)) {
1070
+ const parsed = Number.parseInt(trimmed, 10);
1071
+ return Number.isFinite(parsed) ? parsed : null;
1072
+ }
1073
+ const parsed = Date.parse(trimmed);
1074
+ return Number.isFinite(parsed) ? parsed : null;
1075
+ }
1018
1076
  async function readGjcGoalSnapshot(input: {
1019
1077
  cwd: string;
1020
1078
  value: string | undefined;
@@ -1026,16 +1084,23 @@ async function readGjcGoalSnapshot(input: {
1026
1084
  }): Promise<unknown> {
1027
1085
  if (!input.value?.trim()) {
1028
1086
  if (!input.required) return undefined;
1029
- throw new Error(`${input.errorPrefix} require --gjc-goal-json from a fresh active goal({"op":"get"}) snapshot`);
1087
+ throw new Error(
1088
+ `${input.errorPrefix} require --gjc-goal-json from a fresh active goal({"op":"get"}) snapshot; this is the GJC goal-mode receipt, not the .gjc/ultragoal/goals.json goal record`,
1089
+ );
1030
1090
  }
1031
1091
  const snapshot = await readStructuredValue(input.cwd, input.value);
1032
1092
  const snapshotObject = qualityGateObject(snapshot);
1033
1093
  const detailsObject = qualityGateObject(snapshotObject?.details);
1034
1094
  const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
1035
- if (!goalObject) throw new Error(`${input.errorPrefix} require --gjc-goal-json with a goal object`);
1036
- const updatedAt = typeof goalObject.updatedAt === "number" ? goalObject.updatedAt : null;
1095
+ if (!goalObject)
1096
+ throw new Error(
1097
+ `${input.errorPrefix} require --gjc-goal-json with a goal object from goal({"op":"get"}); pass the active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record`,
1098
+ );
1099
+ const updatedAt = snapshotUpdatedAtMilliseconds(goalObject.updatedAt);
1037
1100
  if (!updatedAt)
1038
- throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.updatedAt from goal({"op":"get"})`);
1101
+ throw new Error(
1102
+ `${input.errorPrefix} require --gjc-goal-json goal.updatedAt as epoch milliseconds or an ISO timestamp from goal({"op":"get"}); pass the active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record`,
1103
+ );
1039
1104
  const nowMilliseconds = Date.now();
1040
1105
  if (updatedAt < nowMilliseconds - GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS) {
1041
1106
  throw new Error(`${input.errorPrefix} require a fresh --gjc-goal-json snapshot`);
@@ -1052,7 +1117,9 @@ async function readGjcGoalSnapshot(input: {
1052
1117
  return snapshot;
1053
1118
  }
1054
1119
  if (!expectedObjectives.has(objective)) {
1055
- throw new Error(`${input.errorPrefix} require --gjc-goal-json objective to match the active Ultragoal objective`);
1120
+ throw new Error(
1121
+ `${input.errorPrefix} require --gjc-goal-json objective to match the active GJC goal-mode objective from goal({"op":"get"}), not the .gjc/ultragoal/goals.json goal ${input.goal?.id ?? "record"}`,
1122
+ );
1056
1123
  }
1057
1124
  if (goalObject.status !== "active") {
1058
1125
  throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.status to be active`);
@@ -1147,6 +1214,54 @@ export async function checkpointUltragoalGoal(input: {
1147
1214
  });
1148
1215
  return plan;
1149
1216
  }
1217
+ export interface UltragoalCheckpointContinuation {
1218
+ plan: UltragoalPlan;
1219
+ checkpointedGoal: UltragoalGoal;
1220
+ nextGoal?: UltragoalGoal;
1221
+ startedNext: boolean;
1222
+ allComplete: boolean;
1223
+ incompleteGoals: UltragoalGoal[];
1224
+ }
1225
+
1226
+ export async function checkpointAndContinueUltragoalGoal(input: {
1227
+ cwd: string;
1228
+ goalId: string;
1229
+ status: UltragoalGoalStatus;
1230
+ evidence: string;
1231
+ gjcGoalJson?: string;
1232
+ qualityGateJson?: string;
1233
+ advanceNext?: boolean;
1234
+ retryFailed?: boolean;
1235
+ }): Promise<UltragoalCheckpointContinuation> {
1236
+ let plan = await checkpointUltragoalGoal(input);
1237
+ const checkpointedGoal = plan.goals.find(goal => goal.id === input.goalId);
1238
+ if (!checkpointedGoal) throw new Error(`No ultragoal goal found for ${input.goalId}.`);
1239
+ if (input.status === "complete" && input.advanceNext === true) {
1240
+ const beforeAdvance = getUltragoalRunCompletionState(plan, { retryFailed: input.retryFailed });
1241
+ if (beforeAdvance.nextGoal && beforeAdvance.nextGoal.status !== "active") {
1242
+ const started = await startNextUltragoalGoal({ cwd: input.cwd, retryFailed: input.retryFailed });
1243
+ plan = started.plan;
1244
+ const afterAdvance = getUltragoalRunCompletionState(plan, { retryFailed: input.retryFailed });
1245
+ return {
1246
+ plan,
1247
+ checkpointedGoal,
1248
+ nextGoal: started.goal,
1249
+ startedNext: Boolean(started.goal),
1250
+ allComplete: afterAdvance.allComplete,
1251
+ incompleteGoals: afterAdvance.incompleteGoals,
1252
+ };
1253
+ }
1254
+ }
1255
+ const state = getUltragoalRunCompletionState(plan, { retryFailed: input.retryFailed });
1256
+ return {
1257
+ plan,
1258
+ checkpointedGoal,
1259
+ nextGoal: state.nextGoal,
1260
+ startedNext: false,
1261
+ allComplete: state.allComplete,
1262
+ incompleteGoals: state.incompleteGoals,
1263
+ };
1264
+ }
1150
1265
 
1151
1266
  export async function addUltragoalSubgoal(input: {
1152
1267
  cwd: string;
@@ -1235,6 +1350,8 @@ function hasFlag(args: readonly string[], flag: string): boolean {
1235
1350
  return args.includes(flag);
1236
1351
  }
1237
1352
 
1353
+ const HELP_FLAGS = new Set(["--help", "-h"]);
1354
+
1238
1355
  const FLAGS_WITH_VALUES = new Set([
1239
1356
  "--brief",
1240
1357
  "--brief-file",
@@ -1250,6 +1367,10 @@ const FLAGS_WITH_VALUES = new Set([
1250
1367
  "--rationale",
1251
1368
  ]);
1252
1369
 
1370
+ function isHelpArg(arg: string): boolean {
1371
+ return HELP_FLAGS.has(arg);
1372
+ }
1373
+
1253
1374
  function commandName(args: readonly string[]): string {
1254
1375
  let skipNext = false;
1255
1376
  for (const arg of args) {
@@ -1261,11 +1382,62 @@ function commandName(args: readonly string[]): string {
1261
1382
  skipNext = true;
1262
1383
  continue;
1263
1384
  }
1385
+ if (isHelpArg(arg)) continue;
1264
1386
  if (!arg.startsWith("-")) return arg;
1265
1387
  }
1266
1388
  return "status";
1267
1389
  }
1268
1390
 
1391
+ function renderUltragoalHelp(args: readonly string[]): string | null {
1392
+ if (!args.some(isHelpArg) && args[0] !== "help") return null;
1393
+ const subject =
1394
+ args[0] === "help" ? args.find((arg, index) => index > 0 && !arg.startsWith("-")) : commandName(args);
1395
+ if (subject === "checkpoint") {
1396
+ return [
1397
+ "Run native GJC Ultragoal workflow commands",
1398
+ "",
1399
+ "USAGE",
1400
+ " $ gjc ultragoal checkpoint --goal-id <id> --status <status> --evidence <text> [FLAGS]",
1401
+ "",
1402
+ "FLAGS",
1403
+ " --goal-id=<value> Durable .gjc/ultragoal goal id, e.g. G001",
1404
+ " --status=<value> pending|active|complete|failed|blocked|review_blocked|superseded",
1405
+ " --evidence=<value> Completion or checkpoint evidence text",
1406
+ " --quality-gate-json=<value> JSON string or path for complete checkpoints",
1407
+ ' --gjc-goal-json=<value> JSON string or path containing the current goal({"op":"get"}) snapshot',
1408
+ " --json Output a machine-readable receipt",
1409
+ "",
1410
+ "COMPLETE CHECKPOINT RECEIPTS",
1411
+ " --quality-gate-json must be an object with architectReview, executorQa, and iteration.",
1412
+ " executorQa.contractCoverage[] rows require an obligation field; description is not a substitute.",
1413
+ ' --gjc-goal-json must contain the active GJC goal-mode snapshot from goal({"op":"get"}), not the .gjc/ultragoal/goals.json goal record.',
1414
+ " goal.updatedAt may be epoch milliseconds or an ISO timestamp and must be fresh.",
1415
+ "",
1416
+ "EXAMPLES",
1417
+ ' $ gjc ultragoal checkpoint --goal-id G001 --status blocked --evidence "waiting on review"',
1418
+ ' $ gjc ultragoal checkpoint --goal-id G001 --status complete --evidence "tests passed" --gjc-goal-json ./goal.json --quality-gate-json ./quality-gate.json --json',
1419
+ "",
1420
+ ].join("\n");
1421
+ }
1422
+ return [
1423
+ "Run native GJC Ultragoal workflow commands",
1424
+ "",
1425
+ "USAGE",
1426
+ " $ gjc ultragoal <command> [FLAGS]",
1427
+ "",
1428
+ "COMMANDS",
1429
+ " status",
1430
+ " create-goals",
1431
+ " complete-goals",
1432
+ " checkpoint",
1433
+ " steer",
1434
+ " record-review-blockers",
1435
+ "",
1436
+ "Run `gjc ultragoal checkpoint --help` for complete checkpoint receipt requirements.",
1437
+ "",
1438
+ ].join("\n");
1439
+ }
1440
+
1269
1441
  async function readBrief(cwd: string, args: readonly string[]): Promise<string> {
1270
1442
  const inline = flagValue(args, "--brief");
1271
1443
  if (inline !== undefined) return inline;
@@ -1306,8 +1478,54 @@ function renderCompleteHandoff(
1306
1478
  "",
1307
1479
  ].join("\n");
1308
1480
  }
1481
+ function renderCheckpointContinuation(
1482
+ result: UltragoalCheckpointContinuation,
1483
+ status: UltragoalGoalStatus,
1484
+ json: boolean,
1485
+ cwd: string,
1486
+ ): string {
1487
+ if (json)
1488
+ return renderCliWriteReceipt({
1489
+ ok: true,
1490
+ goal_id: result.checkpointedGoal.id,
1491
+ status,
1492
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1493
+ completion_receipt_kind: result.checkpointedGoal.completionVerification?.receiptKind,
1494
+ quality_gate_hash: result.checkpointedGoal.completionVerification?.qualityGateHash,
1495
+ all_complete: result.allComplete,
1496
+ next_goal_id: result.nextGoal?.id,
1497
+ next_goal_status: result.nextGoal?.status,
1498
+ started_next: result.startedNext,
1499
+ incomplete_goal_ids: result.incompleteGoals.map(goal => goal.id),
1500
+ });
1501
+ const lines = [`Checkpointed ${result.checkpointedGoal.id} as ${status}.`];
1502
+ if (status === "complete") {
1503
+ if (result.allComplete) {
1504
+ lines.push("All ultragoal goals are complete.");
1505
+ } else if (result.nextGoal) {
1506
+ lines.push(`Next ultragoal goal: ${result.nextGoal.id} — ${result.nextGoal.title}`);
1507
+ lines.push(`Objective: ${result.nextGoal.objective}`);
1508
+ lines.push(`GJC objective: ${result.plan.gjcObjective}`);
1509
+ lines.push(
1510
+ result.startedNext
1511
+ ? "The next ultragoal goal is active; continue the current aggregate GJC goal and checkpoint this story when verified."
1512
+ : "Run `gjc ultragoal complete-goals` to activate the next ultragoal story.",
1513
+ );
1514
+ }
1515
+ } else if (status === "failed") {
1516
+ lines.push("Resume failed goals with `gjc ultragoal complete-goals --retry-failed` after the blocker is fixed.");
1517
+ } else if (status === "blocked" || status === "review_blocked") {
1518
+ lines.push(
1519
+ "Blocked ultragoal work must be resolved with explicit blocker work or steering before final completion.",
1520
+ );
1521
+ }
1522
+ lines.push("");
1523
+ return lines.join("\n");
1524
+ }
1309
1525
 
1310
1526
  async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<UltragoalCommandResult> {
1527
+ const help = renderUltragoalHelp(args);
1528
+ if (help) return { status: 0, stdout: help };
1311
1529
  try {
1312
1530
  const command = commandName(args);
1313
1531
  const json = hasFlag(args, "--json");
@@ -1344,27 +1562,18 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
1344
1562
  const goalId = flagValue(args, "--goal-id") ?? "";
1345
1563
  const status = parseGoalStatus(flagValue(args, "--status"));
1346
1564
  const evidence = flagValue(args, "--evidence") ?? "";
1347
- const plan = await checkpointUltragoalGoal({
1565
+ const result = await checkpointAndContinueUltragoalGoal({
1348
1566
  cwd,
1349
1567
  goalId,
1350
1568
  status,
1351
1569
  evidence,
1352
1570
  gjcGoalJson: flagValue(args, "--gjc-goal-json"),
1353
1571
  qualityGateJson: flagValue(args, "--quality-gate-json"),
1572
+ advanceNext: status === "complete",
1354
1573
  });
1355
- const goal = plan.goals.find(item => item.id === goalId);
1356
1574
  return {
1357
1575
  status: 0,
1358
- stdout: json
1359
- ? renderCliWriteReceipt({
1360
- ok: true,
1361
- goal_id: goalId,
1362
- status,
1363
- goals_path: getUltragoalPaths(cwd).goalsPath,
1364
- completion_receipt_kind: goal?.completionVerification?.receiptKind,
1365
- quality_gate_hash: goal?.completionVerification?.qualityGateHash,
1366
- })
1367
- : `ultragoal checkpoint goal-id=${goalId} status=${status}\n`,
1576
+ stdout: renderCheckpointContinuation(result, status, json, cwd),
1368
1577
  };
1369
1578
  }
1370
1579
  case "steer": {
@@ -1469,7 +1678,8 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
1469
1678
  export async function runNativeUltragoalCommand(args: string[], cwd = process.cwd()): Promise<UltragoalCommandResult> {
1470
1679
  const command = commandName(args);
1471
1680
  const result = await dispatchUltragoalCommand(args, cwd);
1472
- if (result.status === 0 && RECONCILE_COMMANDS.has(command)) {
1681
+ const isHelp = args.some(isHelpArg) || args[0] === "help";
1682
+ if (!isHelp && result.status === 0 && RECONCILE_COMMANDS.has(command)) {
1473
1683
  await reconcileUltragoalState(cwd);
1474
1684
  }
1475
1685
  return result;
@@ -17,10 +17,13 @@ import {
17
17
  type CompletionEvidence,
18
18
  type ReceiptEnvelope,
19
19
  type ReceiptSubject,
20
+ type ReviewFailureEvidence,
21
+ type ReviewVerdictEvidence,
20
22
  type ValidationEvidence,
21
23
  validateReceipt,
22
24
  } from "./receipts";
23
25
  import { readReceiptIndex, writeReceiptImmutable } from "./storage";
26
+ import { isReviewVerdict, type ReviewVerdict } from "./types";
24
27
 
25
28
  export interface ValidationCommandSpec {
26
29
  name: string;
@@ -49,6 +52,12 @@ export interface FinalizeOptions {
49
52
  requireTests?: boolean;
50
53
  requireCommit?: boolean;
51
54
  requirePr?: boolean;
55
+ /** Review-only sessions produce a terminal verdict instead of implementation validation. */
56
+ reviewOnly?: boolean;
57
+ /** Operator/loop-supplied terminal review verdict (closed vocabulary). */
58
+ verdict?: string | null;
59
+ /** Bounded PR/issue reference for the review target (e.g. "PR-414"). Never resolved from the live repo. */
60
+ prTarget?: string | null;
52
61
  validationCommands?: ValidationCommandSpec[];
53
62
  checks: FinalizeChecks;
54
63
  clock?: () => number;
@@ -60,6 +69,7 @@ export interface FinalizeResult {
60
69
  validation: { name: string; valid: boolean; exitStatus: number }[];
61
70
  commitHash: string | null;
62
71
  prUrl: string | null;
72
+ verdict?: ReviewVerdict | null;
63
73
  issueArtifact: string | null;
64
74
  blockers: string[];
65
75
  }
@@ -69,6 +79,8 @@ function receiptId(prefix: string): string {
69
79
  }
70
80
 
71
81
  export async function runFinalize(opts: FinalizeOptions): Promise<FinalizeResult> {
82
+ if (opts.reviewOnly) return runReviewFinalize(opts);
83
+
72
84
  const now = () => new Date(opts.clock ? opts.clock() : Date.now()).toISOString();
73
85
  const blockers: string[] = [];
74
86
  const validation: FinalizeResult["validation"] = [];
@@ -178,6 +190,78 @@ export async function runFinalize(opts: FinalizeOptions): Promise<FinalizeResult
178
190
  };
179
191
  }
180
192
 
193
+ /**
194
+ * Review-only finalizer: produces a terminal verdict receipt (no implementation validation,
195
+ * no commit/PR resolution) when a valid, autonomous verdict is supplied; otherwise writes a
196
+ * durable, bounded `review-failure` receipt suitable for fallback routing.
197
+ *
198
+ * It never *resolves* PR/commit metadata from the live repo; the only PR reference attached is
199
+ * the session's own declared review target (`prTarget`), so a review session cannot report an
200
+ * unrelated PR resolved from the current checkout.
201
+ *
202
+ * `OWNER_CONFIRMATION_REQUIRED` is a valid verdict but is NOT an autonomous success: it is
203
+ * recorded durably yet returns `completed: false` with an `owner-confirmation-required` blocker
204
+ * so downstream routing escalates to a human instead of treating it as merge-ready.
205
+ */
206
+ async function runReviewFinalize(opts: FinalizeOptions): Promise<FinalizeResult> {
207
+ const now = () => new Date(opts.clock ? opts.clock() : Date.now()).toISOString();
208
+ const prTarget = opts.prTarget ?? null;
209
+ const subject: ReceiptSubject = { workspace: opts.workspace, branch: opts.branch, head: null, commit: null };
210
+ const baseResult: Omit<FinalizeResult, "completed" | "receiptPath" | "verdict" | "blockers"> = {
211
+ validation: [],
212
+ commitHash: null,
213
+ prUrl: null,
214
+ issueArtifact: null,
215
+ };
216
+
217
+ if (!isReviewVerdict(opts.verdict)) {
218
+ const reason = opts.verdict == null ? "review-verdict-missing" : "review-verdict-invalid";
219
+ const failure: ReviewFailureEvidence = { reason, prTarget, failedAt: now(), fallback: "operator-or-omx-review" };
220
+ const receipt = buildReceipt<ReviewFailureEvidence>({
221
+ receiptId: receiptId("revfail"),
222
+ sessionId: opts.sessionId,
223
+ family: "review-failure",
224
+ source: "finalizer",
225
+ subject,
226
+ evidence: failure,
227
+ createdAt: now(),
228
+ });
229
+ const outcome = validateReceipt(receipt);
230
+ const entry = await writeReceiptImmutable(
231
+ opts.root,
232
+ opts.sessionId,
233
+ "review-failure",
234
+ receipt.receiptId,
235
+ receipt,
236
+ );
237
+ const blockers = outcome.valid ? [reason] : [reason, ...outcome.reasons];
238
+ return { ...baseResult, completed: false, receiptPath: entry.path, verdict: null, blockers };
239
+ }
240
+
241
+ const verdict = opts.verdict as ReviewVerdict;
242
+ const evidence: ReviewVerdictEvidence = {
243
+ verdict,
244
+ prTarget,
245
+ finalizedAt: now(),
246
+ summaryRef: typeof opts.prTarget === "string" ? `verdict:${verdict}@${opts.prTarget}` : `verdict:${verdict}`,
247
+ };
248
+ const receipt = buildReceipt<ReviewVerdictEvidence>({
249
+ receiptId: receiptId("verdict"),
250
+ sessionId: opts.sessionId,
251
+ family: "review-verdict",
252
+ source: "finalizer",
253
+ subject,
254
+ evidence,
255
+ createdAt: now(),
256
+ });
257
+ const outcome = validateReceipt(receipt);
258
+ const entry = await writeReceiptImmutable(opts.root, opts.sessionId, "review-verdict", receipt.receiptId, receipt);
259
+ // A confirmation-required verdict is recorded but never an autonomous success.
260
+ const humanActionRequired = verdict === "OWNER_CONFIRMATION_REQUIRED";
261
+ const completed = outcome.valid && !humanActionRequired;
262
+ const blockers = !outcome.valid ? outcome.reasons : humanActionRequired ? ["owner-confirmation-required"] : [];
263
+ return { ...baseResult, completed, receiptPath: entry.path, verdict, blockers };
264
+ }
181
265
  function git(workspace: string, args: string[]): string | null {
182
266
  try {
183
267
  return execFileSync("git", args, {
@@ -14,10 +14,10 @@
14
14
  import { execFileSync } from "node:child_process";
15
15
  import { randomBytes, randomUUID } from "node:crypto";
16
16
  import { existsSync } from "node:fs";
17
+ import { observeRpcOutboundFrame } from "../modes/shared/agent-wire/event-observation";
17
18
  import { classifyRecovery } from "./classifier";
18
19
  import { ControlServer, type EndpointRequest } from "./control-endpoint";
19
20
  import { defaultFinalizeChecks, type FinalizeChecks, runFinalize, type ValidationCommandSpec } from "./finalize";
20
- import { mapRpcFrame } from "./frame-mapper";
21
21
  import { type OperateResult, operate } from "./operate";
22
22
  import { preserveDirtyWorktree } from "./preserve";
23
23
  import {
@@ -174,7 +174,7 @@ export class RuntimeOwner {
174
174
 
175
175
  /** Map an RPC frame and route it: semantic/signal-bearing -> serial emit; high-frequency progress -> coalesce. */
176
176
  #handleFrame(frame: Record<string, unknown>): void {
177
- const mapped = mapRpcFrame(frame);
177
+ const mapped = observeRpcOutboundFrame(frame);
178
178
  if (!mapped) return;
179
179
  if (mapped.semantic || (mapped.signal && !mapped.coalesceKey)) {
180
180
  this.#framePump = this.#framePump
@@ -199,7 +199,7 @@ export class RuntimeOwner {
199
199
  await this.#emit("info", "rpc_activity", { coalescedFrames });
200
200
  }
201
201
 
202
- async #emitMapped(mapped: NonNullable<ReturnType<typeof mapRpcFrame>>): Promise<void> {
202
+ async #emitMapped(mapped: NonNullable<ReturnType<typeof observeRpcOutboundFrame>>): Promise<void> {
203
203
  await this.#emit(
204
204
  mapped.severity,
205
205
  mapped.kind,
@@ -359,6 +359,14 @@ export class RuntimeOwner {
359
359
 
360
360
  async #validate(): Promise<PrimitiveResponse> {
361
361
  const state = await this.#loadState();
362
+ if (state.handle.mode === "review") {
363
+ // Review-only sessions do not run implementation validation and never attach PR metadata.
364
+ state.lifecycle = "validating";
365
+ state.updatedAt = new Date(this.#opts.clock ? this.#opts.clock() : Date.now()).toISOString();
366
+ await writeSessionState(this.#opts.root, state);
367
+ await this.#emit("info", "validated", { count: 0, reviewOnly: true });
368
+ return this.#response(state, { validation: [], reviewOnly: true });
369
+ }
362
370
  const checks = this.#finalizeChecks ?? defaultFinalizeChecks(state.handle.workspace);
363
371
  const commit = await checks.resolveCommit();
364
372
  const subject: ReceiptSubject = {
@@ -495,11 +503,15 @@ export class RuntimeOwner {
495
503
  const state = await this.#loadState();
496
504
  const workspace = state.handle.workspace;
497
505
  const checks = this.#finalizeChecks ?? defaultFinalizeChecks(workspace);
506
+ const reviewOnly = state.handle.mode === "review";
498
507
  const fin = await runFinalize({
499
508
  root: this.#opts.root,
500
509
  sessionId: this.#opts.sessionId,
501
510
  workspace,
502
511
  branch: state.handle.branch ?? "",
512
+ reviewOnly,
513
+ verdict: reviewOnly ? (typeof input.verdict === "string" ? input.verdict : null) : undefined,
514
+ prTarget: reviewOnly ? state.handle.issueOrPr : undefined,
503
515
  requireTests: input.requireTests !== false,
504
516
  requireCommit: input.requireCommit !== false,
505
517
  requirePr: input.requirePr !== false,
@@ -514,6 +526,7 @@ export class RuntimeOwner {
514
526
  await this.#emit(fin.completed ? "info" : "critical", "finalized", {
515
527
  completed: fin.completed,
516
528
  blockers: fin.blockers,
529
+ ...(reviewOnly ? { verdict: fin.verdict ?? null, reviewOnly: true } : {}),
517
530
  });
518
531
  return this.#response(state, { finalize: fin }, fin.completed);
519
532
  }
@@ -13,7 +13,13 @@
13
13
  * - completion the finalize gate: receipt-valid + commit + PR/issue + validations
14
14
  */
15
15
  import { createHash } from "node:crypto";
16
- import type { GitDelta, ReceiptFamily, RecoveryClassification } from "./types";
16
+ import {
17
+ type GitDelta,
18
+ isReviewVerdict,
19
+ type ReceiptFamily,
20
+ type RecoveryClassification,
21
+ type ReviewVerdict,
22
+ } from "./types";
17
23
 
18
24
  export interface ReceiptSubject {
19
25
  workspace: string;
@@ -144,6 +150,23 @@ export interface CompletionEvidence {
144
150
  blockers: string[];
145
151
  }
146
152
 
153
+ export interface ReviewVerdictEvidence {
154
+ verdict: ReviewVerdict;
155
+ prTarget: string | null;
156
+ finalizedAt: string;
157
+ /** Bounded summary code/reference for the verdict; never raw assistant text. */
158
+ summaryRef: string | null;
159
+ }
160
+
161
+ export interface ReviewFailureEvidence {
162
+ /** Machine-actionable reason the review produced no terminal verdict. */
163
+ reason: string;
164
+ prTarget: string | null;
165
+ failedAt: string;
166
+ /** Routing hint for the operator/fallback path. */
167
+ fallback: string;
168
+ }
169
+
147
170
  function validateFamily(receipt: ReceiptEnvelope<unknown>): string[] {
148
171
  switch (receipt.family) {
149
172
  case "vanish":
@@ -154,6 +177,10 @@ function validateFamily(receipt: ReceiptEnvelope<unknown>): string[] {
154
177
  return validateValidation(receipt.evidence as ValidationEvidence);
155
178
  case "completion":
156
179
  return validateCompletion(receipt.evidence as CompletionEvidence);
180
+ case "review-verdict":
181
+ return validateReviewVerdict(receipt.evidence as ReviewVerdictEvidence);
182
+ case "review-failure":
183
+ return validateReviewFailure(receipt.evidence as ReviewFailureEvidence);
157
184
  default:
158
185
  return [`unknown-family:${receipt.family}`];
159
186
  }
@@ -206,6 +233,17 @@ function validateCompletion(e: CompletionEvidence): string[] {
206
233
  return reasons;
207
234
  }
208
235
 
236
+ function validateReviewVerdict(e: ReviewVerdictEvidence): string[] {
237
+ if (!e) return ["review-verdict-missing-evidence"];
238
+ if (!isReviewVerdict(e.verdict)) return ["review-verdict-not-in-vocabulary"];
239
+ return [];
240
+ }
241
+
242
+ function validateReviewFailure(e: ReviewFailureEvidence): string[] {
243
+ if (!e || typeof e.reason !== "string" || e.reason.length === 0) return ["review-failure-missing-reason"];
244
+ return [];
245
+ }
246
+
209
247
  /** Classifications that MUST have a valid `vanish` receipt before the action proceeds. */
210
248
  export function requiresVanishBeforeAction(classification: RecoveryClassification): boolean {
211
249
  return (
@@ -174,7 +174,13 @@ export class GajaeCodeRpc implements HarnessRpc {
174
174
  // Any other frame is a session/agent event: advance the cursor.
175
175
  this.#cursor += 1;
176
176
  this.#lastFrameAt = new Date().toISOString();
177
- if (type === "agent_start") {
177
+ // Session events arrive as canonical `event` frames: the agent event type
178
+ // lives in `payload.event_type`. Non-event frames keep their flat `type`.
179
+ const effectiveType =
180
+ type === "event" && frame.payload && typeof frame.payload === "object"
181
+ ? (frame.payload as { event_type?: unknown }).event_type
182
+ : type;
183
+ if (effectiveType === "agent_start") {
178
184
  const cursor = this.#cursor;
179
185
  this.#agentStartCursors.push(cursor);
180
186
  this.#waiters = this.#waiters.filter(w => {