@gajae-code/coding-agent 0.1.3 → 0.2.1

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 (50) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/types/cli/skills-cli.d.ts +9 -0
  3. package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
  4. package/dist/types/commands/skills.d.ts +26 -0
  5. package/dist/types/config/model-registry.d.ts +31 -2
  6. package/dist/types/config/models-config-schema.d.ts +39 -0
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +35 -1
  9. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +15 -10
  10. package/dist/types/hooks/skill-state.d.ts +4 -1
  11. package/dist/types/modes/components/model-selector.d.ts +21 -1
  12. package/dist/types/skill-state/active-state.d.ts +19 -0
  13. package/dist/types/skill-state/workflow-hud.d.ts +62 -0
  14. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  15. package/package.json +7 -7
  16. package/src/cli/args.ts +14 -0
  17. package/src/cli/skills-cli.ts +88 -0
  18. package/src/cli.ts +1 -0
  19. package/src/commands/deep-interview.ts +21 -2
  20. package/src/commands/gjc-runtime-bridge.ts +161 -15
  21. package/src/commands/ralplan.ts +21 -2
  22. package/src/commands/skills.ts +48 -0
  23. package/src/commands/team.ts +54 -3
  24. package/src/commands/ultragoal.ts +21 -1
  25. package/src/commit/agentic/index.ts +1 -0
  26. package/src/commit/pipeline.ts +1 -0
  27. package/src/config/model-registry.ts +259 -8
  28. package/src/config/models-config-schema.ts +18 -0
  29. package/src/defaults/gjc/skills/deep-interview/SKILL.md +6 -6
  30. package/src/defaults/gjc/skills/ralplan/SKILL.md +5 -9
  31. package/src/defaults/gjc/skills/team/SKILL.md +4 -4
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -8
  33. package/src/gjc-runtime/launch-tmux.ts +73 -2
  34. package/src/gjc-runtime/team-runtime.ts +285 -34
  35. package/src/gjc-runtime/ultragoal-guard.ts +43 -1
  36. package/src/gjc-runtime/ultragoal-runtime.ts +307 -187
  37. package/src/hooks/skill-state.ts +4 -1
  38. package/src/internal-urls/docs-index.generated.ts +1 -1
  39. package/src/main.ts +10 -1
  40. package/src/modes/components/model-selector.ts +109 -28
  41. package/src/modes/components/skill-hud/render.ts +35 -8
  42. package/src/modes/controllers/selector-controller.ts +42 -2
  43. package/src/prompts/system/system-prompt.md +5 -4
  44. package/src/sdk.ts +1 -0
  45. package/src/session/agent-session.ts +6 -0
  46. package/src/setup/provider-onboarding.ts +2 -0
  47. package/src/skill-state/active-state.ts +104 -4
  48. package/src/skill-state/workflow-hud.ts +160 -0
  49. package/src/slash-commands/acp-builtins.ts +11 -2
  50. package/src/slash-commands/builtin-registry.ts +16 -1
@@ -1,6 +1,8 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import type { WorkflowHudSummary } from "../skill-state/active-state";
5
+ import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
4
6
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
5
7
 
6
8
  export type UltragoalGjcGoalMode = "aggregate" | "per-story";
@@ -50,6 +52,7 @@ export interface UltragoalCompletionVerification {
50
52
  gjcGoalMode: UltragoalGjcGoalMode;
51
53
  gjcObjective: string;
52
54
  qualityGateHash: string;
55
+ gjcGoalSnapshotHash: string;
53
56
  planGeneration: string;
54
57
  basis: {
55
58
  planHashBeforeCheckpoint: string;
@@ -100,21 +103,21 @@ const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "
100
103
  const CLEAN_ARCHITECT_STATUS = "CLEAR";
101
104
  const APPROVE_RECOMMENDATION = "APPROVE";
102
105
  const PASSED_STATUS = "passed";
106
+ const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
107
+ const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
103
108
 
104
109
  const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
105
110
 
106
111
  function stableStructuredValue(value: unknown): unknown {
107
112
  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;
113
+ if (typeof value !== "object" || value === null) return value;
114
+ const record = value as Record<string, unknown>;
115
+ const sorted: Record<string, unknown> = {};
116
+ for (const key of Object.keys(record).sort()) {
117
+ const item = record[key];
118
+ if (item !== undefined) sorted[key] = stableStructuredValue(item);
116
119
  }
117
- return value;
120
+ return sorted;
118
121
  }
119
122
 
120
123
  export function hashStructuredValue(value: unknown): string {
@@ -177,6 +180,151 @@ async function writePlan(cwd: string, plan: UltragoalPlan): Promise<void> {
177
180
  await Bun.write(paths.goalsPath, `${JSON.stringify(plan, null, 2)}\n`);
178
181
  }
179
182
 
183
+ function requiredUltragoalGoals(plan: UltragoalPlan): UltragoalGoal[] {
184
+ return plan.goals.filter(goal => goal.status !== "superseded");
185
+ }
186
+
187
+ function receiptRelevantGoals(
188
+ plan: UltragoalPlan,
189
+ goal: UltragoalGoal,
190
+ receiptKind: UltragoalReceiptKind,
191
+ ): UltragoalGoal[] {
192
+ return receiptKind === "final-aggregate" ? requiredUltragoalGoals(plan) : [goal];
193
+ }
194
+
195
+ function ledgerEventId(event: UltragoalLedgerEvent): string | null {
196
+ return typeof event.eventId === "string" && event.eventId.trim().length > 0 ? event.eventId : null;
197
+ }
198
+
199
+ function latestRelevantLedgerEventId(
200
+ ledger: readonly UltragoalLedgerEvent[],
201
+ relevantGoalIds: readonly string[],
202
+ excludeEventId?: string,
203
+ ): string | null {
204
+ const relevant = new Set(relevantGoalIds);
205
+ for (const event of [...ledger].reverse()) {
206
+ const eventId = ledgerEventId(event);
207
+ if (eventId && eventId === excludeEventId) continue;
208
+ const goalId = typeof event.goalId === "string" ? event.goalId : null;
209
+ if (!goalId || relevant.has(goalId)) return eventId;
210
+ }
211
+ return null;
212
+ }
213
+
214
+ function planSnapshotForReceipt(input: {
215
+ plan: UltragoalPlan;
216
+ goal: UltragoalGoal;
217
+ beforeStatus: UltragoalGoalStatus;
218
+ targetGoalUpdatedAt: string;
219
+ }): unknown {
220
+ return {
221
+ ...input.plan,
222
+ updatedAt: undefined,
223
+ goals: input.plan.goals.map(goal => ({
224
+ ...goal,
225
+ status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
226
+ updatedAt: goal.id === input.goal.id ? input.targetGoalUpdatedAt : goal.updatedAt,
227
+ evidence: goal.id === input.goal.id ? undefined : goal.evidence,
228
+ completedAt: goal.id === input.goal.id ? undefined : goal.completedAt,
229
+ completionVerification: undefined,
230
+ })),
231
+ };
232
+ }
233
+
234
+ export function computeUltragoalPlanGeneration(input: {
235
+ plan: UltragoalPlan;
236
+ ledger: readonly UltragoalLedgerEvent[];
237
+ goal: UltragoalGoal;
238
+ receiptKind: UltragoalReceiptKind;
239
+ beforeStatus: UltragoalGoalStatus;
240
+ excludeEventId?: string;
241
+ targetGoalUpdatedAt?: string;
242
+ }): {
243
+ planGeneration: string;
244
+ basis: UltragoalCompletionVerification["basis"];
245
+ } {
246
+ const relevantGoals = receiptRelevantGoals(input.plan, input.goal, input.receiptKind);
247
+ const relevantGoalIds = relevantGoals.map(goal => goal.id);
248
+ const targetGoalUpdatedAt = input.targetGoalUpdatedAt ?? input.goal.updatedAt;
249
+ const planHashBeforeCheckpoint = hashStructuredValue(
250
+ planSnapshotForReceipt({
251
+ plan: input.plan,
252
+ goal: input.goal,
253
+ beforeStatus: input.beforeStatus,
254
+ targetGoalUpdatedAt,
255
+ }),
256
+ );
257
+ const requiredGoalSetHashBeforeCheckpoint = hashStructuredValue(
258
+ relevantGoals.map(goal => ({
259
+ id: goal.id,
260
+ status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
261
+ updatedAt: goal.id === input.goal.id ? targetGoalUpdatedAt : goal.updatedAt,
262
+ })),
263
+ );
264
+ const basis: UltragoalCompletionVerification["basis"] = {
265
+ planHashBeforeCheckpoint,
266
+ latestRelevantLedgerEventIdBeforeCheckpoint: latestRelevantLedgerEventId(
267
+ input.ledger,
268
+ relevantGoalIds,
269
+ input.excludeEventId,
270
+ ),
271
+ goalUpdatedAtBeforeCheckpoint: targetGoalUpdatedAt,
272
+ relevantGoalIdsBeforeCheckpoint: relevantGoalIds,
273
+ requiredGoalSetHashBeforeCheckpoint,
274
+ };
275
+ return { planGeneration: hashStructuredValue(basis), basis };
276
+ }
277
+
278
+ function chooseReceiptKind(
279
+ plan: UltragoalPlan,
280
+ goal: UltragoalGoal,
281
+ status: UltragoalGoalStatus,
282
+ ): UltragoalReceiptKind {
283
+ if (plan.gjcGoalMode === "per-story") return "per-goal";
284
+ if (status !== "complete") return "per-goal";
285
+ const unfinishedRequiredGoals = requiredUltragoalGoals(plan).filter(
286
+ item => item.id !== goal.id && !TERMINAL_OR_SKIPPED_STATUSES.has(item.status),
287
+ );
288
+ return unfinishedRequiredGoals.length === 0 ? "final-aggregate" : "per-goal";
289
+ }
290
+
291
+ function buildCompletionReceipt(input: {
292
+ plan: UltragoalPlan;
293
+ ledger: readonly UltragoalLedgerEvent[];
294
+ goal: UltragoalGoal;
295
+ receiptKind: UltragoalReceiptKind;
296
+ beforeStatus: UltragoalGoalStatus;
297
+ qualityGateJson: JsonObject;
298
+ gjcGoalJson: JsonObject;
299
+ now: string;
300
+ checkpointLedgerEventId: string;
301
+ }): UltragoalCompletionVerification {
302
+ const generation = computeUltragoalPlanGeneration({
303
+ plan: input.plan,
304
+ ledger: input.ledger,
305
+ goal: input.goal,
306
+ receiptKind: input.receiptKind,
307
+ beforeStatus: input.beforeStatus,
308
+ targetGoalUpdatedAt: input.now,
309
+ excludeEventId: input.checkpointLedgerEventId,
310
+ });
311
+ return {
312
+ schemaVersion: 1,
313
+ receiptId: crypto.randomUUID(),
314
+ verifiedAt: input.now,
315
+ goalId: input.goal.id,
316
+ receiptKind: input.receiptKind,
317
+ goalStatusBeforeCheckpoint: input.beforeStatus,
318
+ gjcGoalMode: input.plan.gjcGoalMode,
319
+ gjcObjective: input.plan.gjcObjective,
320
+ qualityGateHash: hashStructuredValue(input.qualityGateJson),
321
+ gjcGoalSnapshotHash: hashStructuredValue(input.gjcGoalJson),
322
+ planGeneration: generation.planGeneration,
323
+ basis: generation.basis,
324
+ checkpointLedgerEventId: input.checkpointLedgerEventId,
325
+ };
326
+ }
327
+
180
328
  function nonEmptyString(value: unknown): string | null {
181
329
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
182
330
  }
@@ -302,6 +450,19 @@ export async function getUltragoalStatus(cwd: string): Promise<UltragoalStatusSu
302
450
  goals: plan.goals,
303
451
  };
304
452
  }
453
+ export function buildUltragoalHudSummary(
454
+ summary: UltragoalStatusSummary,
455
+ latestLedger?: UltragoalLedgerEvent,
456
+ ): WorkflowHudSummary {
457
+ return buildWorkflowUltragoalHudSummary({
458
+ status: summary.status,
459
+ currentGoal: summary.currentGoal,
460
+ counts: summary.counts,
461
+ goals: summary.goals,
462
+ latestLedgerEvent: latestLedger,
463
+ updatedAt: new Date().toISOString(),
464
+ });
465
+ }
305
466
 
306
467
  function titleFromBrief(brief: string): string {
307
468
  const firstLine = brief
@@ -387,196 +548,134 @@ function qualityGateObject(value: unknown): JsonObject | null {
387
548
  return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as JsonObject) : null;
388
549
  }
389
550
 
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
551
  function nonEmptyStringArray(value: unknown): string[] | null {
498
552
  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;
553
+ const strings = value.filter(item => typeof item === "string" && item.trim().length > 0);
554
+ return strings.length === value.length && strings.length > 0 ? strings : null;
501
555
  }
502
556
 
503
- function emptyArray(value: unknown): boolean {
504
- return Array.isArray(value) && value.length === 0;
557
+ function requireNonEmptyString(value: unknown, fieldName: string): void {
558
+ if (typeof value !== "string" || value.trim().length === 0) {
559
+ throw new Error(`qualityGate ${fieldName} must be a non-empty string`);
560
+ }
505
561
  }
506
562
 
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}`);
563
+ function requireEmptyBlockers(value: unknown, fieldName: string): void {
564
+ if (!Array.isArray(value) || value.length !== 0) {
565
+ throw new Error(`qualityGate ${fieldName} must be an empty blockers array`);
516
566
  }
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
567
  }
522
568
 
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
- ) {
569
+ function validateCompletionQualityGate(gate: JsonObject): void {
570
+ const codeReview = qualityGateObject(gate.codeReview);
571
+ if (codeReview) {
536
572
  throw new Error(
537
- "checkpoint --status complete requires architect review approval: codeReview.recommendation must be APPROVE and codeReview.architectStatus must be CLEAR",
573
+ "checkpoint --status complete requires architect review approval through architectReview, executorQa, and iteration quality-gate evidence; legacy codeReview-only gates are not sufficient",
538
574
  );
539
575
  }
540
576
  const allowedKeys = new Set(["architectReview", "executorQa", "iteration"]);
541
- const unsupportedKeys = Object.keys(gateObject).filter(key => !allowedKeys.has(key));
577
+ const unsupportedKeys = Object.keys(gate).filter(key => !allowedKeys.has(key));
542
578
  if (unsupportedKeys.length > 0) {
543
579
  throw new Error(`qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`);
544
580
  }
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;
581
+ const architectReview = qualityGateObject(gate.architectReview);
582
+ const executorQa = qualityGateObject(gate.executorQa);
583
+ const iteration = qualityGateObject(gate.iteration);
584
+ if (!architectReview || !executorQa || !iteration) {
585
+ throw new Error("qualityGate requires architectReview, executorQa, and iteration objects");
586
+ }
587
+ if (
588
+ architectReview.architectureStatus !== CLEAN_ARCHITECT_STATUS ||
589
+ architectReview.productStatus !== CLEAN_ARCHITECT_STATUS ||
590
+ architectReview.codeStatus !== CLEAN_ARCHITECT_STATUS ||
591
+ architectReview.recommendation !== APPROVE_RECOMMENDATION
592
+ ) {
593
+ throw new Error(
594
+ "checkpoint --status complete requires architect review approval: architectReview architecture/product/code must be CLEAR and recommendation must be APPROVE",
595
+ );
596
+ }
597
+ if (!nonEmptyStringArray(architectReview.commands)) {
598
+ throw new Error("qualityGate architectReview.commands must be a non-empty string array");
599
+ }
600
+ requireNonEmptyString(architectReview.evidence, "architectReview.evidence");
601
+ requireEmptyBlockers(architectReview.blockers, "architectReview.blockers");
602
+ if (
603
+ executorQa.status !== PASSED_STATUS ||
604
+ executorQa.e2eStatus !== PASSED_STATUS ||
605
+ executorQa.redTeamStatus !== PASSED_STATUS
606
+ ) {
607
+ throw new Error("qualityGate executorQa status, e2eStatus, and redTeamStatus must be passed");
608
+ }
609
+ if (!nonEmptyStringArray(executorQa.e2eCommands) || !nonEmptyStringArray(executorQa.redTeamCommands)) {
610
+ throw new Error("qualityGate executorQa e2eCommands and redTeamCommands must be non-empty string arrays");
611
+ }
612
+ requireNonEmptyString(executorQa.evidence, "executorQa.evidence");
613
+ requireEmptyBlockers(executorQa.blockers, "executorQa.blockers");
614
+ if (iteration.status !== PASSED_STATUS || iteration.fullRerun !== true) {
615
+ throw new Error("qualityGate iteration must be passed with fullRerun true");
616
+ }
617
+ if (!nonEmptyStringArray(iteration.rerunCommands)) {
618
+ throw new Error("qualityGate iteration.rerunCommands must be a non-empty string array");
619
+ }
620
+ requireNonEmptyString(iteration.evidence, "iteration.evidence");
621
+ requireEmptyBlockers(iteration.blockers, "iteration.blockers");
622
+ }
623
+
624
+ async function readRequiredCompletionQualityGate(cwd: string, value: string | undefined): Promise<unknown> {
625
+ if (!value?.trim()) {
626
+ throw new Error(
627
+ "complete checkpoints require --quality-gate-json with architectReview, executorQa, and iteration evidence",
628
+ );
629
+ }
630
+ const gate = await readStructuredValue(cwd, value);
631
+ const gateObject = qualityGateObject(gate);
632
+ if (!gateObject) throw new Error("qualityGate must be a JSON object");
633
+ validateCompletionQualityGate(gateObject);
634
+ return gate;
635
+ }
636
+
637
+ async function readGjcGoalSnapshot(input: {
638
+ cwd: string;
639
+ value: string | undefined;
640
+ plan: UltragoalPlan;
641
+ goal?: UltragoalGoal;
642
+ required: boolean;
643
+ errorPrefix: string;
644
+ allowCompletedLegacyBlocker?: boolean;
645
+ }): Promise<unknown> {
646
+ if (!input.value?.trim()) {
647
+ if (!input.required) return undefined;
648
+ throw new Error(`${input.errorPrefix} require --gjc-goal-json from a fresh active get_goal snapshot`);
649
+ }
650
+ const snapshot = await readStructuredValue(input.cwd, input.value);
651
+ const snapshotObject = qualityGateObject(snapshot);
652
+ const detailsObject = qualityGateObject(snapshotObject?.details);
653
+ const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
654
+ if (!goalObject) throw new Error(`${input.errorPrefix} require --gjc-goal-json with a goal object`);
655
+ const updatedAt = typeof goalObject.updatedAt === "number" ? goalObject.updatedAt : null;
656
+ if (!updatedAt) throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.updatedAt from get_goal`);
657
+ const nowMilliseconds = Date.now();
658
+ if (updatedAt < nowMilliseconds - GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS) {
659
+ throw new Error(`${input.errorPrefix} require a fresh --gjc-goal-json snapshot`);
660
+ }
661
+ if (updatedAt > nowMilliseconds + GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS) {
662
+ throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.updatedAt that is not from the future`);
663
+ }
664
+ const objective = typeof goalObject.objective === "string" ? goalObject.objective : "";
665
+ const expectedObjectives = new Set([input.plan.gjcObjective, ...(input.plan.gjcObjectiveAliases ?? [])]);
666
+ if (input.plan.gjcGoalMode === "per-story" && input.goal?.objective) {
667
+ expectedObjectives.add(input.goal.objective);
668
+ }
669
+ if (input.allowCompletedLegacyBlocker && goalObject.status === "complete" && !expectedObjectives.has(objective)) {
670
+ return snapshot;
671
+ }
672
+ if (!expectedObjectives.has(objective)) {
673
+ throw new Error(`${input.errorPrefix} require --gjc-goal-json objective to match the active Ultragoal objective`);
674
+ }
675
+ if (goalObject.status !== "active") {
676
+ throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.status to be active`);
677
+ }
678
+ return snapshot;
580
679
  }
581
680
 
582
681
  export async function checkpointUltragoalGoal(input: {
@@ -599,9 +698,6 @@ export async function checkpointUltragoalGoal(input: {
599
698
  : input.qualityGateJson
600
699
  ? await readStructuredValue(input.cwd, input.qualityGateJson)
601
700
  : 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
- }
605
701
  const now = new Date().toISOString();
606
702
  const ledgerBefore = await readUltragoalLedger(input.cwd);
607
703
  const beforeStatus = goal.status;
@@ -618,6 +714,25 @@ export async function checkpointUltragoalGoal(input: {
618
714
  }
619
715
  }
620
716
  const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, goal, input.status) : null;
717
+ const gjcGoalJson =
718
+ input.status === "complete"
719
+ ? await readGjcGoalSnapshot({
720
+ cwd: input.cwd,
721
+ value: input.gjcGoalJson,
722
+ plan,
723
+ goal,
724
+ required: true,
725
+ errorPrefix: "complete checkpoints",
726
+ })
727
+ : await readGjcGoalSnapshot({
728
+ cwd: input.cwd,
729
+ value: input.gjcGoalJson,
730
+ plan,
731
+ goal,
732
+ required: false,
733
+ errorPrefix: `${input.status} checkpoints`,
734
+ allowCompletedLegacyBlocker: input.status === "blocked",
735
+ });
621
736
  const pendingCheckpointEventId = crypto.randomUUID();
622
737
  if (input.status === "complete" && receiptKind && qualityGateJson && !Array.isArray(qualityGateJson)) {
623
738
  goal.completionVerification = buildCompletionReceipt({
@@ -627,6 +742,7 @@ export async function checkpointUltragoalGoal(input: {
627
742
  receiptKind,
628
743
  beforeStatus,
629
744
  qualityGateJson: qualityGateJson as JsonObject,
745
+ gjcGoalJson: gjcGoalJson as JsonObject,
630
746
  now,
631
747
  checkpointLedgerEventId: pendingCheckpointEventId,
632
748
  });
@@ -643,8 +759,9 @@ export async function checkpointUltragoalGoal(input: {
643
759
  goalId: goal.id,
644
760
  status: input.status,
645
761
  evidence,
646
- gjcGoalJson: input.gjcGoalJson ? await readStructuredValue(input.cwd, input.gjcGoalJson) : undefined,
762
+ gjcGoalJson,
647
763
  qualityGateJson,
764
+ completionVerification: goal.completionVerification,
648
765
  });
649
766
  return plan;
650
767
  }
@@ -699,6 +816,9 @@ export async function recordUltragoalReviewBlockers(input: {
699
816
  }): Promise<UltragoalPlan> {
700
817
  const objective = input.objective.trim();
701
818
  if (!objective) throw new Error("record-review-blockers --objective is required");
819
+ if (!input.gjcGoalJson?.trim()) {
820
+ throw new Error("record-review-blockers require --gjc-goal-json from a fresh active get_goal snapshot");
821
+ }
702
822
  const plan = await checkpointUltragoalGoal({
703
823
  cwd: input.cwd,
704
824
  goalId: input.goalId,
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
4
4
  import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
5
5
  import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
6
+ import type { SkillActiveEntry as CanonicalSkillActiveEntry, WorkflowHudSummary } from "../skill-state/active-state";
6
7
  import {
7
8
  compareSkillKeywordMatches,
8
9
  GJC_SKILL_KEYWORD_DEFINITIONS,
@@ -72,7 +73,7 @@ export interface SkillKeywordMatch {
72
73
  priority: number;
73
74
  }
74
75
 
75
- export interface SkillActiveEntry {
76
+ export interface SkillActiveEntry extends Omit<CanonicalSkillActiveEntry, "skill"> {
76
77
  skill: GjcWorkflowSkill;
77
78
  phase?: string;
78
79
  active?: boolean;
@@ -81,6 +82,8 @@ export interface SkillActiveEntry {
81
82
  session_id?: string;
82
83
  thread_id?: string;
83
84
  turn_id?: string;
85
+ hud?: WorkflowHudSummary;
86
+ stale?: boolean;
84
87
  }
85
88
 
86
89
  export interface SkillActiveState {