@gajae-code/coding-agent 0.1.1 → 0.1.2
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 +4 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +26 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +44 -0
- package/dist/types/goals/tools/goal-tool.d.ts +4 -4
- package/dist/types/hooks/skill-state.d.ts +3 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -4
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +28 -0
- package/package.json +11 -7
- package/src/config/model-registry.ts +41 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +30 -30
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +51 -21
- package/src/gjc-runtime/ultragoal-guard.ts +239 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +318 -4
- package/src/goals/tools/goal-tool.ts +10 -4
- package/src/hooks/native-skill-hook.ts +26 -0
- package/src/hooks/skill-state.ts +59 -0
- package/src/main.ts +1 -17
- package/src/modes/components/model-selector.ts +120 -28
- package/src/modes/controllers/selector-controller.ts +16 -3
- package/src/modes/prompt-action-autocomplete.ts +40 -15
- package/src/session/agent-session.ts +31 -1
- package/src/setup/model-onboarding-guidance.ts +5 -3
- package/src/skill-state/deep-interview-mutation-guard.ts +303 -0
- package/src/slash-commands/builtin-registry.ts +130 -11
- package/src/tools/ask.ts +55 -17
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/bash.ts +2 -1
- package/src/tools/gh.ts +37 -9
- package/src/tools/path-utils.ts +1 -0
|
@@ -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<
|
|
147
|
+
async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLedgerEvent> {
|
|
91
148
|
const paths = getUltragoalPaths(cwd);
|
|
92
149
|
await ensureUltragoalDir(paths);
|
|
93
|
-
const entry = {
|
|
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
|
|
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
|
|
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;
|
|
@@ -152,6 +152,16 @@ function readTurnId(payload: HookPayload): string | undefined {
|
|
|
152
152
|
return safeString(payload.turn_id ?? payload.turnId).trim() || undefined;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function readSessionFile(payload: HookPayload): string | undefined {
|
|
156
|
+
return (
|
|
157
|
+
safeString(
|
|
158
|
+
payload.session_file ?? payload.sessionFile ?? payload.transcript_path ?? payload.transcriptPath,
|
|
159
|
+
).trim() ||
|
|
160
|
+
process.env.GJC_SESSION_FILE?.trim() ||
|
|
161
|
+
undefined
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
155
165
|
export async function dispatchGjcNativeSkillHook(
|
|
156
166
|
payload: HookPayload,
|
|
157
167
|
options: GjcNativeHookDispatchOptions = {},
|
|
@@ -180,7 +190,22 @@ export async function dispatchGjcNativeSkillHook(
|
|
|
180
190
|
sessionId: readSessionId(payload),
|
|
181
191
|
threadId: readThreadId(payload),
|
|
182
192
|
stateDir: options.stateDir,
|
|
193
|
+
prompt,
|
|
194
|
+
sessionFile: readSessionFile(payload),
|
|
183
195
|
});
|
|
196
|
+
if (activeUltragoalContext?.startsWith("BLOCK_ULTRAGOAL_COMPLETION:")) {
|
|
197
|
+
return {
|
|
198
|
+
hookEventName,
|
|
199
|
+
outputJson: {
|
|
200
|
+
decision: "block",
|
|
201
|
+
reason: activeUltragoalContext,
|
|
202
|
+
hookSpecificOutput: {
|
|
203
|
+
hookEventName,
|
|
204
|
+
additionalContext: activeUltragoalContext,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
184
209
|
return {
|
|
185
210
|
hookEventName,
|
|
186
211
|
outputJson:
|
|
@@ -205,6 +230,7 @@ export async function dispatchGjcNativeSkillHook(
|
|
|
205
230
|
sessionId: readSessionId(payload),
|
|
206
231
|
threadId: readThreadId(payload),
|
|
207
232
|
stateDir: options.stateDir,
|
|
233
|
+
sessionFile: readSessionFile(payload),
|
|
208
234
|
}),
|
|
209
235
|
};
|
|
210
236
|
}
|
package/src/hooks/skill-state.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
|
|
4
|
+
import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
|
|
5
|
+
import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
|
|
4
6
|
import {
|
|
5
7
|
compareSkillKeywordMatches,
|
|
6
8
|
GJC_SKILL_KEYWORD_DEFINITIONS,
|
|
@@ -124,6 +126,7 @@ export interface StopHookInput {
|
|
|
124
126
|
sessionId?: string;
|
|
125
127
|
threadId?: string;
|
|
126
128
|
stateDir?: string;
|
|
129
|
+
sessionFile?: string;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
export interface UserPromptSubmitStateInput {
|
|
@@ -131,6 +134,8 @@ export interface UserPromptSubmitStateInput {
|
|
|
131
134
|
sessionId?: string;
|
|
132
135
|
threadId?: string;
|
|
133
136
|
stateDir?: string;
|
|
137
|
+
prompt?: string;
|
|
138
|
+
sessionFile?: string;
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
function escapeRegex(value: string): string {
|
|
@@ -361,6 +366,19 @@ function stateMatchesContext(state: ModeState, sessionId?: string, threadId?: st
|
|
|
361
366
|
return true;
|
|
362
367
|
}
|
|
363
368
|
|
|
369
|
+
async function readCurrentGoalObjectiveFromSessionFile(sessionFile: string | undefined): Promise<string | null> {
|
|
370
|
+
const trimmed = sessionFile?.trim();
|
|
371
|
+
if (!trimmed) return null;
|
|
372
|
+
const entries = (await loadEntriesFromFile(trimmed)).filter(
|
|
373
|
+
(entry): entry is SessionEntry => entry.type !== "session",
|
|
374
|
+
);
|
|
375
|
+
const context = buildSessionContext(entries);
|
|
376
|
+
const goal = context.modeData?.goal;
|
|
377
|
+
if (typeof goal !== "object" || goal === null) return null;
|
|
378
|
+
const objective = (goal as { objective?: unknown }).objective;
|
|
379
|
+
return typeof objective === "string" && objective.trim().length > 0 ? objective.trim() : null;
|
|
380
|
+
}
|
|
381
|
+
|
|
364
382
|
export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitStateInput): Promise<string | null> {
|
|
365
383
|
const visibleModeState = await readVisibleModeState(input.cwd, "ultragoal", input.sessionId, input.stateDir);
|
|
366
384
|
if (!visibleModeState) return null;
|
|
@@ -368,6 +386,22 @@ export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitS
|
|
|
368
386
|
if (!stateMatchesContext(visibleModeState.state, input.sessionId, input.threadId)) return null;
|
|
369
387
|
|
|
370
388
|
const phase = String(visibleModeState.state.current_phase ?? "active");
|
|
389
|
+
const objective =
|
|
390
|
+
(await readCurrentGoalObjectiveFromSessionFile(input.sessionFile)) ??
|
|
391
|
+
(typeof visibleModeState.state.objective === "string"
|
|
392
|
+
? visibleModeState.state.objective
|
|
393
|
+
: typeof visibleModeState.state.gjcObjective === "string"
|
|
394
|
+
? visibleModeState.state.gjcObjective
|
|
395
|
+
: "");
|
|
396
|
+
if (input.prompt && isUltragoalBypassPrompt(input.prompt) && objective) {
|
|
397
|
+
const diagnostic = await readUltragoalVerificationState({
|
|
398
|
+
cwd: input.cwd,
|
|
399
|
+
currentGoal: { objective },
|
|
400
|
+
});
|
|
401
|
+
if (!["inactive", "unrelated_goal", "active_verified_complete"].includes(diagnostic.state)) {
|
|
402
|
+
return `BLOCK_ULTRAGOAL_COMPLETION: ${diagnostic.message} Use durable blocker work or run strict \`gjc ultragoal checkpoint --status complete --quality-gate-json <file> --gjc-goal-json <file>\` before completion.`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
371
405
|
return `Ultragoal is active (phase: ${phase}; state: ${visibleModeState.statePath}). If the user prompt is a steering request, use \`gjc ultragoal steer\` to add or steer subgoals. Normal prose should not mutate Ultragoal state.`;
|
|
372
406
|
}
|
|
373
407
|
|
|
@@ -384,6 +418,31 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
|
|
|
384
418
|
if (isTerminalModeState(modeState)) continue;
|
|
385
419
|
const phase = String(modeState?.current_phase ?? entry.phase ?? skillState.phase ?? "active");
|
|
386
420
|
const statePath = modeStatePath(resolvedStateDir, entry.skill, input.sessionId);
|
|
421
|
+
if (entry.skill === "ultragoal") {
|
|
422
|
+
const objective =
|
|
423
|
+
(await readCurrentGoalObjectiveFromSessionFile(input.sessionFile)) ??
|
|
424
|
+
(typeof modeState?.objective === "string"
|
|
425
|
+
? modeState.objective
|
|
426
|
+
: typeof modeState?.gjcObjective === "string"
|
|
427
|
+
? modeState.gjcObjective
|
|
428
|
+
: "");
|
|
429
|
+
if (objective) {
|
|
430
|
+
const diagnostic = await readUltragoalVerificationState({
|
|
431
|
+
cwd: input.cwd,
|
|
432
|
+
currentGoal: { objective },
|
|
433
|
+
});
|
|
434
|
+
if (diagnostic.state === "active_verified_complete") continue;
|
|
435
|
+
if (!["inactive", "unrelated_goal"].includes(diagnostic.state)) {
|
|
436
|
+
const ultragoalMessage = `GJC ultragoal verification is blocking stop: ${diagnostic.message} Run strict checkpoint verification or record review blockers before stopping.`;
|
|
437
|
+
return {
|
|
438
|
+
decision: "block",
|
|
439
|
+
reason: ultragoalMessage,
|
|
440
|
+
stopReason: `gjc_ultragoal_verification_${diagnostic.state}`,
|
|
441
|
+
systemMessage: ultragoalMessage,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
387
446
|
const systemMessage = `GJC skill "${entry.skill}" is still active (phase: ${phase}; state: ${statePath}). Continue or explicitly finish/cancel the skill before stopping.`;
|
|
388
447
|
return {
|
|
389
448
|
decision: "block",
|
package/src/main.ts
CHANGED
|
@@ -480,21 +480,6 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
/** Discover SYSTEM.md file if no CLI system prompt was provided */
|
|
484
|
-
function discoverSystemPromptFile(): string | undefined {
|
|
485
|
-
// Check project-local first (.gjc/SYSTEM.md, .pi/SYSTEM.md legacy)
|
|
486
|
-
const projectPath = findConfigFile("SYSTEM.md", { user: false });
|
|
487
|
-
if (projectPath) {
|
|
488
|
-
return projectPath;
|
|
489
|
-
}
|
|
490
|
-
// If not found, check SYSTEM.md file in the global directory.
|
|
491
|
-
const globalPath = findConfigFile("SYSTEM.md", { user: true });
|
|
492
|
-
if (globalPath) {
|
|
493
|
-
return globalPath;
|
|
494
|
-
}
|
|
495
|
-
return undefined;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
483
|
/** Discover APPEND_SYSTEM.md file if no CLI append system prompt was provided */
|
|
499
484
|
function discoverAppendSystemPromptFile(): string | undefined {
|
|
500
485
|
const projectPath = findConfigFile("APPEND_SYSTEM.md", { user: false });
|
|
@@ -519,8 +504,7 @@ async function buildSessionOptions(
|
|
|
519
504
|
cwd: parsed.cwd ?? getProjectDir(),
|
|
520
505
|
};
|
|
521
506
|
|
|
522
|
-
|
|
523
|
-
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
|
507
|
+
const systemPromptSource = parsed.systemPrompt;
|
|
524
508
|
const resolvedSystemPrompt = await resolvePromptInput(systemPromptSource, "system prompt");
|
|
525
509
|
const appendPromptSource = parsed.appendSystemPrompt ?? discoverAppendSystemPromptFile();
|
|
526
510
|
const resolvedAppendPrompt = await resolvePromptInput(appendPromptSource, "append system prompt");
|