@gajae-code/coding-agent 0.4.1 → 0.4.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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/types/commands/ultragoal.d.ts +1 -0
  3. package/dist/types/config/settings-schema.d.ts +11 -3
  4. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  6. package/dist/types/harness-control-plane/finalize.d.ts +8 -0
  7. package/dist/types/harness-control-plane/receipts.d.ts +16 -1
  8. package/dist/types/harness-control-plane/types.d.ts +9 -1
  9. package/dist/types/reminders/star-reminder.d.ts +115 -0
  10. package/dist/types/session/agent-session.d.ts +18 -0
  11. package/examples/extensions/README.md +20 -41
  12. package/package.json +7 -7
  13. package/src/cli/grep-cli.ts +1 -1
  14. package/src/commands/harness.ts +42 -3
  15. package/src/commands/ultragoal.ts +1 -0
  16. package/src/config/settings-schema.ts +13 -3
  17. package/src/defaults/gjc/skills/team/SKILL.md +10 -1
  18. package/src/defaults/gjc/skills/ultragoal/SKILL.md +3 -2
  19. package/src/gjc-runtime/launch-tmux.ts +25 -2
  20. package/src/gjc-runtime/team-runtime.ts +78 -3
  21. package/src/gjc-runtime/ultragoal-guard.ts +18 -2
  22. package/src/gjc-runtime/ultragoal-runtime.ts +240 -30
  23. package/src/harness-control-plane/finalize.ts +84 -0
  24. package/src/harness-control-plane/owner.ts +13 -0
  25. package/src/harness-control-plane/receipts.ts +39 -1
  26. package/src/harness-control-plane/types.ts +25 -1
  27. package/src/internal-urls/docs-index.generated.ts +1 -1
  28. package/src/modes/interactive-mode.ts +26 -0
  29. package/src/reminders/star-reminder.ts +422 -0
  30. package/src/session/agent-session.ts +79 -13
@@ -96,11 +96,12 @@ Loop until `gjc ultragoal status` reports all goals complete:
96
96
  7. Before any `--status complete` checkpoint, run the mandatory final cleanup/review gate below. In aggregate mode, do **not** call `goal({"op":"complete"})` for intermediate stories; checkpoint each story with a fresh `goal({"op":"get"})` snapshot whose aggregate objective is still `active`. On the final story, use the same fresh active snapshot to create the final aggregate receipt first; only after that receipt exists may `goal({"op":"complete"})` run.
97
97
  8. Checkpoint the durable ledger with that fresh active snapshot. Complete checkpoints require `--quality-gate-json`; the runtime hook rejects closure without a clean architect review:
98
98
  `gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --gjc-goal-json <goal-get-json-or-path> --quality-gate-json <quality-gate-json-or-path>`
99
+ A successful complete checkpoint is story completion, not automatic run completion. Read the checkpoint output: when it prints `Next ultragoal goal: <id>`, continue that active story under the same aggregate GJC goal; when it prints `All ultragoal goals are complete`, the durable run is terminal. `gjc ultragoal complete-goals` remains the supported manual next-story command if continuation output was missed.
99
100
  9. If blocked or failed, checkpoint failure:
100
101
  `gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"`
101
- 11. For legacy per-story completed-goal blockers, preserve the non-terminal blocker with:
102
+ 10. For legacy per-story completed-goal blockers, preserve the non-terminal blocker with:
102
103
  `gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks goal create in this thread>" --gjc-goal-json <goal-get-json-or-path>`
103
- 12. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
104
+ 11. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
104
105
 
105
106
  ## Dynamic steering
106
107
 
@@ -48,6 +48,7 @@ export interface TmuxLaunchContext {
48
48
  currentBranch?: string | null;
49
49
  existingBranchSessionName?: string | null;
50
50
  project?: string | null;
51
+ diagnosticWriter?: (message: string) => void;
51
52
  }
52
53
 
53
54
  export interface TmuxSpawnResult {
@@ -120,6 +121,16 @@ function isInteractiveRootLaunch(parsed: Args, tty: TtyState): boolean {
120
121
  );
121
122
  }
122
123
 
124
+ function isBunVirtualPath(value: string | undefined): boolean {
125
+ return value?.startsWith("/$bunfs/") === true;
126
+ }
127
+
128
+ function formatTmuxLaunchDiagnostic(stage: string, stderr?: string): string {
129
+ const detail = stderr?.trim();
130
+ const suffix = detail ? ` ${detail.slice(0, 240)}` : "";
131
+ return `gjc --tmux failed after creating tmux session: ${stage}.${suffix}\n`;
132
+ }
133
+
123
134
  function shellQuote(value: string): string {
124
135
  if (value.length === 0) return "''";
125
136
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -148,6 +159,9 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
148
159
  function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
149
160
  const entrypoint = context.argv[1];
150
161
  if (!entrypoint) return ["gjc"];
162
+ if (isBunVirtualPath(entrypoint)) {
163
+ return isBunVirtualPath(context.execPath) ? ["gjc"] : [context.execPath];
164
+ }
151
165
  const resolvedEntrypoint = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(context.cwd, entrypoint);
152
166
  if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
153
167
  return [context.execPath, resolvedEntrypoint];
@@ -264,10 +278,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
264
278
  });
265
279
  if (profile.failures.length > 0) {
266
280
  cleanupCreatedTmuxSession(plan, spawnSync, options);
267
- return false;
281
+ const failure =
282
+ profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
283
+ (context.diagnosticWriter ?? process.stderr.write.bind(process.stderr))(
284
+ formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
285
+ );
286
+ return true;
268
287
  }
269
288
  }
270
289
  if (created.exitCode !== 0) return false;
271
290
  const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
272
- return attached.exitCode === 0;
291
+ if (attached.exitCode === 0) return true;
292
+ (context.diagnosticWriter ?? process.stderr.write.bind(process.stderr))(
293
+ formatTmuxLaunchDiagnostic("attach failed", attached.stderr),
294
+ );
295
+ return true;
273
296
  }
@@ -1673,9 +1673,10 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
1673
1673
  `You are ${worker.id} in gjc team ${config.team_name}.`,
1674
1674
  `Team state root: ${config.state_root}.`,
1675
1675
  workspace,
1676
- `Task: ${config.task}`,
1676
+ `Team brief (context only): ${config.task}`,
1677
+ "Before implementation, claim your worker-owned task and treat the claimed task record as the source of truth. Do not implement directly from the broad team brief.",
1677
1678
  `Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
1678
- `Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
1679
+ `Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
1679
1680
  ].join("\n");
1680
1681
  const env = [
1681
1682
  `GJC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
@@ -1689,7 +1690,80 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
1689
1690
  ];
1690
1691
  return `${env.join(" ")} ${config.worker_command} ${shellQuote(prompt)}`;
1691
1692
  }
1693
+ interface GjcTeamInitialLane {
1694
+ label: string;
1695
+ title: string;
1696
+ body: string;
1697
+ }
1698
+
1699
+ function normalizeLaneId(label: string): string {
1700
+ return `lane-${sanitizeName(label).toLowerCase() || stableHash(label).slice(0, 8)}`;
1701
+ }
1702
+
1703
+ function parseExplicitTeamLanes(task: string): GjcTeamInitialLane[] {
1704
+ const lines = task.split(/\r?\n/);
1705
+ const lanes: GjcTeamInitialLane[] = [];
1706
+ let current: { label: string; title: string; body: string[] } | null = null;
1707
+ const laneHeading = /^#{2,6}\s+Lane\s+([A-Za-z0-9]+)\s*(?:[—–-]\s*(.+))?\s*$/;
1708
+ const boundaryHeading = /^#{1,6}\s+(?:Integration Owner|Verification Plan|ADR|Approval State)\b/i;
1709
+
1710
+ for (const line of lines) {
1711
+ const match = line.match(laneHeading);
1712
+ if (match) {
1713
+ if (current) lanes.push({ ...current, body: current.body.join("\n").trim() });
1714
+ current = {
1715
+ label: match[1] ?? `${lanes.length + 1}`,
1716
+ title: (match[2] ?? `Lane ${match[1] ?? lanes.length + 1}`).trim(),
1717
+ body: [],
1718
+ };
1719
+ continue;
1720
+ }
1721
+ if (current && boundaryHeading.test(line)) {
1722
+ lanes.push({ ...current, body: current.body.join("\n").trim() });
1723
+ current = null;
1724
+ continue;
1725
+ }
1726
+ if (current) current.body.push(line);
1727
+ }
1728
+ if (current) lanes.push({ ...current, body: current.body.join("\n").trim() });
1729
+ return lanes.filter(lane => lane.body.length > 0 || lane.title.length > 0);
1730
+ }
1731
+
1732
+ function hasAmbiguousLaneSplitIntent(task: string): boolean {
1733
+ return (
1734
+ /\bsplit\s+lanes?\s*:/i.test(task) || /\blanes?\s*:\s*[A-Z]\b/i.test(task) || /\bLane\s+[A-Z]\s*[—–-]/.test(task)
1735
+ );
1736
+ }
1737
+
1692
1738
  function buildInitialTasks(task: string, workers: GjcTeamWorker[]): GjcTeamTask[] {
1739
+ const lanes = parseExplicitTeamLanes(task);
1740
+ if (lanes.length > 0)
1741
+ return lanes.map((lane, index) => {
1742
+ const worker = workers[index % workers.length];
1743
+ if (!worker) throw new Error("team_lane_requires_worker");
1744
+ const laneTitle = `Lane ${lane.label} — ${lane.title}`;
1745
+ const objective = [`${laneTitle}`, lane.body].filter(part => part.trim().length > 0).join("\n\n");
1746
+ return {
1747
+ id: `task-${index + 1}`,
1748
+ subject: laneTitle,
1749
+ description: objective,
1750
+ title: laneTitle,
1751
+ objective,
1752
+ status: "pending",
1753
+ owner: worker.id,
1754
+ lane: normalizeLaneId(lane.label),
1755
+ required_role: worker.role,
1756
+ version: 1,
1757
+ created_at: now(),
1758
+ updated_at: now(),
1759
+ };
1760
+ });
1761
+
1762
+ if (workers.length > 1 && hasAmbiguousLaneSplitIntent(task))
1763
+ throw new Error(
1764
+ "ambiguous_team_lane_split: multi-worker team launch mentions lanes but does not provide explicit markdown lane sections such as `### Lane A — Title`",
1765
+ );
1766
+
1693
1767
  return workers.map(worker => ({
1694
1768
  id: `task-${worker.index}`,
1695
1769
  subject: `Execute team brief (${worker.id})`,
@@ -2456,6 +2530,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
2456
2530
  ? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
2457
2531
  : readCurrentTmuxLeaderContext(tmuxCommand, env);
2458
2532
  const initialWorkers = buildWorkers(options.workerCount, options.agentType, stateRoot);
2533
+ const initialTasks = buildInitialTasks(options.task, initialWorkers);
2459
2534
  const workers: GjcTeamWorker[] = [];
2460
2535
  try {
2461
2536
  for (const worker of initialWorkers)
@@ -2509,7 +2584,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
2509
2584
  updated_at: createdAt,
2510
2585
  });
2511
2586
  await writePhase(dir, "starting");
2512
- for (const task of buildInitialTasks(options.task, config.workers)) await writeTask(dir, task);
2587
+ for (const task of initialTasks) await writeTask(dir, task);
2513
2588
  await appendEvent(dir, {
2514
2589
  type: "team_started",
2515
2590
  message: options.dryRun
@@ -3,6 +3,7 @@ import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
3
3
  import {
4
4
  computeUltragoalPlanGeneration,
5
5
  getUltragoalPaths,
6
+ getUltragoalRunCompletionState,
6
7
  hashStructuredValue,
7
8
  readUltragoalLedger,
8
9
  readUltragoalPlan,
@@ -246,7 +247,8 @@ export async function readUltragoalVerificationState(input: {
246
247
  message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
247
248
  };
248
249
  }
249
- if (plan.goals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
250
+ const runState = getUltragoalRunCompletionState(plan);
251
+ if (runState.incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
250
252
  return {
251
253
  state: "active_dirty_quality_gate",
252
254
  message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
@@ -259,7 +261,21 @@ export async function readUltragoalVerificationState(input: {
259
261
  message: "Ultragoal aggregate completion requires a fresh final aggregate receipt.",
260
262
  };
261
263
  }
262
- return validateCompletionReceipt({ plan, ledger, goal: receiptTarget.goal, receiptKind: receiptTarget.receiptKind });
264
+ const receiptDiagnostic = validateCompletionReceipt({
265
+ plan,
266
+ ledger,
267
+ goal: receiptTarget.goal,
268
+ receiptKind: receiptTarget.receiptKind,
269
+ });
270
+ if (receiptDiagnostic.state !== "active_verified_complete") return receiptDiagnostic;
271
+ if (runState.incompleteGoals.length > 0) {
272
+ return {
273
+ state: "active_missing_final_receipt",
274
+ message: `Ultragoal still has incomplete required goals: ${runState.incompleteGoals.map(goal => goal.id).join(", ")}. Run \`gjc ultragoal complete-goals\` to continue.`,
275
+ goalId: receiptTarget.goal.id,
276
+ };
277
+ }
278
+ return receiptDiagnostic;
263
279
  }
264
280
 
265
281
  export async function assertCanCompleteCurrentGoal(input: {
@@ -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;