@gajae-code/coding-agent 0.7.8 → 0.7.10

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 (92) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +8 -0
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/model-profile-activation.d.ts +19 -2
  5. package/dist/types/config/model-profiles.d.ts +1 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +12 -3
  8. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  9. package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
  10. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  12. package/dist/types/modes/components/hook-input.d.ts +1 -0
  13. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  14. package/dist/types/modes/components/model-selector.d.ts +9 -0
  15. package/dist/types/modes/components/status-line.d.ts +3 -0
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +3 -1
  18. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  19. package/dist/types/modes/types.d.ts +3 -1
  20. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  21. package/dist/types/notifications/html-format.d.ts +2 -0
  22. package/dist/types/notifications/recent-activity.d.ts +5 -1
  23. package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
  24. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  25. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  26. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  27. package/dist/types/notifications/threaded-render.d.ts +1 -0
  28. package/dist/types/tools/ask.d.ts +26 -0
  29. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  30. package/dist/types/utils/cmux-workspace.d.ts +23 -0
  31. package/dist/types/web/search/index.d.ts +1 -1
  32. package/dist/types/web/search/provider.d.ts +5 -0
  33. package/dist/types/web/search/render.d.ts +1 -0
  34. package/package.json +7 -7
  35. package/src/cli/notify-cli.ts +1 -1
  36. package/src/cli/web-search-cli.ts +3 -2
  37. package/src/cli.ts +1 -0
  38. package/src/config/keybindings.ts +1 -1
  39. package/src/config/model-profile-activation.ts +92 -11
  40. package/src/config/model-profiles.ts +15 -2
  41. package/src/config/model-registry.ts +81 -5
  42. package/src/config/settings-schema.ts +14 -3
  43. package/src/coordinator-mcp/server.ts +4 -2
  44. package/src/deep-interview/render-middleware.ts +1 -0
  45. package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  47. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  48. package/src/extensibility/extensions/types.ts +11 -0
  49. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  50. package/src/gjc-runtime/launch-tmux.ts +366 -48
  51. package/src/gjc-runtime/team-runtime.ts +69 -4
  52. package/src/gjc-runtime/ultragoal-guard.ts +150 -24
  53. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  54. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  55. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  56. package/src/hooks/skill-state.ts +33 -73
  57. package/src/internal-urls/docs-index.generated.ts +8 -7
  58. package/src/modes/components/custom-model-preset-wizard.ts +2 -4
  59. package/src/modes/components/hook-editor.ts +7 -4
  60. package/src/modes/components/hook-input.ts +4 -0
  61. package/src/modes/components/hook-selector.ts +21 -3
  62. package/src/modes/components/model-selector.ts +84 -13
  63. package/src/modes/components/settings-selector.ts +83 -24
  64. package/src/modes/components/status-line/presets.ts +11 -0
  65. package/src/modes/components/status-line/segments.ts +10 -2
  66. package/src/modes/components/status-line.ts +1 -0
  67. package/src/modes/controllers/extension-ui-controller.ts +20 -2
  68. package/src/modes/controllers/selector-controller.ts +130 -7
  69. package/src/modes/interactive-mode.ts +7 -2
  70. package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
  71. package/src/modes/types.ts +6 -1
  72. package/src/notifications/daemon-paths.ts +22 -0
  73. package/src/notifications/html-format.ts +75 -1
  74. package/src/notifications/index.ts +12 -1
  75. package/src/notifications/recent-activity.ts +32 -8
  76. package/src/notifications/telegram-daemon-cli.ts +13 -4
  77. package/src/notifications/telegram-daemon-control.ts +9 -0
  78. package/src/notifications/telegram-daemon.ts +69 -45
  79. package/src/notifications/telegram-reference.ts +36 -14
  80. package/src/notifications/threaded-render.ts +14 -6
  81. package/src/prompts/system/system-prompt.md +10 -1
  82. package/src/sdk.ts +13 -6
  83. package/src/session/agent-session.ts +10 -1
  84. package/src/tools/ask.ts +147 -17
  85. package/src/tools/eval.ts +2 -2
  86. package/src/tools/render-utils.ts +2 -0
  87. package/src/tools/ultragoal-ask-guard.ts +39 -5
  88. package/src/utils/cmux-workspace.ts +106 -0
  89. package/src/utils/title-generator.ts +2 -0
  90. package/src/web/search/index.ts +22 -10
  91. package/src/web/search/provider.ts +15 -0
  92. package/src/web/search/render.ts +3 -0
@@ -1931,7 +1931,22 @@ export function buildWorkerCommand(
1931
1931
  const workspace = worker.worktree_path
1932
1932
  ? `Worker worktree: ${worker.worktree_path}.`
1933
1933
  : `Worker cwd: ${config.leader.cwd}.`;
1934
- const prompt = [
1934
+ // The worker prompt body is dispatched into the pane through
1935
+ // `tmux send-keys`, which treats embedded LF characters as Enter
1936
+ // keypresses. A multi-line prompt therefore lands in the pane as
1937
+ // multiple prompt bodies followed by premature Enter presses, which on
1938
+ // Windows + psmux/ConPTY causes the worker CLI to bail out before the
1939
+ // startup ACK fires. Normalize the body to a single line by replacing
1940
+ // any LF/CRLF with a space, and strip a defensive U+FEFF in case a
1941
+ // caller managed to inject a UTF-8 BOM into the task text. Empty /
1942
+ // whitespace-only bodies fall back to a one-line placeholder so the
1943
+ // worker never sits idle at an empty prompt.
1944
+ const normalizePrompt = (raw: string): string =>
1945
+ raw
1946
+ .replace(/[\uFEFF\u200B]/g, "")
1947
+ .replace(/\r?\n+/g, " ")
1948
+ .trim();
1949
+ const rawPrompt = [
1935
1950
  `You are ${worker.id} in gjc team ${config.team_name}.`,
1936
1951
  `Team state root: ${config.state_root}.`,
1937
1952
  workspace,
@@ -1940,6 +1955,7 @@ export function buildWorkerCommand(
1940
1955
  `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.`,
1941
1956
  `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.`,
1942
1957
  ].join("\n");
1958
+ const prompt = normalizePrompt(rawPrompt) || `Worker ${worker.id} ready.`;
1943
1959
  const envLines = [
1944
1960
  envAssignment("GJC_TEAM_WORKER", `${config.team_name}/${worker.id}`),
1945
1961
  envAssignment("GJC_TEAM_INTERNAL_WORKER", `${config.team_name}/${worker.id}`),
@@ -1951,8 +1967,33 @@ export function buildWorkerCommand(
1951
1967
  ...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
1952
1968
  ];
1953
1969
  const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
1954
- const invocation = platform === "win32" ? `& ${config.worker_command}` : config.worker_command;
1955
- return `${joined} ${invocation} ${quote(prompt)}`;
1970
+ // On Windows we wrap the worker command invocation in `& { & 'cmd' 'arg1' ... }`
1971
+ // so pwsh keeps the whole multi-statement body in command position. Two
1972
+ // failure modes this avoids:
1973
+ // 1. Bare `bun 'cli.ts' 'prompt'` after `$env:X = 'y'; ...` would be
1974
+ // parsed in expression position and PowerShell would reject the
1975
+ // second quoted token with "Unexpected token '<cli.ts>'".
1976
+ // 2. `& { 'cmd' 'arg1' 'arg2' }` (single & inside a block with adjacent
1977
+ // single-quoted tokens) is itself invalid because pwsh does not
1978
+ // concatenate adjacent single-quoted strings inside a script block.
1979
+ // The nested `&` inside the block forces command-position parsing for
1980
+ // the invocation. POSIX shells do not need this — they already treat
1981
+ // `cmd 'arg'` as a normal command invocation after `;`-separated
1982
+ // variable assignments.
1983
+ //
1984
+ // ASSUMPTION: this branch only fires when the worker pane is a PowerShell
1985
+ // shell. psmux launches pwsh by default on Windows, so the unset /
1986
+ // unset-and-replace cases are correct. If a user has explicitly set
1987
+ // `set -g default-shell "C:/Program Files/Git/bin/bash.exe"` (or any
1988
+ // other non-pwsh shell), this branch will send PowerShell syntax to a
1989
+ // bash pane and the worker will fail with a parse error. Detecting the
1990
+ // pane's shell at runtime and switching the quoting + invocation style
1991
+ // accordingly is a follow-up; for now the pwsh default is assumed.
1992
+ if (platform === "win32") {
1993
+ const invocation = `& ${config.worker_command} ${quote(prompt)}`;
1994
+ return `& { ${joined} ${invocation} }`;
1995
+ }
1996
+ return `${joined} ${config.worker_command} ${quote(prompt)}`;
1956
1997
  }
1957
1998
  interface GjcTeamInitialLane {
1958
1999
  label: string;
@@ -2070,7 +2111,6 @@ async function startTmuxSession(
2070
2111
  "#{pane_id}",
2071
2112
  "-c",
2072
2113
  worker.worktree_path ?? config.leader.cwd,
2073
- buildWorkerCommand(config, worker),
2074
2114
  ],
2075
2115
  { stdout: "pipe", stderr: "pipe" },
2076
2116
  );
@@ -2081,6 +2121,31 @@ async function startTmuxSession(
2081
2121
  rollbackPaneIds.push(paneId);
2082
2122
  if (worker.index === 1) rightStackRootPaneId = paneId;
2083
2123
  workers.push({ ...worker, pane_id: paneId });
2124
+ // On psmux/ConPTY (Windows pwsh default-shell) panes, tmux writes the
2125
+ // split-window command argv to the new pane's stdin but pwsh's
2126
+ // readline never receives an Enter, so the worker never starts.
2127
+ // Create the pane without a command, then dispatch the worker
2128
+ // command through send-keys + Enter so it actually executes.
2129
+ // Two-step dispatch because tmux's `-l` (literal mode) flag is
2130
+ // global per send-keys invocation, not per arg: passing `-l
2131
+ // <body> Enter` would treat "Enter" as literal text too. Sending
2132
+ // the body in literal mode first and the Enter keypress second
2133
+ // keeps the body verbatim while still submitting the prompt as a
2134
+ // keystroke.
2135
+ const body = buildWorkerCommand(config, worker);
2136
+ Bun.spawnSync([config.tmux_command, "send-keys", "-l", "-t", paneId, body], {
2137
+ stdout: "ignore",
2138
+ stderr: "ignore",
2139
+ });
2140
+ const sendKeys = Bun.spawnSync([config.tmux_command, "send-keys", "-t", paneId, "Enter"], {
2141
+ stdout: "ignore",
2142
+ stderr: "ignore",
2143
+ });
2144
+ // void-cast the exit code so the linter does not flag an unused
2145
+ // expression; the value is intentionally discarded here because
2146
+ // the actual spawn outcome is recovered by the leader through
2147
+ // the worker startup-ack watcher, not via the spawn exit code.
2148
+ void sendKeys.exitCode;
2084
2149
  }
2085
2150
  Bun.spawnSync([config.tmux_command, "select-layout", "-t", config.tmux_target, "main-vertical"], {
2086
2151
  stdout: "ignore",
@@ -69,9 +69,12 @@ function isKnownUltragoalObjective(currentObjective: string): boolean {
69
69
  );
70
70
  }
71
71
 
72
- async function ultragoalReadPaths(cwd: string): Promise<{ paths: UltragoalPaths; sessionId: string | null }> {
73
- const envSessionId = process.env.GJC_SESSION_ID?.trim();
74
- if (envSessionId) return { paths: getUltragoalPaths(cwd, envSessionId), sessionId: envSessionId };
72
+ async function ultragoalReadPaths(
73
+ cwd: string,
74
+ options: { sessionId?: string | null } = {},
75
+ ): Promise<{ paths: UltragoalPaths; sessionId: string | null }> {
76
+ const explicitSessionId = options.sessionId?.trim() || process.env.GJC_SESSION_ID?.trim();
77
+ if (explicitSessionId) return { paths: getUltragoalPaths(cwd, explicitSessionId), sessionId: explicitSessionId };
75
78
  try {
76
79
  const session = await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
77
80
  return { paths: getUltragoalPaths(cwd, session.gjcSessionId), sessionId: session.gjcSessionId };
@@ -250,13 +253,6 @@ export function validateCompletionReceipt(input: {
250
253
  goalId: input.goal.id,
251
254
  };
252
255
  }
253
- if (hashStructuredValue(event.gjcGoalJson) !== receipt.gjcGoalSnapshotHash) {
254
- return {
255
- state: "active_stale_receipt",
256
- message: `Ultragoal ${input.goal.id} receipt goal({"op":"get"}) snapshot hash does not match ledger.`,
257
- goalId: input.goal.id,
258
- };
259
- }
260
256
  if (input.goal.updatedAt !== receipt.verifiedAt) {
261
257
  return {
262
258
  state: "active_stale_receipt",
@@ -273,15 +269,28 @@ export function validateCompletionReceipt(input: {
273
269
  goalId: input.goal.id,
274
270
  };
275
271
  }
276
- const missingReceipts = requiredGoals(input.plan).filter(
277
- goal => goal.id !== input.goal.id && !goal.completionVerification,
278
- );
279
- if (missingReceipts.length > 0) {
280
- return {
281
- state: "active_missing_receipt",
282
- message: `Ultragoal final receipt is missing per-goal evidence for: ${missingReceipts.map(goal => goal.id).join(", ")}.`,
283
- goalId: input.goal.id,
284
- };
272
+ for (const priorGoal of requiredGoals(input.plan)) {
273
+ if (priorGoal.id === input.goal.id) continue;
274
+ if (!priorGoal.completionVerification) {
275
+ return {
276
+ state: "active_missing_receipt",
277
+ message: `Ultragoal final receipt is missing per-goal evidence for: ${priorGoal.id}.`,
278
+ goalId: input.goal.id,
279
+ };
280
+ }
281
+ const priorDiagnostic = validateCompletionReceipt({
282
+ plan: input.plan,
283
+ ledger: input.ledger,
284
+ goal: priorGoal,
285
+ receiptKind: "per-goal",
286
+ });
287
+ if (priorDiagnostic.state !== "active_verified_complete") {
288
+ return {
289
+ state: priorDiagnostic.state,
290
+ message: `Ultragoal final receipt requires a valid per-goal receipt for ${priorGoal.id}: ${priorDiagnostic.message}`,
291
+ goalId: input.goal.id,
292
+ };
293
+ }
285
294
  }
286
295
  }
287
296
  return {
@@ -372,11 +381,128 @@ export async function readUltragoalVerificationState(input: {
372
381
  return receiptDiagnostic;
373
382
  }
374
383
 
375
- export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
384
+ export async function verifyUltragoalDurableCompletionState(input: {
385
+ cwd: string;
386
+ sessionId?: string | null;
387
+ }): Promise<UltragoalGuardDiagnostic> {
388
+ let paths: UltragoalPaths;
389
+ let sessionId: string | null;
390
+ try {
391
+ ({ paths, sessionId } = await ultragoalReadPaths(input.cwd, { sessionId: input.sessionId }));
392
+ } catch (error) {
393
+ return {
394
+ state: "unreadable_fail_closed",
395
+ message: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
396
+ };
397
+ }
398
+ if (sessionId === null)
399
+ return { state: "inactive", message: "No active GJC session resolved; ultragoal is inactive." };
400
+ try {
401
+ await fs.stat(paths.dir);
402
+ } catch (error) {
403
+ if (isEnoent(error)) return { state: "inactive", message: "No durable .gjc/ultragoal state exists." };
404
+ return {
405
+ state: "unreadable_fail_closed",
406
+ message: `Durable .gjc/ultragoal state is present but unreadable: ${error instanceof Error ? error.message : String(error)}`,
407
+ };
408
+ }
409
+
410
+ let plan: UltragoalPlan | null;
411
+ let ledger: UltragoalLedgerEvent[];
412
+ try {
413
+ plan = await readUltragoalPlan(input.cwd, sessionId);
414
+ ledger = await readUltragoalLedger(input.cwd, sessionId);
415
+ } catch (error) {
416
+ return {
417
+ state: "unreadable_fail_closed",
418
+ message: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
419
+ };
420
+ }
421
+ if (!plan) return { state: "inactive", message: "No Ultragoal plan exists." };
422
+
423
+ if (plan.goals.some(goal => goal.status === "review_blocked")) {
424
+ return {
425
+ state: "active_review_blocked_recorded",
426
+ message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
427
+ };
428
+ }
429
+
430
+ const runState = getUltragoalRunCompletionState(plan);
431
+ if (runState.incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
432
+ return {
433
+ state: "active_dirty_quality_gate",
434
+ message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
435
+ };
436
+ }
437
+
438
+ if (plan.gjcGoalMode === "per-story") {
439
+ const incomplete = requiredGoals(plan).filter(goal => goal.status !== "complete");
440
+ if (incomplete.length > 0) {
441
+ return {
442
+ state: "active_missing_receipt",
443
+ message: `Ultragoal per-story completion requires all required stories to be complete; incomplete: ${incomplete.map(goal => goal.id).join(", ")}.`,
444
+ goalId: incomplete[0]?.id,
445
+ };
446
+ }
447
+ for (const goal of requiredGoals(plan)) {
448
+ const diagnostic = validateCompletionReceipt({
449
+ plan,
450
+ ledger,
451
+ goal,
452
+ receiptKind: "per-goal",
453
+ });
454
+ if (diagnostic.state !== "active_verified_complete") return diagnostic;
455
+ }
456
+ return {
457
+ state: "active_verified_complete",
458
+ message: "Ultragoal per-story run is verified complete.",
459
+ };
460
+ }
461
+
462
+ const ask = await isUltragoalAskBlocked(input.cwd, { sessionId });
463
+ if (!ask.active) {
464
+ return {
465
+ state: "active_verified_complete",
466
+ message: ask.reason,
467
+ goalId: ask.goalIds?.at(0),
468
+ };
469
+ }
470
+ if (ask.source === "durable_state_unreadable") {
471
+ return {
472
+ state: "unreadable_fail_closed",
473
+ message: ask.reason,
474
+ goalId: ask.goalIds?.at(0),
475
+ };
476
+ }
477
+ if (ask.reason.includes("recorded review blockers")) {
478
+ return {
479
+ state: "active_review_blocked_recorded",
480
+ message: ask.reason,
481
+ goalId: ask.goalIds?.at(0),
482
+ };
483
+ }
484
+ if (ask.reason.includes("incomplete required goals")) {
485
+ return {
486
+ state: "active_missing_final_receipt",
487
+ message: ask.reason,
488
+ goalId: ask.goalIds?.at(0),
489
+ };
490
+ }
491
+ return {
492
+ state: "active_missing_final_receipt",
493
+ message: ask.reason,
494
+ goalId: ask.goalIds?.at(0),
495
+ };
496
+ }
497
+
498
+ export async function isUltragoalAskBlocked(
499
+ cwd: string,
500
+ options: { sessionId?: string | null } = {},
501
+ ): Promise<UltragoalAskBlockDiagnostic> {
376
502
  let paths: UltragoalPaths;
377
503
  let sessionId: string | null;
378
504
  try {
379
- ({ paths, sessionId } = await ultragoalReadPaths(cwd));
505
+ ({ paths, sessionId } = await ultragoalReadPaths(cwd, options));
380
506
  } catch (error) {
381
507
  return activeAskDiagnostic({
382
508
  reason: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
@@ -594,8 +720,8 @@ export async function assertCanCompleteCurrentGoal(input: {
594
720
  sessionId?: string | null;
595
721
  }): Promise<void> {
596
722
  if (!input.cwd) return;
597
- const diagnostic = await readUltragoalVerificationState(input);
598
- if (["inactive", "unrelated_goal", "active_verified_complete"].includes(diagnostic.state)) return;
723
+ const diagnostic = await verifyUltragoalDurableCompletionState(input);
724
+ if (["inactive", "active_verified_complete"].includes(diagnostic.state)) return;
599
725
  const nudge = await consumeUltragoalNudge({
600
726
  cwd: input.cwd,
601
727
  surface: "premature_complete",
@@ -604,7 +730,7 @@ export async function assertCanCompleteCurrentGoal(input: {
604
730
  });
605
731
  if (nudge.nudged) throw new Error(nudge.message);
606
732
  throw new Error(
607
- `${diagnostic.message} Run strict \`gjc ultragoal checkpoint --status complete --quality-gate-json <file> --gjc-goal-json <file>\` first, or record review blockers and rerun verification.`,
733
+ `${diagnostic.message} Run \`gjc ultragoal checkpoint --status complete --quality-gate-json <file>\` first, or record review blockers and rerun verification.`,
608
734
  );
609
735
  }
610
736
 
@@ -2,12 +2,10 @@ import * as crypto from "node:crypto";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { inflateSync } from "node:zlib";
5
- import { normalizeGoal } from "../goals/state";
6
- import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
7
5
  import type { WorkflowHudSummary } from "../skill-state/active-state";
8
6
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
9
7
  import { renderCliWriteReceipt } from "./cli-write-receipt";
10
- import { DEFAULT_ULTRAGOAL_OBJECTIVE, GJC_SESSION_FILE_ENV } from "./goal-mode-request";
8
+ import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
11
9
  import { gjcRoot, sessionUltragoalDir } from "./session-layout";
12
10
  import { resolveGjcSessionForRead, resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
13
11
  import { renderUltragoalStatusMarkdown } from "./state-renderer";
@@ -68,7 +66,6 @@ export interface UltragoalCompletionVerification {
68
66
  gjcGoalMode: UltragoalGjcGoalMode;
69
67
  gjcObjective: string;
70
68
  qualityGateHash: string;
71
- gjcGoalSnapshotHash: string;
72
69
  planGeneration: string;
73
70
  basis: {
74
71
  planHashBeforeCheckpoint: string;
@@ -174,10 +171,9 @@ const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
174
171
  const MIN_SUBSTANTIVE_EVIDENCE_WORDS = 5;
175
172
  const MIN_SUBSTANTIVE_EVIDENCE_CHARS = 32;
176
173
 
177
- const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
178
- const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
179
-
180
174
  const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
175
+ const COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES = new Set<UltragoalGoalStatus>(["active", "failed"]);
176
+
181
177
  const NATIVE_STEERING_KINDS = [
182
178
  "add_subgoal",
183
179
  "split_subgoal",
@@ -570,7 +566,6 @@ function buildCompletionReceipt(input: {
570
566
  receiptKind: UltragoalReceiptKind;
571
567
  beforeStatus: UltragoalGoalStatus;
572
568
  qualityGateJson: JsonObject;
573
- gjcGoalJson: JsonObject;
574
569
  now: string;
575
570
  checkpointLedgerEventId: string;
576
571
  }): UltragoalCompletionVerification {
@@ -593,7 +588,6 @@ function buildCompletionReceipt(input: {
593
588
  gjcGoalMode: input.plan.gjcGoalMode,
594
589
  gjcObjective: input.plan.gjcObjective,
595
590
  qualityGateHash: hashStructuredValue(input.qualityGateJson),
596
- gjcGoalSnapshotHash: hashStructuredValue(input.gjcGoalJson),
597
591
  planGeneration: generation.planGeneration,
598
592
  basis: generation.basis,
599
593
  checkpointLedgerEventId: input.checkpointLedgerEventId,
@@ -2470,93 +2464,24 @@ async function readRequiredCompletionQualityGate(
2470
2464
  return gate;
2471
2465
  }
2472
2466
 
2473
- function snapshotUpdatedAtMilliseconds(value: unknown): number | null {
2474
- if (typeof value === "number" && Number.isFinite(value)) return value;
2475
- if (typeof value !== "string" || value.trim().length === 0) return null;
2476
- const trimmed = value.trim();
2477
- if (/^\d+$/.test(trimmed)) {
2478
- const parsed = Number.parseInt(trimmed, 10);
2479
- return Number.isFinite(parsed) ? parsed : null;
2480
- }
2481
- const parsed = Date.parse(trimmed);
2482
- return Number.isFinite(parsed) ? parsed : null;
2483
- }
2484
-
2485
- function singleSessionLeafId(entries: readonly SessionEntry[]): string | undefined {
2486
- if (entries.length === 0) return undefined;
2487
- const parentIds = new Set(
2488
- entries.map(entry => entry.parentId).filter((parentId): parentId is string => typeof parentId === "string"),
2489
- );
2490
- const leafIds = entries.map(entry => entry.id).filter(id => !parentIds.has(id));
2491
- return leafIds.length === 1 ? leafIds[0] : undefined;
2492
- }
2493
-
2494
- async function readCurrentSessionGjcGoalSnapshot(): Promise<unknown | undefined> {
2495
- const sessionFile = process.env[GJC_SESSION_FILE_ENV]?.trim();
2496
- if (!sessionFile) return undefined;
2497
- const fileEntries = await loadEntriesFromFile(sessionFile);
2498
- const entries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session");
2499
- const leafId = singleSessionLeafId(entries);
2500
- if (!leafId) return undefined;
2501
- const context = buildSessionContext(entries, leafId);
2502
- if (context.mode !== "goal" && context.mode !== "goal_paused") return undefined;
2503
- const goal = normalizeGoal(context.modeData?.goal);
2504
- return goal ? { goal } : undefined;
2505
- }
2506
- async function readGjcGoalSnapshot(input: {
2507
- cwd: string;
2508
- value: string | undefined;
2509
- plan: UltragoalPlan;
2510
- goal?: UltragoalGoal;
2511
- required: boolean;
2512
- errorPrefix: string;
2513
- allowCompletedLegacyBlocker?: boolean;
2514
- }): Promise<unknown> {
2515
- const snapshot = input.value?.trim()
2516
- ? await readStructuredValue(input.cwd, input.value)
2517
- : await readCurrentSessionGjcGoalSnapshot();
2518
- if (snapshot === undefined) {
2519
- if (!input.required) return undefined;
2520
- throw new Error(
2521
- `${input.errorPrefix} require an active GJC goal-mode snapshot from the current session or --gjc-goal-json; this is the GJC goal-mode receipt, not the .gjc/ultragoal/goals.json goal record`,
2522
- );
2523
- }
2524
- const snapshotObject = qualityGateObject(snapshot);
2525
- const detailsObject = qualityGateObject(snapshotObject?.details);
2526
- const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
2527
- if (!goalObject)
2528
- throw new Error(
2529
- `${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`,
2530
- );
2531
- const updatedAt = snapshotUpdatedAtMilliseconds(goalObject.updatedAt);
2532
- if (!updatedAt)
2467
+ function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
2468
+ if (COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES.has(goal.status)) return;
2469
+ if (goal.status === "pending") {
2533
2470
  throw new Error(
2534
- `${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`,
2471
+ `Cannot checkpoint ${goal.id} as complete while its durable goals.json status is pending; start the goal before completing it.`,
2535
2472
  );
2536
- const nowMilliseconds = Date.now();
2537
- if (updatedAt < nowMilliseconds - GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS) {
2538
- throw new Error(`${input.errorPrefix} require a fresh --gjc-goal-json snapshot`);
2539
- }
2540
- if (updatedAt > nowMilliseconds + GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS) {
2541
- throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.updatedAt that is not from the future`);
2542
- }
2543
- const objective = typeof goalObject.objective === "string" ? goalObject.objective : "";
2544
- const expectedObjectives = new Set([input.plan.gjcObjective, ...(input.plan.gjcObjectiveAliases ?? [])]);
2545
- if (input.plan.gjcGoalMode === "per-story" && input.goal?.objective) {
2546
- expectedObjectives.add(input.goal.objective);
2547
- }
2548
- if (input.allowCompletedLegacyBlocker && goalObject.status === "complete" && !expectedObjectives.has(objective)) {
2549
- return snapshot;
2550
2473
  }
2551
- if (!expectedObjectives.has(objective)) {
2474
+ if (goal.status === "complete") {
2552
2475
  throw new Error(
2553
- `${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"}`,
2476
+ `Cannot checkpoint ${goal.id} as complete with different evidence because its durable goals.json status is already complete.`,
2554
2477
  );
2555
2478
  }
2556
- if (goalObject.status !== "active") {
2557
- throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.status to be active`);
2479
+ if (goal.status === "superseded") {
2480
+ throw new Error(`Cannot checkpoint ${goal.id} as complete because its durable goals.json status is superseded.`);
2558
2481
  }
2559
- return snapshot;
2482
+ throw new Error(
2483
+ `Cannot checkpoint ${goal.id} as complete while its durable goals.json status is ${goal.status}; only active or retryable failed goals can be completed.`,
2484
+ );
2560
2485
  }
2561
2486
 
2562
2487
  export async function checkpointUltragoalGoal(input: {
@@ -2564,7 +2489,6 @@ export async function checkpointUltragoalGoal(input: {
2564
2489
  goalId: string;
2565
2490
  status: UltragoalGoalStatus;
2566
2491
  evidence: string;
2567
- gjcGoalJson?: string;
2568
2492
  qualityGateJson?: string;
2569
2493
  }): Promise<UltragoalPlan> {
2570
2494
  const plan = await readUltragoalPlan(input.cwd);
@@ -2593,6 +2517,7 @@ export async function checkpointUltragoalGoal(input: {
2593
2517
  // instead of silently dropping it.
2594
2518
  return plan;
2595
2519
  }
2520
+ if (input.status === "complete") validateCompleteCheckpointTargetGoal(goal);
2596
2521
  const changeSet = input.status === "complete" ? await computeCheckpointChangeSet(input.cwd) : undefined;
2597
2522
  const qualityGateJson =
2598
2523
  input.status === "complete"
@@ -2615,25 +2540,6 @@ export async function checkpointUltragoalGoal(input: {
2615
2540
  }
2616
2541
  }
2617
2542
  const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, goal, input.status) : null;
2618
- const gjcGoalJson =
2619
- input.status === "complete"
2620
- ? await readGjcGoalSnapshot({
2621
- cwd: input.cwd,
2622
- value: input.gjcGoalJson,
2623
- plan,
2624
- goal,
2625
- required: true,
2626
- errorPrefix: "complete checkpoints",
2627
- })
2628
- : await readGjcGoalSnapshot({
2629
- cwd: input.cwd,
2630
- value: input.gjcGoalJson,
2631
- plan,
2632
- goal,
2633
- required: false,
2634
- errorPrefix: `${input.status} checkpoints`,
2635
- allowCompletedLegacyBlocker: input.status === "blocked",
2636
- });
2637
2543
  const pendingCheckpointEventId = crypto.randomUUID();
2638
2544
  if (input.status === "complete" && receiptKind && qualityGateJson && !Array.isArray(qualityGateJson)) {
2639
2545
  goal.completionVerification = buildCompletionReceipt({
@@ -2643,7 +2549,6 @@ export async function checkpointUltragoalGoal(input: {
2643
2549
  receiptKind,
2644
2550
  beforeStatus,
2645
2551
  qualityGateJson: qualityGateJson as JsonObject,
2646
- gjcGoalJson: gjcGoalJson as JsonObject,
2647
2552
  now,
2648
2553
  checkpointLedgerEventId: pendingCheckpointEventId,
2649
2554
  });
@@ -2662,7 +2567,6 @@ export async function checkpointUltragoalGoal(input: {
2662
2567
  goalId: goal.id,
2663
2568
  status: input.status,
2664
2569
  evidence,
2665
- gjcGoalJson,
2666
2570
  qualityGateJson,
2667
2571
  completionVerification: goal.completionVerification,
2668
2572
  });
@@ -2682,7 +2586,6 @@ export async function checkpointAndContinueUltragoalGoal(input: {
2682
2586
  goalId: string;
2683
2587
  status: UltragoalGoalStatus;
2684
2588
  evidence: string;
2685
- gjcGoalJson?: string;
2686
2589
  qualityGateJson?: string;
2687
2590
  advanceNext?: boolean;
2688
2591
  retryFailed?: boolean;
@@ -3091,19 +2994,14 @@ export async function recordUltragoalReviewBlockers(input: {
3091
2994
  title: string;
3092
2995
  objective: string;
3093
2996
  evidence: string;
3094
- gjcGoalJson?: string;
3095
2997
  }): Promise<UltragoalPlan> {
3096
2998
  const objective = input.objective.trim();
3097
2999
  if (!objective) throw new Error("record-review-blockers --objective is required");
3098
- if (!input.gjcGoalJson?.trim()) {
3099
- throw new Error('record-review-blockers require --gjc-goal-json from a fresh active goal({"op":"get"}) snapshot');
3100
- }
3101
3000
  const plan = await checkpointUltragoalGoal({
3102
3001
  cwd: input.cwd,
3103
3002
  goalId: input.goalId,
3104
3003
  status: "review_blocked",
3105
3004
  evidence: input.evidence,
3106
- gjcGoalJson: input.gjcGoalJson,
3107
3005
  });
3108
3006
  const persistedPlan = await readUltragoalPlan(input.cwd);
3109
3007
  if (persistedPlan?.state_revision !== undefined) plan.state_revision = persistedPlan.state_revision;
@@ -3551,7 +3449,6 @@ const FLAGS_WITH_VALUES = new Set([
3551
3449
  "--goal-id",
3552
3450
  "--status",
3553
3451
  "--evidence",
3554
- "--gjc-goal-json",
3555
3452
  "--quality-gate-json",
3556
3453
  "--executor-qa-json",
3557
3454
  "--executor-qa",
@@ -3605,15 +3502,12 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
3605
3502
  " --status=<value> pending|active|complete|failed|blocked|review_blocked|superseded",
3606
3503
  " --evidence=<value> Completion or checkpoint evidence text",
3607
3504
  " --quality-gate-json=<value> JSON string or path for complete checkpoints",
3608
- " --gjc-goal-json=<value> Optional JSON/path override for current goal snapshot; omitted complete checkpoints read current session goal state",
3609
3505
  " --json Output a machine-readable receipt",
3610
3506
  "",
3611
3507
  "COMPLETE CHECKPOINT RECEIPTS",
3612
3508
  " --quality-gate-json must be an object with architectReview, executorQa, and iteration.",
3613
3509
  " executorQa.contractCoverage[] rows require an obligation field; description is not a substitute.",
3614
- " Complete checkpoints use the current session's active GJC goal-mode snapshot when --gjc-goal-json is omitted.",
3615
- " Explicit --gjc-goal-json values must contain an active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record.",
3616
- " goal.updatedAt may be epoch milliseconds or an ISO timestamp and must be fresh.",
3510
+ " Complete checkpoints validate the target durable goals.json record before writing a receipt.",
3617
3511
  "",
3618
3512
  "EXAMPLES",
3619
3513
  ' $ gjc ultragoal checkpoint --goal-id G001 --status blocked --evidence "waiting on review"',
@@ -3949,7 +3843,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3949
3843
  goalId,
3950
3844
  status,
3951
3845
  evidence,
3952
- gjcGoalJson: flagValue(args, "--gjc-goal-json"),
3953
3846
  qualityGateJson: flagValue(args, "--quality-gate-json"),
3954
3847
  advanceNext: status === "complete",
3955
3848
  });
@@ -3981,7 +3874,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3981
3874
  title: flagValue(args, "--title") ?? "Resolve final code-review blockers",
3982
3875
  objective: flagValue(args, "--objective") ?? "",
3983
3876
  evidence: flagValue(args, "--evidence") ?? "",
3984
- gjcGoalJson: flagValue(args, "--gjc-goal-json"),
3985
3877
  });
3986
3878
  const goal = plan.goals.at(-1);
3987
3879
  return {
@@ -1479,14 +1479,6 @@
1479
1479
  "required": true,
1480
1480
  "type": "string"
1481
1481
  },
1482
- {
1483
- "appliesToVerbs": [
1484
- "checkpoint",
1485
- "record-review-blockers"
1486
- ],
1487
- "name": "gjc-goal-json",
1488
- "type": "string"
1489
- },
1490
1482
  {
1491
1483
  "appliesToVerbs": [
1492
1484
  "checkpoint"
@@ -271,7 +271,6 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
271
271
  required: true,
272
272
  appliesToVerbs: ["checkpoint", "record-review-blockers", "steer", "classify-blocker"],
273
273
  },
274
- { name: "gjc-goal-json", type: "string", appliesToVerbs: ["checkpoint", "record-review-blockers"] },
275
274
  { name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint"] },
276
275
  { name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
277
276
  { name: "goal-id", type: "string", appliesToVerbs: ["classify-blocker"] },