@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.
- package/CHANGELOG.md +25 -0
- package/dist/types/cli/skills-cli.d.ts +9 -0
- package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
- package/dist/types/commands/skills.d.ts +26 -0
- package/dist/types/config/model-registry.d.ts +31 -2
- package/dist/types/config/models-config-schema.d.ts +39 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +35 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +15 -10
- package/dist/types/hooks/skill-state.d.ts +4 -1
- package/dist/types/modes/components/model-selector.d.ts +21 -1
- package/dist/types/skill-state/active-state.d.ts +19 -0
- package/dist/types/skill-state/workflow-hud.d.ts +62 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +14 -0
- package/src/cli/skills-cli.ts +88 -0
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +21 -2
- package/src/commands/gjc-runtime-bridge.ts +161 -15
- package/src/commands/ralplan.ts +21 -2
- package/src/commands/skills.ts +48 -0
- package/src/commands/team.ts +54 -3
- package/src/commands/ultragoal.ts +21 -1
- package/src/commit/agentic/index.ts +1 -0
- package/src/commit/pipeline.ts +1 -0
- package/src/config/model-registry.ts +259 -8
- package/src/config/models-config-schema.ts +18 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +6 -6
- package/src/defaults/gjc/skills/ralplan/SKILL.md +5 -9
- package/src/defaults/gjc/skills/team/SKILL.md +4 -4
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -8
- package/src/gjc-runtime/launch-tmux.ts +73 -2
- package/src/gjc-runtime/team-runtime.ts +285 -34
- package/src/gjc-runtime/ultragoal-guard.ts +43 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +307 -187
- package/src/hooks/skill-state.ts +4 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +10 -1
- package/src/modes/components/model-selector.ts +109 -28
- package/src/modes/components/skill-hud/render.ts +35 -8
- package/src/modes/controllers/selector-controller.ts +42 -2
- package/src/prompts/system/system-prompt.md +5 -4
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +6 -0
- package/src/setup/provider-onboarding.ts +2 -0
- package/src/skill-state/active-state.ts +104 -4
- package/src/skill-state/workflow-hud.ts +160 -0
- package/src/slash-commands/acp-builtins.ts +11 -2
- 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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
|
500
|
-
return
|
|
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
|
|
504
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"
|
|
575
|
-
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
if (iteration
|
|
579
|
-
|
|
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
|
|
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,
|
package/src/hooks/skill-state.ts
CHANGED
|
@@ -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 {
|