@gajae-code/coding-agent 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/types/config/model-registry.d.ts +8 -0
  3. package/dist/types/config/model-resolver.d.ts +4 -1
  4. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  5. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +26 -0
  6. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +44 -0
  7. package/dist/types/goals/tools/goal-tool.d.ts +4 -4
  8. package/dist/types/hooks/skill-state.d.ts +3 -0
  9. package/dist/types/modes/components/model-selector.d.ts +5 -7
  10. package/dist/types/modes/interactive-mode.d.ts +1 -0
  11. package/dist/types/sdk.d.ts +2 -4
  12. package/dist/types/session/agent-session.d.ts +3 -9
  13. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +28 -0
  14. package/package.json +13 -9
  15. package/src/config/model-registry.ts +45 -0
  16. package/src/config/model-resolver.ts +5 -1
  17. package/src/defaults/gjc/skills/deep-interview/SKILL.md +30 -30
  18. package/src/defaults/gjc/skills/team/SKILL.md +1 -0
  19. package/src/defaults/gjc/skills/ultragoal/SKILL.md +51 -21
  20. package/src/gjc-runtime/team-runtime.ts +80 -1
  21. package/src/gjc-runtime/ultragoal-guard.ts +239 -0
  22. package/src/gjc-runtime/ultragoal-runtime.ts +318 -4
  23. package/src/goals/tools/goal-tool.ts +10 -4
  24. package/src/hooks/native-skill-hook.ts +26 -0
  25. package/src/hooks/skill-state.ts +59 -0
  26. package/src/main.ts +2 -17
  27. package/src/modes/components/model-selector.ts +225 -33
  28. package/src/modes/controllers/selector-controller.ts +16 -3
  29. package/src/modes/interactive-mode.ts +34 -22
  30. package/src/modes/prompt-action-autocomplete.ts +40 -15
  31. package/src/sdk.ts +3 -1
  32. package/src/session/agent-session.ts +40 -4
  33. package/src/setup/model-onboarding-guidance.ts +5 -3
  34. package/src/skill-state/deep-interview-mutation-guard.ts +303 -0
  35. package/src/slash-commands/builtin-registry.ts +130 -11
  36. package/src/tools/ask.ts +55 -17
  37. package/src/tools/ast-edit.ts +7 -0
  38. package/src/tools/bash.ts +2 -1
  39. package/src/tools/gh.ts +37 -9
  40. package/src/tools/image-gen.ts +19 -10
  41. package/src/tools/path-utils.ts +1 -0
@@ -0,0 +1,239 @@
1
+ import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
2
+ import {
3
+ computeUltragoalPlanGeneration,
4
+ hashStructuredValue,
5
+ readUltragoalLedger,
6
+ readUltragoalPlan,
7
+ type UltragoalCompletionVerification,
8
+ type UltragoalGoal,
9
+ type UltragoalLedgerEvent,
10
+ type UltragoalPlan,
11
+ type UltragoalReceiptKind,
12
+ } from "./ultragoal-runtime";
13
+
14
+ export type UltragoalGuardState =
15
+ | "inactive"
16
+ | "unrelated_goal"
17
+ | "active_verified_complete"
18
+ | "active_missing_receipt"
19
+ | "active_stale_receipt"
20
+ | "active_missing_final_receipt"
21
+ | "active_dirty_quality_gate"
22
+ | "active_review_blocked_unrecorded"
23
+ | "active_review_blocked_recorded"
24
+ | "unreadable_fail_closed";
25
+
26
+ export interface UltragoalGuardDiagnostic {
27
+ state: UltragoalGuardState;
28
+ message: string;
29
+ goalId?: string;
30
+ }
31
+
32
+ export interface CurrentGoalLike {
33
+ objective: string;
34
+ status?: string;
35
+ }
36
+
37
+ function objectiveMatches(currentObjective: string, plan: UltragoalPlan): boolean {
38
+ const normalized = currentObjective.trim();
39
+ if (!normalized) return false;
40
+ if (normalized === plan.gjcObjective || normalized === DEFAULT_ULTRAGOAL_OBJECTIVE) return true;
41
+ if (plan.gjcObjectiveAliases?.some(alias => alias === normalized)) return true;
42
+ return plan.goals.some(goal => goal.objective === normalized);
43
+ }
44
+
45
+ function requiredGoals(plan: UltragoalPlan): UltragoalGoal[] {
46
+ return plan.goals.filter(goal => goal.status !== "superseded");
47
+ }
48
+
49
+ function findReceiptGoal(
50
+ plan: UltragoalPlan,
51
+ currentObjective: string,
52
+ ): { goal: UltragoalGoal; receiptKind: UltragoalReceiptKind } | null {
53
+ if (
54
+ currentObjective === plan.gjcObjective ||
55
+ currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE ||
56
+ plan.gjcObjectiveAliases?.some(alias => alias === currentObjective)
57
+ ) {
58
+ const finalGoal = [...requiredGoals(plan)]
59
+ .reverse()
60
+ .find(goal => goal.completionVerification?.receiptKind === "final-aggregate");
61
+ return finalGoal ? { goal: finalGoal, receiptKind: "final-aggregate" } : null;
62
+ }
63
+ const storyGoal = plan.goals.find(goal => goal.objective === currentObjective);
64
+ return storyGoal ? { goal: storyGoal, receiptKind: "per-goal" } : null;
65
+ }
66
+
67
+ function findLedgerReceiptEvent(
68
+ ledger: readonly UltragoalLedgerEvent[],
69
+ receipt: UltragoalCompletionVerification,
70
+ ): UltragoalLedgerEvent | null {
71
+ return (
72
+ ledger.find(event => {
73
+ if (event.eventId !== receipt.checkpointLedgerEventId) return false;
74
+ if (event.event !== "goal_checkpointed") return false;
75
+ if (event.goalId !== receipt.goalId) return false;
76
+ const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
77
+ return (
78
+ event.status === "complete" &&
79
+ eventReceipt?.receiptId === receipt.receiptId &&
80
+ eventReceipt.receiptKind === receipt.receiptKind &&
81
+ eventReceipt.planGeneration === receipt.planGeneration
82
+ );
83
+ }) ?? null
84
+ );
85
+ }
86
+
87
+ export function validateCompletionReceipt(input: {
88
+ plan: UltragoalPlan;
89
+ ledger: readonly UltragoalLedgerEvent[];
90
+ goal: UltragoalGoal;
91
+ receiptKind: UltragoalReceiptKind;
92
+ }): UltragoalGuardDiagnostic {
93
+ const receipt = input.goal.completionVerification;
94
+ if (!receipt) {
95
+ return {
96
+ state: input.receiptKind === "final-aggregate" ? "active_missing_final_receipt" : "active_missing_receipt",
97
+ message: `Ultragoal ${input.goal.id} has no ${input.receiptKind} completion verification receipt.`,
98
+ goalId: input.goal.id,
99
+ };
100
+ }
101
+ if (
102
+ receipt.schemaVersion !== 1 ||
103
+ receipt.goalId !== input.goal.id ||
104
+ receipt.receiptKind !== input.receiptKind ||
105
+ !receipt.planGeneration ||
106
+ !receipt.checkpointLedgerEventId
107
+ ) {
108
+ return {
109
+ state: "active_stale_receipt",
110
+ message: `Ultragoal ${input.goal.id} receipt is malformed or stale.`,
111
+ goalId: input.goal.id,
112
+ };
113
+ }
114
+ const event = findLedgerReceiptEvent(input.ledger, receipt);
115
+ if (!event) {
116
+ return {
117
+ state: "active_stale_receipt",
118
+ message: `Ultragoal ${input.goal.id} receipt ledger event is missing.`,
119
+ goalId: input.goal.id,
120
+ };
121
+ }
122
+ const generation = computeUltragoalPlanGeneration({
123
+ plan: input.plan,
124
+ ledger: input.ledger,
125
+ goal: input.goal,
126
+ receiptKind: input.receiptKind,
127
+ beforeStatus: receipt.goalStatusBeforeCheckpoint,
128
+ excludeEventId: receipt.checkpointLedgerEventId,
129
+ });
130
+ if (generation.planGeneration !== receipt.planGeneration) {
131
+ return {
132
+ state: "active_stale_receipt",
133
+ message: `Ultragoal ${input.goal.id} receipt generation is stale.`,
134
+ goalId: input.goal.id,
135
+ };
136
+ }
137
+ if (hashStructuredValue(event.qualityGateJson) !== receipt.qualityGateHash) {
138
+ return {
139
+ state: "active_dirty_quality_gate",
140
+ message: `Ultragoal ${input.goal.id} receipt quality-gate hash does not match ledger.`,
141
+ goalId: input.goal.id,
142
+ };
143
+ }
144
+ if (input.goal.updatedAt !== receipt.verifiedAt) {
145
+ return {
146
+ state: "active_stale_receipt",
147
+ message: `Ultragoal ${input.goal.id} receipt target changed after verification.`,
148
+ goalId: input.goal.id,
149
+ };
150
+ }
151
+ if (input.receiptKind === "final-aggregate") {
152
+ const incomplete = requiredGoals(input.plan).filter(goal => goal.status !== "complete");
153
+ if (incomplete.length > 0) {
154
+ return {
155
+ state: "active_missing_final_receipt",
156
+ message: `Ultragoal final receipt is not valid while required goals remain incomplete: ${incomplete.map(goal => goal.id).join(", ")}.`,
157
+ goalId: input.goal.id,
158
+ };
159
+ }
160
+ const missingReceipts = requiredGoals(input.plan).filter(
161
+ goal => goal.id !== input.goal.id && !goal.completionVerification,
162
+ );
163
+ if (missingReceipts.length > 0) {
164
+ return {
165
+ state: "active_missing_receipt",
166
+ message: `Ultragoal final receipt is missing per-goal evidence for: ${missingReceipts.map(goal => goal.id).join(", ")}.`,
167
+ goalId: input.goal.id,
168
+ };
169
+ }
170
+ }
171
+ return {
172
+ state: "active_verified_complete",
173
+ message: `Ultragoal ${input.goal.id} has a fresh ${input.receiptKind} receipt.`,
174
+ goalId: input.goal.id,
175
+ };
176
+ }
177
+
178
+ export async function readUltragoalVerificationState(input: {
179
+ cwd: string;
180
+ currentGoal?: CurrentGoalLike | null;
181
+ }): Promise<UltragoalGuardDiagnostic> {
182
+ const currentObjective = input.currentGoal?.objective?.trim() ?? "";
183
+ if (!currentObjective) return { state: "inactive", message: "No current goal objective is active." };
184
+ let plan: UltragoalPlan | null;
185
+ let ledger: UltragoalLedgerEvent[];
186
+ try {
187
+ plan = await readUltragoalPlan(input.cwd);
188
+ ledger = await readUltragoalLedger(input.cwd);
189
+ } catch (error) {
190
+ if (currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE) {
191
+ return {
192
+ state: "unreadable_fail_closed",
193
+ message: `Unable to read Ultragoal verification state: ${error instanceof Error ? error.message : String(error)}`,
194
+ };
195
+ }
196
+ return { state: "unrelated_goal", message: "Current goal is not an active Ultragoal objective." };
197
+ }
198
+ if (!plan) return { state: "inactive", message: "No Ultragoal plan exists." };
199
+ if (!objectiveMatches(currentObjective, plan))
200
+ return { state: "unrelated_goal", message: "Current goal is not an active Ultragoal objective." };
201
+ if (plan.goals.some(goal => goal.status === "review_blocked")) {
202
+ return {
203
+ state: "active_review_blocked_recorded",
204
+ message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
205
+ };
206
+ }
207
+ if (plan.goals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
208
+ return {
209
+ state: "active_dirty_quality_gate",
210
+ message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
211
+ };
212
+ }
213
+ const receiptTarget = findReceiptGoal(plan, currentObjective);
214
+ if (!receiptTarget) {
215
+ return {
216
+ state: "active_missing_final_receipt",
217
+ message: "Ultragoal aggregate completion requires a fresh final aggregate receipt.",
218
+ };
219
+ }
220
+ return validateCompletionReceipt({ plan, ledger, goal: receiptTarget.goal, receiptKind: receiptTarget.receiptKind });
221
+ }
222
+
223
+ export async function assertCanCompleteCurrentGoal(input: {
224
+ cwd: string;
225
+ currentGoal?: CurrentGoalLike | null;
226
+ }): Promise<void> {
227
+ if (!input.cwd) return;
228
+ const diagnostic = await readUltragoalVerificationState(input);
229
+ if (["inactive", "unrelated_goal", "active_verified_complete"].includes(diagnostic.state)) return;
230
+ throw new Error(
231
+ `${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.`,
232
+ );
233
+ }
234
+
235
+ export function isUltragoalBypassPrompt(prompt: string): boolean {
236
+ return /update_goal\s*\(|goal\s+complete|checkpoint[^\n]+--status\s+complete|skip\s+verification|weaken\s+verification|mark\s+.*complete/i.test(
237
+ prompt,
238
+ );
239
+ }
@@ -1,3 +1,4 @@
1
+ import * as crypto from "node:crypto";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
@@ -23,6 +24,7 @@ export interface UltragoalGoal {
23
24
  completedAt?: string;
24
25
  evidence?: string;
25
26
  steering?: Record<string, unknown>;
27
+ completionVerification?: UltragoalCompletionVerification;
26
28
  }
27
29
 
28
30
  export interface UltragoalPlan {
@@ -36,6 +38,36 @@ export interface UltragoalPlan {
36
38
  updatedAt: string;
37
39
  }
38
40
 
41
+ export type UltragoalReceiptKind = "per-goal" | "final-aggregate";
42
+
43
+ export interface UltragoalCompletionVerification {
44
+ schemaVersion: 1;
45
+ receiptId: string;
46
+ verifiedAt: string;
47
+ goalId: string;
48
+ receiptKind: UltragoalReceiptKind;
49
+ goalStatusBeforeCheckpoint: UltragoalGoalStatus;
50
+ gjcGoalMode: UltragoalGjcGoalMode;
51
+ gjcObjective: string;
52
+ qualityGateHash: string;
53
+ planGeneration: string;
54
+ basis: {
55
+ planHashBeforeCheckpoint: string;
56
+ latestRelevantLedgerEventIdBeforeCheckpoint: string | null;
57
+ goalUpdatedAtBeforeCheckpoint: string;
58
+ relevantGoalIdsBeforeCheckpoint: string[];
59
+ requiredGoalSetHashBeforeCheckpoint: string;
60
+ };
61
+ checkpointLedgerEventId: string;
62
+ }
63
+
64
+ export interface UltragoalLedgerEvent extends JsonObject {
65
+ eventId?: string;
66
+ event?: string;
67
+ goalId?: string;
68
+ timestamp?: string;
69
+ }
70
+
39
71
  export interface UltragoalPaths {
40
72
  dir: string;
41
73
  briefPath: string;
@@ -65,8 +97,33 @@ interface JsonObject {
65
97
  }
66
98
 
67
99
  const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "superseded"]);
100
+ const CLEAN_ARCHITECT_STATUS = "CLEAR";
101
+ const APPROVE_RECOMMENDATION = "APPROVE";
102
+ const PASSED_STATUS = "passed";
103
+
68
104
  const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
69
105
 
106
+ function stableStructuredValue(value: unknown): unknown {
107
+ if (Array.isArray(value)) return value.map(item => stableStructuredValue(item));
108
+ if (value && typeof value === "object") {
109
+ const record = value as Record<string, unknown>;
110
+ const output: Record<string, unknown> = {};
111
+ for (const key of Object.keys(record).sort()) {
112
+ const item = record[key];
113
+ if (item !== undefined) output[key] = stableStructuredValue(item);
114
+ }
115
+ return output;
116
+ }
117
+ return value;
118
+ }
119
+
120
+ export function hashStructuredValue(value: unknown): string {
121
+ return crypto
122
+ .createHash("sha256")
123
+ .update(JSON.stringify(stableStructuredValue(value)))
124
+ .digest("hex");
125
+ }
126
+
70
127
  export function getUltragoalPaths(cwd: string): UltragoalPaths {
71
128
  const dir = path.join(cwd, ".gjc", "ultragoal");
72
129
  return {
@@ -87,11 +144,30 @@ async function ensureUltragoalDir(paths: UltragoalPaths): Promise<void> {
87
144
  await fs.mkdir(paths.dir, { recursive: true });
88
145
  }
89
146
 
90
- async function appendLedger(cwd: string, event: JsonObject): Promise<void> {
147
+ async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLedgerEvent> {
91
148
  const paths = getUltragoalPaths(cwd);
92
149
  await ensureUltragoalDir(paths);
93
- const entry = { ...event, timestamp: new Date().toISOString() };
150
+ const entry: UltragoalLedgerEvent = {
151
+ eventId: typeof event.eventId === "string" ? event.eventId : crypto.randomUUID(),
152
+ ...event,
153
+ timestamp: new Date().toISOString(),
154
+ };
94
155
  await fs.appendFile(paths.ledgerPath, `${JSON.stringify(entry)}\n`);
156
+ return entry;
157
+ }
158
+
159
+ export async function readUltragoalLedger(cwd: string): Promise<UltragoalLedgerEvent[]> {
160
+ try {
161
+ const raw = await Bun.file(getUltragoalPaths(cwd).ledgerPath).text();
162
+ return raw
163
+ .split(/\r?\n/)
164
+ .map(line => line.trim())
165
+ .filter(line => line.length > 0)
166
+ .map(line => JSON.parse(line) as UltragoalLedgerEvent);
167
+ } catch (error) {
168
+ if (isEnoent(error)) return [];
169
+ throw error;
170
+ }
95
171
  }
96
172
 
97
173
  async function writePlan(cwd: string, plan: UltragoalPlan): Promise<void> {
@@ -159,6 +235,10 @@ function normalizePlan(raw: unknown): UltragoalPlan {
159
235
  typeof goalRecord.steering === "object" && goalRecord.steering !== null
160
236
  ? (goalRecord.steering as Record<string, unknown>)
161
237
  : undefined,
238
+ completionVerification:
239
+ typeof goalRecord.completionVerification === "object" && goalRecord.completionVerification !== null
240
+ ? (goalRecord.completionVerification as UltragoalCompletionVerification)
241
+ : undefined,
162
242
  };
163
243
  });
164
244
  const aliases = Array.isArray(record.gjcObjectiveAliases)
@@ -303,6 +383,201 @@ async function readStructuredValue(cwd: string, value: string): Promise<unknown>
303
383
  throw error;
304
384
  }
305
385
  }
386
+ function qualityGateObject(value: unknown): JsonObject | null {
387
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as JsonObject) : null;
388
+ }
389
+
390
+ function requiredReceiptGoals(plan: UltragoalPlan): UltragoalGoal[] {
391
+ return plan.goals.filter(goal => goal.status !== "superseded");
392
+ }
393
+
394
+ function chooseReceiptKind(
395
+ plan: UltragoalPlan,
396
+ goal: UltragoalGoal,
397
+ status: UltragoalGoalStatus,
398
+ ): UltragoalReceiptKind {
399
+ if (status !== "complete") return "per-goal";
400
+ const incomplete = requiredReceiptGoals(plan).filter(item => item.id !== goal.id && item.status !== "complete");
401
+ return incomplete.length === 0 ? "final-aggregate" : "per-goal";
402
+ }
403
+
404
+ function relevantGoalIds(plan: UltragoalPlan, goal: UltragoalGoal, receiptKind: UltragoalReceiptKind): string[] {
405
+ const ids = receiptKind === "final-aggregate" ? requiredReceiptGoals(plan).map(item => item.id) : [goal.id];
406
+ return [...new Set(ids)].sort();
407
+ }
408
+
409
+ function ledgerEventTouchesGoals(event: UltragoalLedgerEvent, goalIds: readonly string[]): boolean {
410
+ if (typeof event.goalId === "string" && goalIds.includes(event.goalId)) return true;
411
+ const eventGoalIds = Array.isArray(event.goalIds) ? event.goalIds : [];
412
+ return eventGoalIds.some(item => typeof item === "string" && goalIds.includes(item));
413
+ }
414
+
415
+ export function computeUltragoalPlanGeneration(input: {
416
+ plan: UltragoalPlan;
417
+ ledger: readonly UltragoalLedgerEvent[];
418
+ goal: UltragoalGoal;
419
+ receiptKind: UltragoalReceiptKind;
420
+ beforeStatus: UltragoalGoalStatus;
421
+ excludeEventId?: string;
422
+ }): UltragoalCompletionVerification["basis"] & { planGeneration: string } {
423
+ const goalIds = relevantGoalIds(input.plan, input.goal, input.receiptKind);
424
+ const planBeforeCheckpoint = structuredClone(input.plan) as UltragoalPlan;
425
+ const goalBeforeCheckpoint = planBeforeCheckpoint.goals.find(goal => goal.id === input.goal.id);
426
+ const receipt = input.goal.completionVerification;
427
+ const goalUpdatedAtBeforeCheckpoint = receipt?.basis.goalUpdatedAtBeforeCheckpoint ?? input.goal.updatedAt;
428
+ if (goalBeforeCheckpoint) {
429
+ goalBeforeCheckpoint.status = input.beforeStatus;
430
+ goalBeforeCheckpoint.updatedAt = goalUpdatedAtBeforeCheckpoint;
431
+ delete goalBeforeCheckpoint.completedAt;
432
+ delete goalBeforeCheckpoint.completionVerification;
433
+ }
434
+ const relevantLedger = input.ledger.filter(event => {
435
+ if (input.excludeEventId && event.eventId === input.excludeEventId) return false;
436
+ return ledgerEventTouchesGoals(event, goalIds);
437
+ });
438
+ const latestRelevantEvent = relevantLedger.at(-1) ?? null;
439
+ const basis = {
440
+ planHashBeforeCheckpoint: hashStructuredValue(planBeforeCheckpoint),
441
+ latestRelevantLedgerEventIdBeforeCheckpoint:
442
+ typeof latestRelevantEvent?.eventId === "string" ? latestRelevantEvent.eventId : null,
443
+ goalUpdatedAtBeforeCheckpoint,
444
+ relevantGoalIdsBeforeCheckpoint: goalIds,
445
+ requiredGoalSetHashBeforeCheckpoint: hashStructuredValue(goalIds),
446
+ };
447
+ return {
448
+ ...basis,
449
+ planGeneration: hashStructuredValue({
450
+ receiptKind: input.receiptKind,
451
+ goalId: input.goal.id,
452
+ beforeStatus: input.beforeStatus,
453
+ basis,
454
+ }),
455
+ };
456
+ }
457
+
458
+ function buildCompletionReceipt(input: {
459
+ plan: UltragoalPlan;
460
+ ledger: readonly UltragoalLedgerEvent[];
461
+ goal: UltragoalGoal;
462
+ receiptKind: UltragoalReceiptKind;
463
+ beforeStatus: UltragoalGoalStatus;
464
+ qualityGateJson: JsonObject;
465
+ now: string;
466
+ checkpointLedgerEventId: string;
467
+ }): UltragoalCompletionVerification {
468
+ const generation = computeUltragoalPlanGeneration({
469
+ plan: input.plan,
470
+ ledger: input.ledger,
471
+ goal: input.goal,
472
+ receiptKind: input.receiptKind,
473
+ beforeStatus: input.beforeStatus,
474
+ excludeEventId: input.checkpointLedgerEventId,
475
+ });
476
+ return {
477
+ schemaVersion: 1,
478
+ receiptId: crypto.randomUUID(),
479
+ verifiedAt: input.now,
480
+ goalId: input.goal.id,
481
+ receiptKind: input.receiptKind,
482
+ goalStatusBeforeCheckpoint: input.beforeStatus,
483
+ gjcGoalMode: input.plan.gjcGoalMode,
484
+ gjcObjective: input.plan.gjcObjective,
485
+ qualityGateHash: hashStructuredValue(input.qualityGateJson),
486
+ planGeneration: generation.planGeneration,
487
+ basis: {
488
+ planHashBeforeCheckpoint: generation.planHashBeforeCheckpoint,
489
+ latestRelevantLedgerEventIdBeforeCheckpoint: generation.latestRelevantLedgerEventIdBeforeCheckpoint,
490
+ goalUpdatedAtBeforeCheckpoint: generation.goalUpdatedAtBeforeCheckpoint,
491
+ relevantGoalIdsBeforeCheckpoint: generation.relevantGoalIdsBeforeCheckpoint,
492
+ requiredGoalSetHashBeforeCheckpoint: generation.requiredGoalSetHashBeforeCheckpoint,
493
+ },
494
+ checkpointLedgerEventId: input.checkpointLedgerEventId,
495
+ };
496
+ }
497
+ function nonEmptyStringArray(value: unknown): string[] | null {
498
+ if (!Array.isArray(value)) return null;
499
+ const items = value.filter(item => typeof item === "string" && item.trim().length > 0);
500
+ return items.length === value.length && items.length > 0 ? items : null;
501
+ }
502
+
503
+ function emptyArray(value: unknown): boolean {
504
+ return Array.isArray(value) && value.length === 0;
505
+ }
506
+
507
+ function assertCleanSection(
508
+ section: JsonObject | null,
509
+ checks: Record<string, string>,
510
+ commandField: string,
511
+ pathPrefix: string,
512
+ ): void {
513
+ if (!section) throw new Error(`qualityGate.${pathPrefix} is required`);
514
+ for (const [field, expected] of Object.entries(checks)) {
515
+ if (section[field] !== expected) throw new Error(`qualityGate.${pathPrefix}.${field} must be ${expected}`);
516
+ }
517
+ if (!nonEmptyString(section.evidence)) throw new Error(`qualityGate.${pathPrefix}.evidence is required`);
518
+ if (!nonEmptyStringArray(section[commandField]))
519
+ throw new Error(`qualityGate.${pathPrefix}.${commandField} is required`);
520
+ if (!emptyArray(section.blockers)) throw new Error(`qualityGate.${pathPrefix}.blockers must be empty`);
521
+ }
522
+
523
+ async function readRequiredCompletionQualityGate(cwd: string, value: string | undefined): Promise<JsonObject> {
524
+ if (!value?.trim()) {
525
+ throw new Error("complete checkpoints require --quality-gate-json; requires --quality-gate-json");
526
+ }
527
+ const gate = await readStructuredValue(cwd, value);
528
+ const gateObject = qualityGateObject(gate);
529
+ if (!gateObject) throw new Error("qualityGate must be a JSON object");
530
+ const legacyCodeReview = qualityGateObject(gateObject.codeReview);
531
+ if (
532
+ legacyCodeReview &&
533
+ (legacyCodeReview.recommendation !== APPROVE_RECOMMENDATION ||
534
+ legacyCodeReview.architectStatus !== CLEAN_ARCHITECT_STATUS)
535
+ ) {
536
+ throw new Error(
537
+ "checkpoint --status complete requires architect review approval: codeReview.recommendation must be APPROVE and codeReview.architectStatus must be CLEAR",
538
+ );
539
+ }
540
+ const allowedKeys = new Set(["architectReview", "executorQa", "iteration"]);
541
+ const unsupportedKeys = Object.keys(gateObject).filter(key => !allowedKeys.has(key));
542
+ if (unsupportedKeys.length > 0) {
543
+ throw new Error(`qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`);
544
+ }
545
+ assertCleanSection(
546
+ qualityGateObject(gateObject.architectReview),
547
+ {
548
+ architectureStatus: CLEAN_ARCHITECT_STATUS,
549
+ productStatus: CLEAN_ARCHITECT_STATUS,
550
+ codeStatus: CLEAN_ARCHITECT_STATUS,
551
+ recommendation: APPROVE_RECOMMENDATION,
552
+ },
553
+ "commands",
554
+ "architectReview",
555
+ );
556
+ assertCleanSection(
557
+ qualityGateObject(gateObject.executorQa),
558
+ {
559
+ status: PASSED_STATUS,
560
+ e2eStatus: PASSED_STATUS,
561
+ redTeamStatus: PASSED_STATUS,
562
+ },
563
+ "e2eCommands",
564
+ "executorQa",
565
+ );
566
+ const executorQa = qualityGateObject(gateObject.executorQa);
567
+ if (!nonEmptyStringArray(executorQa?.redTeamCommands))
568
+ throw new Error("qualityGate.executorQa.redTeamCommands is required");
569
+ assertCleanSection(
570
+ qualityGateObject(gateObject.iteration),
571
+ {
572
+ status: PASSED_STATUS,
573
+ },
574
+ "rerunCommands",
575
+ "iteration",
576
+ );
577
+ const iteration = qualityGateObject(gateObject.iteration);
578
+ if (iteration?.fullRerun !== true) throw new Error("qualityGate.iteration.fullRerun must be true");
579
+ return gateObject;
580
+ }
306
581
 
307
582
  export async function checkpointUltragoalGoal(input: {
308
583
  cwd: string;
@@ -318,7 +593,44 @@ export async function checkpointUltragoalGoal(input: {
318
593
  if (!goal) throw new Error(`No ultragoal goal found for ${input.goalId}.`);
319
594
  const evidence = input.evidence.trim();
320
595
  if (!evidence) throw new Error("checkpoint evidence is required");
596
+ const qualityGateJson =
597
+ input.status === "complete"
598
+ ? await readRequiredCompletionQualityGate(input.cwd, input.qualityGateJson)
599
+ : input.qualityGateJson
600
+ ? await readStructuredValue(input.cwd, input.qualityGateJson)
601
+ : undefined;
602
+ if (input.status === "complete" && !input.gjcGoalJson?.trim()) {
603
+ throw new Error("complete checkpoints require --gjc-goal-json with a fresh get_goal snapshot");
604
+ }
321
605
  const now = new Date().toISOString();
606
+ const ledgerBefore = await readUltragoalLedger(input.cwd);
607
+ const beforeStatus = goal.status;
608
+ if (input.status === "complete") {
609
+ const blockedGoalId =
610
+ typeof goal.steering?.kind === "string" && goal.steering.kind === "review_blocker"
611
+ ? nonEmptyString(goal.steering.blockedGoalId)
612
+ : null;
613
+ const blockedGoal = blockedGoalId ? plan.goals.find(item => item.id === blockedGoalId) : undefined;
614
+ if (blockedGoal?.status === "review_blocked") {
615
+ blockedGoal.status = "superseded";
616
+ blockedGoal.evidence = `Resolved by verification blocker story ${goal.id}: ${evidence}`;
617
+ blockedGoal.updatedAt = now;
618
+ }
619
+ }
620
+ const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, goal, input.status) : null;
621
+ const pendingCheckpointEventId = crypto.randomUUID();
622
+ if (input.status === "complete" && receiptKind && qualityGateJson && !Array.isArray(qualityGateJson)) {
623
+ goal.completionVerification = buildCompletionReceipt({
624
+ plan,
625
+ ledger: ledgerBefore,
626
+ goal,
627
+ receiptKind,
628
+ beforeStatus,
629
+ qualityGateJson: qualityGateJson as JsonObject,
630
+ now,
631
+ checkpointLedgerEventId: pendingCheckpointEventId,
632
+ });
633
+ }
322
634
  goal.status = input.status;
323
635
  goal.evidence = evidence;
324
636
  goal.updatedAt = now;
@@ -326,12 +638,13 @@ export async function checkpointUltragoalGoal(input: {
326
638
  plan.updatedAt = now;
327
639
  await writePlan(input.cwd, plan);
328
640
  await appendLedger(input.cwd, {
641
+ eventId: pendingCheckpointEventId,
329
642
  event: "goal_checkpointed",
330
643
  goalId: goal.id,
331
644
  status: input.status,
332
645
  evidence,
333
646
  gjcGoalJson: input.gjcGoalJson ? await readStructuredValue(input.cwd, input.gjcGoalJson) : undefined,
334
- qualityGateJson: input.qualityGateJson ? await readStructuredValue(input.cwd, input.qualityGateJson) : undefined,
647
+ qualityGateJson,
335
648
  });
336
649
  return plan;
337
650
  }
@@ -480,7 +793,8 @@ function renderCompleteHandoff(
480
793
  `Ultragoal handoff: ${result.goal.id} — ${result.goal.title}`,
481
794
  `Objective: ${result.goal.objective}`,
482
795
  `GJC objective: ${result.plan.gjcObjective}`,
483
- "Call get_goal({}); create_goal only if no active GJC goal exists, then complete this GJC story and checkpoint it.",
796
+ "Call get_goal({}); create_goal only if no active GJC goal exists, then complete this GJC story.",
797
+ "Before checkpointing complete, obtain a passing architectReview (architecture/product/code CLEAR + APPROVE) and executorQa (e2e/red-team passed); record blockers instead of completing on any finding.",
484
798
  "",
485
799
  ].join("\n");
486
800
  }
@@ -4,6 +4,7 @@ import { Text } from "@gajae-code/tui";
4
4
  import { formatNumber, prompt } from "@gajae-code/utils";
5
5
  import * as z from "zod/v4";
6
6
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
7
+ import { assertCanCompleteCurrentGoal } from "../../gjc-runtime/ultragoal-guard";
7
8
  import type { Theme, ThemeColor } from "../../modes/theme/theme";
8
9
  import createGoalDescription from "../../prompts/tools/create-goal.md" with { type: "text" };
9
10
  import getGoalDescription from "../../prompts/tools/get-goal.md" with { type: "text" };
@@ -128,6 +129,11 @@ async function executeGoalOperation(session: ToolSession, params: GoalToolInput)
128
129
  const dropped = await runtime.dropGoal();
129
130
  return buildGoalToolResponse(dropped ?? null);
130
131
  }
132
+ try {
133
+ await assertCanCompleteCurrentGoal({ cwd: session.cwd, currentGoal: session.getGoalModeState?.()?.goal ?? null });
134
+ } catch (error) {
135
+ throw new ToolError(error instanceof Error ? error.message : String(error));
136
+ }
131
137
  const completed = await runtime.completeGoalFromTool();
132
138
  return buildGoalToolResponse(completed, { includeCompletionReport: true });
133
139
  }
@@ -135,7 +141,7 @@ async function executeGoalOperation(session: ToolSession, params: GoalToolInput)
135
141
  export class GoalTool implements AgentTool<typeof goalSchema, GoalToolDetails> {
136
142
  readonly name = "goal";
137
143
  readonly label = "Goal";
138
- readonly loadMode = "essential";
144
+ readonly loadMode = "essential" as const;
139
145
  readonly description = prompt.render(goalDescription);
140
146
  readonly parameters = goalSchema;
141
147
  readonly strict = true;
@@ -161,7 +167,7 @@ export class GoalTool implements AgentTool<typeof goalSchema, GoalToolDetails> {
161
167
  export class GetGoalTool implements AgentTool<typeof getGoalSchema, GoalToolDetails> {
162
168
  readonly name = "get_goal";
163
169
  readonly label = "Get Goal";
164
- readonly loadMode = "essential";
170
+ readonly loadMode = "essential" as const;
165
171
  readonly description = prompt.render(getGoalDescription);
166
172
  readonly parameters = getGoalSchema;
167
173
  readonly strict = true;
@@ -191,7 +197,7 @@ export class GetGoalTool implements AgentTool<typeof getGoalSchema, GoalToolDeta
191
197
  export class CreateGoalTool implements AgentTool<typeof createGoalSchema, GoalToolDetails> {
192
198
  readonly name = "create_goal";
193
199
  readonly label = "Create Goal";
194
- readonly loadMode = "essential";
200
+ readonly loadMode = "essential" as const;
195
201
  readonly description = prompt.render(createGoalDescription);
196
202
  readonly parameters = createGoalSchema;
197
203
  readonly strict = true;
@@ -225,7 +231,7 @@ export class CreateGoalTool implements AgentTool<typeof createGoalSchema, GoalTo
225
231
  export class UpdateGoalTool implements AgentTool<typeof updateGoalSchema, GoalToolDetails> {
226
232
  readonly name = "update_goal";
227
233
  readonly label = "Update Goal";
228
- readonly loadMode = "essential";
234
+ readonly loadMode = "essential" as const;
229
235
  readonly description = prompt.render(updateGoalDescription);
230
236
  readonly parameters = updateGoalSchema;
231
237
  readonly strict = true;