@gajae-code/coding-agent 0.7.9 → 0.7.11

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 (123) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/dist/types/config/keybindings.d.ts +11 -5
  4. package/dist/types/config/settings-schema.d.ts +10 -1
  5. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  6. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +10 -0
  8. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  9. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  11. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
  12. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  13. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  14. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  15. package/dist/types/modes/components/skill-message.d.ts +1 -0
  16. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  17. package/dist/types/modes/controllers/input-controller.d.ts +4 -0
  18. package/dist/types/modes/interactive-mode.d.ts +1 -0
  19. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  20. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
  21. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  22. package/dist/types/modes/types.d.ts +1 -0
  23. package/dist/types/notifications/html-format.d.ts +2 -0
  24. package/dist/types/notifications/recent-activity.d.ts +5 -1
  25. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  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/sdk.d.ts +1 -1
  29. package/dist/types/session/agent-session.d.ts +8 -5
  30. package/dist/types/slash-commands/types.d.ts +2 -0
  31. package/dist/types/tools/ask.d.ts +2 -0
  32. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  33. package/dist/types/web/search/index.d.ts +4 -2
  34. package/dist/types/web/search/provider.d.ts +14 -0
  35. package/dist/types/web/search/providers/utils.d.ts +42 -13
  36. package/dist/types/web/search/render.d.ts +1 -0
  37. package/package.json +7 -7
  38. package/src/cli/update-cli.ts +24 -0
  39. package/src/cli/web-search-cli.ts +5 -7
  40. package/src/cli.ts +1 -1
  41. package/src/commands/launch.ts +1 -1
  42. package/src/commands/ralplan.ts +1 -0
  43. package/src/config/keybindings.ts +14 -5
  44. package/src/config/model-profiles.ts +16 -0
  45. package/src/config/settings-schema.ts +11 -1
  46. package/src/coordinator-mcp/server.ts +9 -3
  47. package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
  48. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  49. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  50. package/src/defaults/gjc-defaults.ts +16 -0
  51. package/src/extensibility/extensions/runner.ts +2 -1
  52. package/src/extensibility/extensions/types.ts +11 -0
  53. package/src/gjc-runtime/launch-tmux.ts +209 -26
  54. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  55. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  56. package/src/gjc-runtime/state-runtime.ts +8 -5
  57. package/src/gjc-runtime/state-writer.ts +5 -5
  58. package/src/gjc-runtime/ultragoal-guard.ts +139 -19
  59. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  60. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  61. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  62. package/src/hooks/skill-state.ts +33 -73
  63. package/src/internal-urls/docs-index.generated.ts +6 -4
  64. package/src/main.ts +2 -2
  65. package/src/migrate/executor.ts +23 -6
  66. package/src/modes/components/custom-editor.ts +8 -0
  67. package/src/modes/components/hook-selector.ts +21 -3
  68. package/src/modes/components/model-selector.ts +6 -2
  69. package/src/modes/components/settings-selector.ts +47 -5
  70. package/src/modes/components/skill-message.ts +18 -10
  71. package/src/modes/components/status-line/presets.ts +11 -0
  72. package/src/modes/controllers/command-controller.ts +23 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +18 -2
  74. package/src/modes/controllers/input-controller.ts +41 -9
  75. package/src/modes/interactive-mode.ts +16 -7
  76. package/src/modes/prompt-action-autocomplete.ts +23 -1
  77. package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
  78. package/src/modes/slash-command-visibility.ts +9 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  81. package/src/modes/utils/ui-helpers.ts +76 -3
  82. package/src/notifications/html-format.ts +75 -1
  83. package/src/notifications/index.ts +12 -1
  84. package/src/notifications/recent-activity.ts +32 -8
  85. package/src/notifications/telegram-daemon-control.ts +9 -0
  86. package/src/notifications/telegram-daemon.ts +68 -25
  87. package/src/notifications/telegram-reference.ts +36 -14
  88. package/src/notifications/threaded-render.ts +14 -6
  89. package/src/prompts/agents/architect.md +7 -4
  90. package/src/prompts/agents/critic.md +15 -6
  91. package/src/prompts/agents/planner.md +8 -3
  92. package/src/prompts/system/system-prompt.md +5 -1
  93. package/src/prompts/tools/bash.md +1 -1
  94. package/src/sdk.ts +6 -11
  95. package/src/session/agent-session.ts +60 -17
  96. package/src/slash-commands/builtin-registry.ts +30 -10
  97. package/src/slash-commands/types.ts +2 -0
  98. package/src/tools/ask.ts +136 -16
  99. package/src/tools/bash.ts +24 -8
  100. package/src/utils/cmux-workspace.ts +233 -0
  101. package/src/utils/title-generator.ts +2 -0
  102. package/src/web/kagi.ts +1 -1
  103. package/src/web/parallel.ts +2 -2
  104. package/src/web/search/index.ts +137 -52
  105. package/src/web/search/provider.ts +80 -0
  106. package/src/web/search/providers/anthropic.ts +1 -1
  107. package/src/web/search/providers/brave.ts +1 -1
  108. package/src/web/search/providers/codex.ts +1 -1
  109. package/src/web/search/providers/duckduckgo.ts +1 -1
  110. package/src/web/search/providers/exa.ts +1 -1
  111. package/src/web/search/providers/gemini.ts +7 -3
  112. package/src/web/search/providers/jina.ts +1 -1
  113. package/src/web/search/providers/kimi.ts +6 -1
  114. package/src/web/search/providers/openai-compatible.ts +1 -1
  115. package/src/web/search/providers/parallel.ts +1 -1
  116. package/src/web/search/providers/perplexity.ts +2 -2
  117. package/src/web/search/providers/searxng.ts +1 -1
  118. package/src/web/search/providers/synthetic.ts +1 -1
  119. package/src/web/search/providers/tavily.ts +1 -1
  120. package/src/web/search/providers/utils.ts +67 -17
  121. package/src/web/search/providers/xai.ts +1 -1
  122. package/src/web/search/providers/zai.ts +1 -1
  123. package/src/web/search/render.ts +3 -0
@@ -11,7 +11,7 @@ import {
11
11
  type RalplanIndexRow,
12
12
  summarizeRalplanIndex,
13
13
  } from "./ledger-event-renderer";
14
- import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
14
+ import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
15
15
  import { modeStatePath, sessionPlansDir } from "./session-layout";
16
16
  import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
17
17
  import { migrateWorkflowState } from "./state-migrations";
@@ -35,8 +35,9 @@ import {
35
35
  * that lives in the bundled `/skill:ralplan` skill — but it accepts every documented flag so
36
36
  * scripted users see a useful response and the active run is visible to the TUI.
37
37
  *
38
- * 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N> --artifact
39
- * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
38
+ * 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
39
+ * (--artifact <path-or-string> | --artifact-env GJC_RALPLAN_ARTIFACT)
40
+ * [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
40
41
  * / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
41
42
  * an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
42
43
  * the HUD chip to reflect the latest persisted stage.
@@ -81,6 +82,7 @@ const VALUE_FLAGS = new Set([
81
82
  "--stage",
82
83
  "--stage_n",
83
84
  "--artifact",
85
+ "--artifact-env",
84
86
  "--run-id",
85
87
  "--session-id",
86
88
  "--architect",
@@ -409,8 +411,16 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
409
411
  const stageN = parseStageN(flagValue(args, "--stage_n"));
410
412
 
411
413
  const rawArtifact = flagValue(args, "--artifact");
412
- if (rawArtifact === undefined || rawArtifact === "") {
413
- throw new RalplanCommandError(2, "--artifact is required for ralplan --write");
414
+ const artifactEnvName = flagValue(args, "--artifact-env");
415
+ const artifactSources = [rawArtifact, artifactEnvName].filter(value => value !== undefined);
416
+ if (artifactSources.length === 0 || artifactSources.some(value => value === "")) {
417
+ throw new RalplanCommandError(2, "--artifact or --artifact-env is required for ralplan --write");
418
+ }
419
+ if (artifactSources.length > 1) {
420
+ throw new RalplanCommandError(2, "--artifact and --artifact-env are mutually exclusive");
421
+ }
422
+ if (artifactEnvName !== undefined && artifactEnvName !== GJC_RALPLAN_ARTIFACT_ENV) {
423
+ throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
414
424
  }
415
425
 
416
426
  const session = resolveGjcSessionForWrite(cwd, {
@@ -430,7 +440,13 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
430
440
  const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
431
441
  assertSafePathComponent(runId, "run-id");
432
442
 
433
- const artifact = await resolveArtifactContent(rawArtifact, cwd);
443
+ const artifact =
444
+ artifactEnvName !== undefined
445
+ ? (process.env[GJC_RALPLAN_ARTIFACT_ENV] ?? "")
446
+ : await resolveArtifactContent(rawArtifact!, cwd);
447
+ if (artifact === "") {
448
+ throw new RalplanCommandError(2, "artifact content is empty");
449
+ }
434
450
  return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
435
451
  }
436
452
 
@@ -1,4 +1,5 @@
1
1
  export const GJC_RESTRICTED_ROLE_AGENT_BASH_ENV = "GJC_RESTRICTED_ROLE_AGENT_BASH";
2
+ export const GJC_RALPLAN_ARTIFACT_ENV = "GJC_RALPLAN_ARTIFACT";
2
3
 
3
4
  export function isRestrictedRoleAgentBash(): boolean {
4
5
  return process.env[GJC_RESTRICTED_ROLE_AGENT_BASH_ENV] === "1";
@@ -847,11 +847,14 @@ async function writeJsonAtomic(
847
847
  forced: options?.force ?? false,
848
848
  },
849
849
  });
850
- // `writeResult.revision` is computed inside the writer lock, so it is the revision this
851
- // write actually owns. Prefer it over a post-lock file re-read, which a concurrent writer
852
- // could have advanced before the read — that race could otherwise let this payload be
853
- // published with another writer's newer revision.
854
- return { warning, stamped: (await readJsonFile(filePath)) ?? {}, revision: writeResult.revision };
850
+ // `writeResult.stamped` and `.revision` are computed inside the writer lock, so they are
851
+ // the envelope/revision this write actually owns. Never post-lock re-read here: a concurrent
852
+ // writer could advance the file before that read and make this payload publish another
853
+ // writer's newer revision into the derived active-state cache.
854
+ if (!writeResult.written || !isPlainObject(writeResult.stamped)) {
855
+ throw new Error(`state writer did not return a stamped workflow envelope for ${filePath}`);
856
+ }
857
+ return { warning, stamped: writeResult.stamped, revision: writeResult.revision };
855
858
  }
856
859
 
857
860
  function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
@@ -99,7 +99,7 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
99
99
  }
100
100
 
101
101
  export type GuardedWriteResult =
102
- | { path: string; written: true; revision: number }
102
+ | { path: string; written: true; revision: number; stamped: unknown }
103
103
  | { path: string; written: false; reason: "stale-skip"; revision: number };
104
104
 
105
105
  export interface StateWriterOptions {
@@ -511,7 +511,7 @@ async function writeGuardedResolvedJsonAtomic(
511
511
  const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
512
512
  await atomicWrite(filePath, jsonText(next));
513
513
  await maybeAudit(filePath, options);
514
- return { path: filePath, written: true, revision: currentRevision + 1 };
514
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
515
515
  }
516
516
 
517
517
  const incomingSourceRevision =
@@ -526,7 +526,7 @@ async function writeGuardedResolvedJsonAtomic(
526
526
  );
527
527
  await atomicWrite(filePath, jsonText(next));
528
528
  await maybeAudit(filePath, options);
529
- return { path: filePath, written: true, revision: currentRevision + 1 };
529
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
530
530
  },
531
531
  options.lock,
532
532
  );
@@ -574,7 +574,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
574
574
  }
575
575
  await atomicWrite(filePath, jsonText(next));
576
576
  await maybeAudit(filePath, options);
577
- return { path: filePath, written: true, revision: currentRevision + 1 };
577
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
578
578
  }
579
579
 
580
580
  const incomingSourceRevision =
@@ -599,7 +599,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
599
599
  }
600
600
  await atomicWrite(filePath, jsonText(next));
601
601
  await maybeAudit(filePath, options);
602
- return { path: filePath, written: true, revision: currentRevision + 1 };
602
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
603
603
  },
604
604
  options.lock,
605
605
  );
@@ -253,13 +253,6 @@ export function validateCompletionReceipt(input: {
253
253
  goalId: input.goal.id,
254
254
  };
255
255
  }
256
- if (hashStructuredValue(event.gjcGoalJson) !== receipt.gjcGoalSnapshotHash) {
257
- return {
258
- state: "active_stale_receipt",
259
- message: `Ultragoal ${input.goal.id} receipt goal({"op":"get"}) snapshot hash does not match ledger.`,
260
- goalId: input.goal.id,
261
- };
262
- }
263
256
  if (input.goal.updatedAt !== receipt.verifiedAt) {
264
257
  return {
265
258
  state: "active_stale_receipt",
@@ -276,15 +269,28 @@ export function validateCompletionReceipt(input: {
276
269
  goalId: input.goal.id,
277
270
  };
278
271
  }
279
- const missingReceipts = requiredGoals(input.plan).filter(
280
- goal => goal.id !== input.goal.id && !goal.completionVerification,
281
- );
282
- if (missingReceipts.length > 0) {
283
- return {
284
- state: "active_missing_receipt",
285
- message: `Ultragoal final receipt is missing per-goal evidence for: ${missingReceipts.map(goal => goal.id).join(", ")}.`,
286
- goalId: input.goal.id,
287
- };
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
+ }
288
294
  }
289
295
  }
290
296
  return {
@@ -375,6 +381,120 @@ export async function readUltragoalVerificationState(input: {
375
381
  return receiptDiagnostic;
376
382
  }
377
383
 
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
+
378
498
  export async function isUltragoalAskBlocked(
379
499
  cwd: string,
380
500
  options: { sessionId?: string | null } = {},
@@ -600,8 +720,8 @@ export async function assertCanCompleteCurrentGoal(input: {
600
720
  sessionId?: string | null;
601
721
  }): Promise<void> {
602
722
  if (!input.cwd) return;
603
- const diagnostic = await readUltragoalVerificationState(input);
604
- 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;
605
725
  const nudge = await consumeUltragoalNudge({
606
726
  cwd: input.cwd,
607
727
  surface: "premature_complete",
@@ -610,7 +730,7 @@ export async function assertCanCompleteCurrentGoal(input: {
610
730
  });
611
731
  if (nudge.nudged) throw new Error(nudge.message);
612
732
  throw new Error(
613
- `${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.`,
614
734
  );
615
735
  }
616
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"] },