@gajae-code/coding-agent 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/dist/types/config/model-registry.d.ts +8 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -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 +5 -7
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -4
- package/dist/types/session/agent-session.d.ts +3 -9
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +28 -0
- package/package.json +13 -9
- package/src/config/model-registry.ts +45 -0
- package/src/config/model-resolver.ts +5 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +30 -30
- package/src/defaults/gjc/skills/team/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +51 -21
- package/src/gjc-runtime/team-runtime.ts +80 -1
- 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 +2 -17
- package/src/modes/components/model-selector.ts +225 -33
- package/src/modes/controllers/selector-controller.ts +16 -3
- package/src/modes/interactive-mode.ts +34 -22
- package/src/modes/prompt-action-autocomplete.ts +40 -15
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +40 -4
- 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/image-gen.ts +19 -10
- package/src/tools/path-utils.ts +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
2
|
+
import {
|
|
3
|
+
computeUltragoalPlanGeneration,
|
|
4
|
+
hashStructuredValue,
|
|
5
|
+
readUltragoalLedger,
|
|
6
|
+
readUltragoalPlan,
|
|
7
|
+
type UltragoalCompletionVerification,
|
|
8
|
+
type UltragoalGoal,
|
|
9
|
+
type UltragoalLedgerEvent,
|
|
10
|
+
type UltragoalPlan,
|
|
11
|
+
type UltragoalReceiptKind,
|
|
12
|
+
} from "./ultragoal-runtime";
|
|
13
|
+
|
|
14
|
+
export type UltragoalGuardState =
|
|
15
|
+
| "inactive"
|
|
16
|
+
| "unrelated_goal"
|
|
17
|
+
| "active_verified_complete"
|
|
18
|
+
| "active_missing_receipt"
|
|
19
|
+
| "active_stale_receipt"
|
|
20
|
+
| "active_missing_final_receipt"
|
|
21
|
+
| "active_dirty_quality_gate"
|
|
22
|
+
| "active_review_blocked_unrecorded"
|
|
23
|
+
| "active_review_blocked_recorded"
|
|
24
|
+
| "unreadable_fail_closed";
|
|
25
|
+
|
|
26
|
+
export interface UltragoalGuardDiagnostic {
|
|
27
|
+
state: UltragoalGuardState;
|
|
28
|
+
message: string;
|
|
29
|
+
goalId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CurrentGoalLike {
|
|
33
|
+
objective: string;
|
|
34
|
+
status?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function objectiveMatches(currentObjective: string, plan: UltragoalPlan): boolean {
|
|
38
|
+
const normalized = currentObjective.trim();
|
|
39
|
+
if (!normalized) return false;
|
|
40
|
+
if (normalized === plan.gjcObjective || normalized === DEFAULT_ULTRAGOAL_OBJECTIVE) return true;
|
|
41
|
+
if (plan.gjcObjectiveAliases?.some(alias => alias === normalized)) return true;
|
|
42
|
+
return plan.goals.some(goal => goal.objective === normalized);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requiredGoals(plan: UltragoalPlan): UltragoalGoal[] {
|
|
46
|
+
return plan.goals.filter(goal => goal.status !== "superseded");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findReceiptGoal(
|
|
50
|
+
plan: UltragoalPlan,
|
|
51
|
+
currentObjective: string,
|
|
52
|
+
): { goal: UltragoalGoal; receiptKind: UltragoalReceiptKind } | null {
|
|
53
|
+
if (
|
|
54
|
+
currentObjective === plan.gjcObjective ||
|
|
55
|
+
currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE ||
|
|
56
|
+
plan.gjcObjectiveAliases?.some(alias => alias === currentObjective)
|
|
57
|
+
) {
|
|
58
|
+
const finalGoal = [...requiredGoals(plan)]
|
|
59
|
+
.reverse()
|
|
60
|
+
.find(goal => goal.completionVerification?.receiptKind === "final-aggregate");
|
|
61
|
+
return finalGoal ? { goal: finalGoal, receiptKind: "final-aggregate" } : null;
|
|
62
|
+
}
|
|
63
|
+
const storyGoal = plan.goals.find(goal => goal.objective === currentObjective);
|
|
64
|
+
return storyGoal ? { goal: storyGoal, receiptKind: "per-goal" } : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findLedgerReceiptEvent(
|
|
68
|
+
ledger: readonly UltragoalLedgerEvent[],
|
|
69
|
+
receipt: UltragoalCompletionVerification,
|
|
70
|
+
): UltragoalLedgerEvent | null {
|
|
71
|
+
return (
|
|
72
|
+
ledger.find(event => {
|
|
73
|
+
if (event.eventId !== receipt.checkpointLedgerEventId) return false;
|
|
74
|
+
if (event.event !== "goal_checkpointed") return false;
|
|
75
|
+
if (event.goalId !== receipt.goalId) return false;
|
|
76
|
+
const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
|
|
77
|
+
return (
|
|
78
|
+
event.status === "complete" &&
|
|
79
|
+
eventReceipt?.receiptId === receipt.receiptId &&
|
|
80
|
+
eventReceipt.receiptKind === receipt.receiptKind &&
|
|
81
|
+
eventReceipt.planGeneration === receipt.planGeneration
|
|
82
|
+
);
|
|
83
|
+
}) ?? null
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateCompletionReceipt(input: {
|
|
88
|
+
plan: UltragoalPlan;
|
|
89
|
+
ledger: readonly UltragoalLedgerEvent[];
|
|
90
|
+
goal: UltragoalGoal;
|
|
91
|
+
receiptKind: UltragoalReceiptKind;
|
|
92
|
+
}): UltragoalGuardDiagnostic {
|
|
93
|
+
const receipt = input.goal.completionVerification;
|
|
94
|
+
if (!receipt) {
|
|
95
|
+
return {
|
|
96
|
+
state: input.receiptKind === "final-aggregate" ? "active_missing_final_receipt" : "active_missing_receipt",
|
|
97
|
+
message: `Ultragoal ${input.goal.id} has no ${input.receiptKind} completion verification receipt.`,
|
|
98
|
+
goalId: input.goal.id,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
receipt.schemaVersion !== 1 ||
|
|
103
|
+
receipt.goalId !== input.goal.id ||
|
|
104
|
+
receipt.receiptKind !== input.receiptKind ||
|
|
105
|
+
!receipt.planGeneration ||
|
|
106
|
+
!receipt.checkpointLedgerEventId
|
|
107
|
+
) {
|
|
108
|
+
return {
|
|
109
|
+
state: "active_stale_receipt",
|
|
110
|
+
message: `Ultragoal ${input.goal.id} receipt is malformed or stale.`,
|
|
111
|
+
goalId: input.goal.id,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const event = findLedgerReceiptEvent(input.ledger, receipt);
|
|
115
|
+
if (!event) {
|
|
116
|
+
return {
|
|
117
|
+
state: "active_stale_receipt",
|
|
118
|
+
message: `Ultragoal ${input.goal.id} receipt ledger event is missing.`,
|
|
119
|
+
goalId: input.goal.id,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const generation = computeUltragoalPlanGeneration({
|
|
123
|
+
plan: input.plan,
|
|
124
|
+
ledger: input.ledger,
|
|
125
|
+
goal: input.goal,
|
|
126
|
+
receiptKind: input.receiptKind,
|
|
127
|
+
beforeStatus: receipt.goalStatusBeforeCheckpoint,
|
|
128
|
+
excludeEventId: receipt.checkpointLedgerEventId,
|
|
129
|
+
});
|
|
130
|
+
if (generation.planGeneration !== receipt.planGeneration) {
|
|
131
|
+
return {
|
|
132
|
+
state: "active_stale_receipt",
|
|
133
|
+
message: `Ultragoal ${input.goal.id} receipt generation is stale.`,
|
|
134
|
+
goalId: input.goal.id,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (hashStructuredValue(event.qualityGateJson) !== receipt.qualityGateHash) {
|
|
138
|
+
return {
|
|
139
|
+
state: "active_dirty_quality_gate",
|
|
140
|
+
message: `Ultragoal ${input.goal.id} receipt quality-gate hash does not match ledger.`,
|
|
141
|
+
goalId: input.goal.id,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (input.goal.updatedAt !== receipt.verifiedAt) {
|
|
145
|
+
return {
|
|
146
|
+
state: "active_stale_receipt",
|
|
147
|
+
message: `Ultragoal ${input.goal.id} receipt target changed after verification.`,
|
|
148
|
+
goalId: input.goal.id,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (input.receiptKind === "final-aggregate") {
|
|
152
|
+
const incomplete = requiredGoals(input.plan).filter(goal => goal.status !== "complete");
|
|
153
|
+
if (incomplete.length > 0) {
|
|
154
|
+
return {
|
|
155
|
+
state: "active_missing_final_receipt",
|
|
156
|
+
message: `Ultragoal final receipt is not valid while required goals remain incomplete: ${incomplete.map(goal => goal.id).join(", ")}.`,
|
|
157
|
+
goalId: input.goal.id,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const missingReceipts = requiredGoals(input.plan).filter(
|
|
161
|
+
goal => goal.id !== input.goal.id && !goal.completionVerification,
|
|
162
|
+
);
|
|
163
|
+
if (missingReceipts.length > 0) {
|
|
164
|
+
return {
|
|
165
|
+
state: "active_missing_receipt",
|
|
166
|
+
message: `Ultragoal final receipt is missing per-goal evidence for: ${missingReceipts.map(goal => goal.id).join(", ")}.`,
|
|
167
|
+
goalId: input.goal.id,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
state: "active_verified_complete",
|
|
173
|
+
message: `Ultragoal ${input.goal.id} has a fresh ${input.receiptKind} receipt.`,
|
|
174
|
+
goalId: input.goal.id,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function readUltragoalVerificationState(input: {
|
|
179
|
+
cwd: string;
|
|
180
|
+
currentGoal?: CurrentGoalLike | null;
|
|
181
|
+
}): Promise<UltragoalGuardDiagnostic> {
|
|
182
|
+
const currentObjective = input.currentGoal?.objective?.trim() ?? "";
|
|
183
|
+
if (!currentObjective) return { state: "inactive", message: "No current goal objective is active." };
|
|
184
|
+
let plan: UltragoalPlan | null;
|
|
185
|
+
let ledger: UltragoalLedgerEvent[];
|
|
186
|
+
try {
|
|
187
|
+
plan = await readUltragoalPlan(input.cwd);
|
|
188
|
+
ledger = await readUltragoalLedger(input.cwd);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE) {
|
|
191
|
+
return {
|
|
192
|
+
state: "unreadable_fail_closed",
|
|
193
|
+
message: `Unable to read Ultragoal verification state: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { state: "unrelated_goal", message: "Current goal is not an active Ultragoal objective." };
|
|
197
|
+
}
|
|
198
|
+
if (!plan) return { state: "inactive", message: "No Ultragoal plan exists." };
|
|
199
|
+
if (!objectiveMatches(currentObjective, plan))
|
|
200
|
+
return { state: "unrelated_goal", message: "Current goal is not an active Ultragoal objective." };
|
|
201
|
+
if (plan.goals.some(goal => goal.status === "review_blocked")) {
|
|
202
|
+
return {
|
|
203
|
+
state: "active_review_blocked_recorded",
|
|
204
|
+
message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (plan.goals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
|
|
208
|
+
return {
|
|
209
|
+
state: "active_dirty_quality_gate",
|
|
210
|
+
message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const receiptTarget = findReceiptGoal(plan, currentObjective);
|
|
214
|
+
if (!receiptTarget) {
|
|
215
|
+
return {
|
|
216
|
+
state: "active_missing_final_receipt",
|
|
217
|
+
message: "Ultragoal aggregate completion requires a fresh final aggregate receipt.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return validateCompletionReceipt({ plan, ledger, goal: receiptTarget.goal, receiptKind: receiptTarget.receiptKind });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function assertCanCompleteCurrentGoal(input: {
|
|
224
|
+
cwd: string;
|
|
225
|
+
currentGoal?: CurrentGoalLike | null;
|
|
226
|
+
}): Promise<void> {
|
|
227
|
+
if (!input.cwd) return;
|
|
228
|
+
const diagnostic = await readUltragoalVerificationState(input);
|
|
229
|
+
if (["inactive", "unrelated_goal", "active_verified_complete"].includes(diagnostic.state)) return;
|
|
230
|
+
throw new Error(
|
|
231
|
+
`${diagnostic.message} Run strict \`gjc ultragoal checkpoint --status complete --quality-gate-json <file> --gjc-goal-json <file>\` first, or record review blockers and rerun verification.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function isUltragoalBypassPrompt(prompt: string): boolean {
|
|
236
|
+
return /update_goal\s*\(|goal\s+complete|checkpoint[^\n]+--status\s+complete|skip\s+verification|weaken\s+verification|mark\s+.*complete/i.test(
|
|
237
|
+
prompt,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
@@ -23,6 +24,7 @@ export interface UltragoalGoal {
|
|
|
23
24
|
completedAt?: string;
|
|
24
25
|
evidence?: string;
|
|
25
26
|
steering?: Record<string, unknown>;
|
|
27
|
+
completionVerification?: UltragoalCompletionVerification;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface UltragoalPlan {
|
|
@@ -36,6 +38,36 @@ export interface UltragoalPlan {
|
|
|
36
38
|
updatedAt: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
export type UltragoalReceiptKind = "per-goal" | "final-aggregate";
|
|
42
|
+
|
|
43
|
+
export interface UltragoalCompletionVerification {
|
|
44
|
+
schemaVersion: 1;
|
|
45
|
+
receiptId: string;
|
|
46
|
+
verifiedAt: string;
|
|
47
|
+
goalId: string;
|
|
48
|
+
receiptKind: UltragoalReceiptKind;
|
|
49
|
+
goalStatusBeforeCheckpoint: UltragoalGoalStatus;
|
|
50
|
+
gjcGoalMode: UltragoalGjcGoalMode;
|
|
51
|
+
gjcObjective: string;
|
|
52
|
+
qualityGateHash: string;
|
|
53
|
+
planGeneration: string;
|
|
54
|
+
basis: {
|
|
55
|
+
planHashBeforeCheckpoint: string;
|
|
56
|
+
latestRelevantLedgerEventIdBeforeCheckpoint: string | null;
|
|
57
|
+
goalUpdatedAtBeforeCheckpoint: string;
|
|
58
|
+
relevantGoalIdsBeforeCheckpoint: string[];
|
|
59
|
+
requiredGoalSetHashBeforeCheckpoint: string;
|
|
60
|
+
};
|
|
61
|
+
checkpointLedgerEventId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UltragoalLedgerEvent extends JsonObject {
|
|
65
|
+
eventId?: string;
|
|
66
|
+
event?: string;
|
|
67
|
+
goalId?: string;
|
|
68
|
+
timestamp?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
export interface UltragoalPaths {
|
|
40
72
|
dir: string;
|
|
41
73
|
briefPath: string;
|
|
@@ -65,8 +97,33 @@ interface JsonObject {
|
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "superseded"]);
|
|
100
|
+
const CLEAN_ARCHITECT_STATUS = "CLEAR";
|
|
101
|
+
const APPROVE_RECOMMENDATION = "APPROVE";
|
|
102
|
+
const PASSED_STATUS = "passed";
|
|
103
|
+
|
|
68
104
|
const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
|
|
69
105
|
|
|
106
|
+
function stableStructuredValue(value: unknown): unknown {
|
|
107
|
+
if (Array.isArray(value)) return value.map(item => stableStructuredValue(item));
|
|
108
|
+
if (value && typeof value === "object") {
|
|
109
|
+
const record = value as Record<string, unknown>;
|
|
110
|
+
const output: Record<string, unknown> = {};
|
|
111
|
+
for (const key of Object.keys(record).sort()) {
|
|
112
|
+
const item = record[key];
|
|
113
|
+
if (item !== undefined) output[key] = stableStructuredValue(item);
|
|
114
|
+
}
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function hashStructuredValue(value: unknown): string {
|
|
121
|
+
return crypto
|
|
122
|
+
.createHash("sha256")
|
|
123
|
+
.update(JSON.stringify(stableStructuredValue(value)))
|
|
124
|
+
.digest("hex");
|
|
125
|
+
}
|
|
126
|
+
|
|
70
127
|
export function getUltragoalPaths(cwd: string): UltragoalPaths {
|
|
71
128
|
const dir = path.join(cwd, ".gjc", "ultragoal");
|
|
72
129
|
return {
|
|
@@ -87,11 +144,30 @@ async function ensureUltragoalDir(paths: UltragoalPaths): Promise<void> {
|
|
|
87
144
|
await fs.mkdir(paths.dir, { recursive: true });
|
|
88
145
|
}
|
|
89
146
|
|
|
90
|
-
async function appendLedger(cwd: string, event: JsonObject): Promise<
|
|
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;
|