@gajae-code/coding-agent 0.4.1 → 0.4.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 +11 -0
- package/dist/types/commands/ultragoal.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +11 -3
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/finalize.d.ts +8 -0
- package/dist/types/harness-control-plane/receipts.d.ts +16 -1
- package/dist/types/harness-control-plane/types.d.ts +9 -1
- package/dist/types/reminders/star-reminder.d.ts +115 -0
- package/dist/types/session/agent-session.d.ts +18 -0
- package/examples/extensions/README.md +20 -41
- package/package.json +7 -7
- package/src/cli/grep-cli.ts +1 -1
- package/src/commands/harness.ts +42 -3
- package/src/commands/ultragoal.ts +1 -0
- package/src/config/settings-schema.ts +13 -3
- package/src/defaults/gjc/skills/team/SKILL.md +10 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +3 -2
- package/src/gjc-runtime/launch-tmux.ts +25 -2
- package/src/gjc-runtime/team-runtime.ts +78 -3
- package/src/gjc-runtime/ultragoal-guard.ts +18 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +240 -30
- package/src/harness-control-plane/finalize.ts +84 -0
- package/src/harness-control-plane/owner.ts +13 -0
- package/src/harness-control-plane/receipts.ts +39 -1
- package/src/harness-control-plane/types.ts +25 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/interactive-mode.ts +26 -0
- package/src/reminders/star-reminder.ts +422 -0
- package/src/session/agent-session.ts +79 -13
|
@@ -96,11 +96,12 @@ Loop until `gjc ultragoal status` reports all goals complete:
|
|
|
96
96
|
7. Before any `--status complete` checkpoint, run the mandatory final cleanup/review gate below. In aggregate mode, do **not** call `goal({"op":"complete"})` for intermediate stories; checkpoint each story with a fresh `goal({"op":"get"})` snapshot whose aggregate objective is still `active`. On the final story, use the same fresh active snapshot to create the final aggregate receipt first; only after that receipt exists may `goal({"op":"complete"})` run.
|
|
97
97
|
8. Checkpoint the durable ledger with that fresh active snapshot. Complete checkpoints require `--quality-gate-json`; the runtime hook rejects closure without a clean architect review:
|
|
98
98
|
`gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --gjc-goal-json <goal-get-json-or-path> --quality-gate-json <quality-gate-json-or-path>`
|
|
99
|
+
A successful complete checkpoint is story completion, not automatic run completion. Read the checkpoint output: when it prints `Next ultragoal goal: <id>`, continue that active story under the same aggregate GJC goal; when it prints `All ultragoal goals are complete`, the durable run is terminal. `gjc ultragoal complete-goals` remains the supported manual next-story command if continuation output was missed.
|
|
99
100
|
9. If blocked or failed, checkpoint failure:
|
|
100
101
|
`gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"`
|
|
101
|
-
|
|
102
|
+
10. For legacy per-story completed-goal blockers, preserve the non-terminal blocker with:
|
|
102
103
|
`gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks goal create in this thread>" --gjc-goal-json <goal-get-json-or-path>`
|
|
103
|
-
|
|
104
|
+
11. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
|
|
104
105
|
|
|
105
106
|
## Dynamic steering
|
|
106
107
|
|
|
@@ -48,6 +48,7 @@ export interface TmuxLaunchContext {
|
|
|
48
48
|
currentBranch?: string | null;
|
|
49
49
|
existingBranchSessionName?: string | null;
|
|
50
50
|
project?: string | null;
|
|
51
|
+
diagnosticWriter?: (message: string) => void;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export interface TmuxSpawnResult {
|
|
@@ -120,6 +121,16 @@ function isInteractiveRootLaunch(parsed: Args, tty: TtyState): boolean {
|
|
|
120
121
|
);
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
function isBunVirtualPath(value: string | undefined): boolean {
|
|
125
|
+
return value?.startsWith("/$bunfs/") === true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatTmuxLaunchDiagnostic(stage: string, stderr?: string): string {
|
|
129
|
+
const detail = stderr?.trim();
|
|
130
|
+
const suffix = detail ? ` ${detail.slice(0, 240)}` : "";
|
|
131
|
+
return `gjc --tmux failed after creating tmux session: ${stage}.${suffix}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
function shellQuote(value: string): string {
|
|
124
135
|
if (value.length === 0) return "''";
|
|
125
136
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -148,6 +159,9 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
148
159
|
function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
149
160
|
const entrypoint = context.argv[1];
|
|
150
161
|
if (!entrypoint) return ["gjc"];
|
|
162
|
+
if (isBunVirtualPath(entrypoint)) {
|
|
163
|
+
return isBunVirtualPath(context.execPath) ? ["gjc"] : [context.execPath];
|
|
164
|
+
}
|
|
151
165
|
const resolvedEntrypoint = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(context.cwd, entrypoint);
|
|
152
166
|
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
|
|
153
167
|
return [context.execPath, resolvedEntrypoint];
|
|
@@ -264,10 +278,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
264
278
|
});
|
|
265
279
|
if (profile.failures.length > 0) {
|
|
266
280
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
267
|
-
|
|
281
|
+
const failure =
|
|
282
|
+
profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
|
|
283
|
+
(context.diagnosticWriter ?? process.stderr.write.bind(process.stderr))(
|
|
284
|
+
formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
|
|
285
|
+
);
|
|
286
|
+
return true;
|
|
268
287
|
}
|
|
269
288
|
}
|
|
270
289
|
if (created.exitCode !== 0) return false;
|
|
271
290
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
272
|
-
|
|
291
|
+
if (attached.exitCode === 0) return true;
|
|
292
|
+
(context.diagnosticWriter ?? process.stderr.write.bind(process.stderr))(
|
|
293
|
+
formatTmuxLaunchDiagnostic("attach failed", attached.stderr),
|
|
294
|
+
);
|
|
295
|
+
return true;
|
|
273
296
|
}
|
|
@@ -1673,9 +1673,10 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
1673
1673
|
`You are ${worker.id} in gjc team ${config.team_name}.`,
|
|
1674
1674
|
`Team state root: ${config.state_root}.`,
|
|
1675
1675
|
workspace,
|
|
1676
|
-
`
|
|
1676
|
+
`Team brief (context only): ${config.task}`,
|
|
1677
|
+
"Before implementation, claim your worker-owned task and treat the claimed task record as the source of truth. Do not implement directly from the broad team brief.",
|
|
1677
1678
|
`Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
|
|
1678
|
-
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
1679
|
+
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
1679
1680
|
].join("\n");
|
|
1680
1681
|
const env = [
|
|
1681
1682
|
`GJC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
|
|
@@ -1689,7 +1690,80 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
1689
1690
|
];
|
|
1690
1691
|
return `${env.join(" ")} ${config.worker_command} ${shellQuote(prompt)}`;
|
|
1691
1692
|
}
|
|
1693
|
+
interface GjcTeamInitialLane {
|
|
1694
|
+
label: string;
|
|
1695
|
+
title: string;
|
|
1696
|
+
body: string;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function normalizeLaneId(label: string): string {
|
|
1700
|
+
return `lane-${sanitizeName(label).toLowerCase() || stableHash(label).slice(0, 8)}`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function parseExplicitTeamLanes(task: string): GjcTeamInitialLane[] {
|
|
1704
|
+
const lines = task.split(/\r?\n/);
|
|
1705
|
+
const lanes: GjcTeamInitialLane[] = [];
|
|
1706
|
+
let current: { label: string; title: string; body: string[] } | null = null;
|
|
1707
|
+
const laneHeading = /^#{2,6}\s+Lane\s+([A-Za-z0-9]+)\s*(?:[—–-]\s*(.+))?\s*$/;
|
|
1708
|
+
const boundaryHeading = /^#{1,6}\s+(?:Integration Owner|Verification Plan|ADR|Approval State)\b/i;
|
|
1709
|
+
|
|
1710
|
+
for (const line of lines) {
|
|
1711
|
+
const match = line.match(laneHeading);
|
|
1712
|
+
if (match) {
|
|
1713
|
+
if (current) lanes.push({ ...current, body: current.body.join("\n").trim() });
|
|
1714
|
+
current = {
|
|
1715
|
+
label: match[1] ?? `${lanes.length + 1}`,
|
|
1716
|
+
title: (match[2] ?? `Lane ${match[1] ?? lanes.length + 1}`).trim(),
|
|
1717
|
+
body: [],
|
|
1718
|
+
};
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (current && boundaryHeading.test(line)) {
|
|
1722
|
+
lanes.push({ ...current, body: current.body.join("\n").trim() });
|
|
1723
|
+
current = null;
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (current) current.body.push(line);
|
|
1727
|
+
}
|
|
1728
|
+
if (current) lanes.push({ ...current, body: current.body.join("\n").trim() });
|
|
1729
|
+
return lanes.filter(lane => lane.body.length > 0 || lane.title.length > 0);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function hasAmbiguousLaneSplitIntent(task: string): boolean {
|
|
1733
|
+
return (
|
|
1734
|
+
/\bsplit\s+lanes?\s*:/i.test(task) || /\blanes?\s*:\s*[A-Z]\b/i.test(task) || /\bLane\s+[A-Z]\s*[—–-]/.test(task)
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1692
1738
|
function buildInitialTasks(task: string, workers: GjcTeamWorker[]): GjcTeamTask[] {
|
|
1739
|
+
const lanes = parseExplicitTeamLanes(task);
|
|
1740
|
+
if (lanes.length > 0)
|
|
1741
|
+
return lanes.map((lane, index) => {
|
|
1742
|
+
const worker = workers[index % workers.length];
|
|
1743
|
+
if (!worker) throw new Error("team_lane_requires_worker");
|
|
1744
|
+
const laneTitle = `Lane ${lane.label} — ${lane.title}`;
|
|
1745
|
+
const objective = [`${laneTitle}`, lane.body].filter(part => part.trim().length > 0).join("\n\n");
|
|
1746
|
+
return {
|
|
1747
|
+
id: `task-${index + 1}`,
|
|
1748
|
+
subject: laneTitle,
|
|
1749
|
+
description: objective,
|
|
1750
|
+
title: laneTitle,
|
|
1751
|
+
objective,
|
|
1752
|
+
status: "pending",
|
|
1753
|
+
owner: worker.id,
|
|
1754
|
+
lane: normalizeLaneId(lane.label),
|
|
1755
|
+
required_role: worker.role,
|
|
1756
|
+
version: 1,
|
|
1757
|
+
created_at: now(),
|
|
1758
|
+
updated_at: now(),
|
|
1759
|
+
};
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
if (workers.length > 1 && hasAmbiguousLaneSplitIntent(task))
|
|
1763
|
+
throw new Error(
|
|
1764
|
+
"ambiguous_team_lane_split: multi-worker team launch mentions lanes but does not provide explicit markdown lane sections such as `### Lane A — Title`",
|
|
1765
|
+
);
|
|
1766
|
+
|
|
1693
1767
|
return workers.map(worker => ({
|
|
1694
1768
|
id: `task-${worker.index}`,
|
|
1695
1769
|
subject: `Execute team brief (${worker.id})`,
|
|
@@ -2456,6 +2530,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2456
2530
|
? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
|
|
2457
2531
|
: readCurrentTmuxLeaderContext(tmuxCommand, env);
|
|
2458
2532
|
const initialWorkers = buildWorkers(options.workerCount, options.agentType, stateRoot);
|
|
2533
|
+
const initialTasks = buildInitialTasks(options.task, initialWorkers);
|
|
2459
2534
|
const workers: GjcTeamWorker[] = [];
|
|
2460
2535
|
try {
|
|
2461
2536
|
for (const worker of initialWorkers)
|
|
@@ -2509,7 +2584,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2509
2584
|
updated_at: createdAt,
|
|
2510
2585
|
});
|
|
2511
2586
|
await writePhase(dir, "starting");
|
|
2512
|
-
for (const task of
|
|
2587
|
+
for (const task of initialTasks) await writeTask(dir, task);
|
|
2513
2588
|
await appendEvent(dir, {
|
|
2514
2589
|
type: "team_started",
|
|
2515
2590
|
message: options.dryRun
|
|
@@ -3,6 +3,7 @@ import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
|
3
3
|
import {
|
|
4
4
|
computeUltragoalPlanGeneration,
|
|
5
5
|
getUltragoalPaths,
|
|
6
|
+
getUltragoalRunCompletionState,
|
|
6
7
|
hashStructuredValue,
|
|
7
8
|
readUltragoalLedger,
|
|
8
9
|
readUltragoalPlan,
|
|
@@ -246,7 +247,8 @@ export async function readUltragoalVerificationState(input: {
|
|
|
246
247
|
message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
|
|
247
248
|
};
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
+
const runState = getUltragoalRunCompletionState(plan);
|
|
251
|
+
if (runState.incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
|
|
250
252
|
return {
|
|
251
253
|
state: "active_dirty_quality_gate",
|
|
252
254
|
message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
|
|
@@ -259,7 +261,21 @@ export async function readUltragoalVerificationState(input: {
|
|
|
259
261
|
message: "Ultragoal aggregate completion requires a fresh final aggregate receipt.",
|
|
260
262
|
};
|
|
261
263
|
}
|
|
262
|
-
|
|
264
|
+
const receiptDiagnostic = validateCompletionReceipt({
|
|
265
|
+
plan,
|
|
266
|
+
ledger,
|
|
267
|
+
goal: receiptTarget.goal,
|
|
268
|
+
receiptKind: receiptTarget.receiptKind,
|
|
269
|
+
});
|
|
270
|
+
if (receiptDiagnostic.state !== "active_verified_complete") return receiptDiagnostic;
|
|
271
|
+
if (runState.incompleteGoals.length > 0) {
|
|
272
|
+
return {
|
|
273
|
+
state: "active_missing_final_receipt",
|
|
274
|
+
message: `Ultragoal still has incomplete required goals: ${runState.incompleteGoals.map(goal => goal.id).join(", ")}. Run \`gjc ultragoal complete-goals\` to continue.`,
|
|
275
|
+
goalId: receiptTarget.goal.id,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return receiptDiagnostic;
|
|
263
279
|
}
|
|
264
280
|
|
|
265
281
|
export async function assertCanCompleteCurrentGoal(input: {
|
|
@@ -228,18 +228,35 @@ function planSnapshotForReceipt(input: {
|
|
|
228
228
|
goal: UltragoalGoal;
|
|
229
229
|
beforeStatus: UltragoalGoalStatus;
|
|
230
230
|
targetGoalUpdatedAt: string;
|
|
231
|
+
receiptKind: UltragoalReceiptKind;
|
|
231
232
|
}): unknown {
|
|
233
|
+
const targetGoalSnapshot = {
|
|
234
|
+
...input.goal,
|
|
235
|
+
status: input.beforeStatus,
|
|
236
|
+
updatedAt: input.targetGoalUpdatedAt,
|
|
237
|
+
evidence: undefined,
|
|
238
|
+
completedAt: undefined,
|
|
239
|
+
completionVerification: undefined,
|
|
240
|
+
};
|
|
241
|
+
const goals =
|
|
242
|
+
input.receiptKind === "final-aggregate"
|
|
243
|
+
? input.plan.goals.map(goal => ({
|
|
244
|
+
...goal,
|
|
245
|
+
status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
|
|
246
|
+
updatedAt: goal.id === input.goal.id ? input.targetGoalUpdatedAt : goal.updatedAt,
|
|
247
|
+
evidence: goal.id === input.goal.id ? undefined : goal.evidence,
|
|
248
|
+
completedAt: goal.id === input.goal.id ? undefined : goal.completedAt,
|
|
249
|
+
completionVerification: undefined,
|
|
250
|
+
}))
|
|
251
|
+
: [targetGoalSnapshot];
|
|
232
252
|
return {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
completedAt: goal.id === input.goal.id ? undefined : goal.completedAt,
|
|
241
|
-
completionVerification: undefined,
|
|
242
|
-
})),
|
|
253
|
+
version: input.plan.version,
|
|
254
|
+
brief: input.plan.brief,
|
|
255
|
+
gjcGoalMode: input.plan.gjcGoalMode,
|
|
256
|
+
gjcObjective: input.plan.gjcObjective,
|
|
257
|
+
gjcObjectiveAliases: input.plan.gjcObjectiveAliases,
|
|
258
|
+
createdAt: input.plan.createdAt,
|
|
259
|
+
goals,
|
|
243
260
|
};
|
|
244
261
|
}
|
|
245
262
|
|
|
@@ -264,6 +281,7 @@ export function computeUltragoalPlanGeneration(input: {
|
|
|
264
281
|
goal: input.goal,
|
|
265
282
|
beforeStatus: input.beforeStatus,
|
|
266
283
|
targetGoalUpdatedAt,
|
|
284
|
+
receiptKind: input.receiptKind,
|
|
267
285
|
}),
|
|
268
286
|
);
|
|
269
287
|
const requiredGoalSetHashBeforeCheckpoint = hashStructuredValue(
|
|
@@ -569,6 +587,31 @@ function chooseNextGoal(plan: UltragoalPlan, retryFailed: boolean): UltragoalGoa
|
|
|
569
587
|
(retryFailed ? plan.goals.find(goal => goal.status === "failed") : undefined)
|
|
570
588
|
);
|
|
571
589
|
}
|
|
590
|
+
export interface UltragoalRunCompletionState {
|
|
591
|
+
requiredGoals: UltragoalGoal[];
|
|
592
|
+
incompleteGoals: UltragoalGoal[];
|
|
593
|
+
nextGoal?: UltragoalGoal;
|
|
594
|
+
allComplete: boolean;
|
|
595
|
+
hasBlockers: boolean;
|
|
596
|
+
needsFinalAggregateReceipt: boolean;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function getUltragoalRunCompletionState(
|
|
600
|
+
plan: UltragoalPlan,
|
|
601
|
+
options: { retryFailed?: boolean } = {},
|
|
602
|
+
): UltragoalRunCompletionState {
|
|
603
|
+
const requiredGoals = requiredUltragoalGoals(plan);
|
|
604
|
+
const incompleteGoals = requiredGoals.filter(goal => !TERMINAL_OR_SKIPPED_STATUSES.has(goal.status));
|
|
605
|
+
const nextGoal = chooseNextGoal(plan, options.retryFailed === true);
|
|
606
|
+
return {
|
|
607
|
+
requiredGoals,
|
|
608
|
+
incompleteGoals,
|
|
609
|
+
nextGoal,
|
|
610
|
+
allComplete: requiredGoals.length > 0 && incompleteGoals.length === 0,
|
|
611
|
+
hasBlockers: incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "review_blocked"),
|
|
612
|
+
needsFinalAggregateReceipt: plan.gjcGoalMode === "aggregate" && incompleteGoals.length === 0,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
572
615
|
|
|
573
616
|
export async function startNextUltragoalGoal(input: { cwd: string; retryFailed?: boolean }): Promise<{
|
|
574
617
|
plan: UltragoalPlan;
|
|
@@ -578,7 +621,7 @@ export async function startNextUltragoalGoal(input: { cwd: string; retryFailed?:
|
|
|
578
621
|
const plan = await readUltragoalPlan(input.cwd);
|
|
579
622
|
if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
|
|
580
623
|
const goal = chooseNextGoal(plan, input.retryFailed === true);
|
|
581
|
-
if (!goal) return { plan, allComplete: plan.
|
|
624
|
+
if (!goal) return { plan, allComplete: getUltragoalRunCompletionState(plan).allComplete };
|
|
582
625
|
if (goal.status !== "active") {
|
|
583
626
|
const now = new Date().toISOString();
|
|
584
627
|
goal.status = "active";
|
|
@@ -639,7 +682,11 @@ function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
|
|
|
639
682
|
function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
|
|
640
683
|
const value = row[key];
|
|
641
684
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
642
|
-
|
|
685
|
+
const hint =
|
|
686
|
+
key === "obligation" && typeof row.description === "string" && row.description.trim().length > 0
|
|
687
|
+
? "; found description, but complete-checkpoint contractCoverage rows require obligation"
|
|
688
|
+
: "";
|
|
689
|
+
throw new Error(`qualityGate ${fieldName}.${key} must be a non-empty string${hint}`);
|
|
643
690
|
}
|
|
644
691
|
return value.trim();
|
|
645
692
|
}
|
|
@@ -1015,6 +1062,17 @@ async function readRequiredCompletionQualityGate(cwd: string, value: string | un
|
|
|
1015
1062
|
return gate;
|
|
1016
1063
|
}
|
|
1017
1064
|
|
|
1065
|
+
function snapshotUpdatedAtMilliseconds(value: unknown): number | null {
|
|
1066
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1067
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
1068
|
+
const trimmed = value.trim();
|
|
1069
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1070
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1071
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1072
|
+
}
|
|
1073
|
+
const parsed = Date.parse(trimmed);
|
|
1074
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1075
|
+
}
|
|
1018
1076
|
async function readGjcGoalSnapshot(input: {
|
|
1019
1077
|
cwd: string;
|
|
1020
1078
|
value: string | undefined;
|
|
@@ -1026,16 +1084,23 @@ async function readGjcGoalSnapshot(input: {
|
|
|
1026
1084
|
}): Promise<unknown> {
|
|
1027
1085
|
if (!input.value?.trim()) {
|
|
1028
1086
|
if (!input.required) return undefined;
|
|
1029
|
-
throw new Error(
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`${input.errorPrefix} require --gjc-goal-json from a fresh active goal({"op":"get"}) snapshot; this is the GJC goal-mode receipt, not the .gjc/ultragoal/goals.json goal record`,
|
|
1089
|
+
);
|
|
1030
1090
|
}
|
|
1031
1091
|
const snapshot = await readStructuredValue(input.cwd, input.value);
|
|
1032
1092
|
const snapshotObject = qualityGateObject(snapshot);
|
|
1033
1093
|
const detailsObject = qualityGateObject(snapshotObject?.details);
|
|
1034
1094
|
const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
|
|
1035
|
-
if (!goalObject)
|
|
1036
|
-
|
|
1095
|
+
if (!goalObject)
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
`${input.errorPrefix} require --gjc-goal-json with a goal object from goal({"op":"get"}); pass the active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record`,
|
|
1098
|
+
);
|
|
1099
|
+
const updatedAt = snapshotUpdatedAtMilliseconds(goalObject.updatedAt);
|
|
1037
1100
|
if (!updatedAt)
|
|
1038
|
-
throw new Error(
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`${input.errorPrefix} require --gjc-goal-json goal.updatedAt as epoch milliseconds or an ISO timestamp from goal({"op":"get"}); pass the active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record`,
|
|
1103
|
+
);
|
|
1039
1104
|
const nowMilliseconds = Date.now();
|
|
1040
1105
|
if (updatedAt < nowMilliseconds - GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS) {
|
|
1041
1106
|
throw new Error(`${input.errorPrefix} require a fresh --gjc-goal-json snapshot`);
|
|
@@ -1052,7 +1117,9 @@ async function readGjcGoalSnapshot(input: {
|
|
|
1052
1117
|
return snapshot;
|
|
1053
1118
|
}
|
|
1054
1119
|
if (!expectedObjectives.has(objective)) {
|
|
1055
|
-
throw new Error(
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`${input.errorPrefix} require --gjc-goal-json objective to match the active GJC goal-mode objective from goal({"op":"get"}), not the .gjc/ultragoal/goals.json goal ${input.goal?.id ?? "record"}`,
|
|
1122
|
+
);
|
|
1056
1123
|
}
|
|
1057
1124
|
if (goalObject.status !== "active") {
|
|
1058
1125
|
throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.status to be active`);
|
|
@@ -1147,6 +1214,54 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
1147
1214
|
});
|
|
1148
1215
|
return plan;
|
|
1149
1216
|
}
|
|
1217
|
+
export interface UltragoalCheckpointContinuation {
|
|
1218
|
+
plan: UltragoalPlan;
|
|
1219
|
+
checkpointedGoal: UltragoalGoal;
|
|
1220
|
+
nextGoal?: UltragoalGoal;
|
|
1221
|
+
startedNext: boolean;
|
|
1222
|
+
allComplete: boolean;
|
|
1223
|
+
incompleteGoals: UltragoalGoal[];
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export async function checkpointAndContinueUltragoalGoal(input: {
|
|
1227
|
+
cwd: string;
|
|
1228
|
+
goalId: string;
|
|
1229
|
+
status: UltragoalGoalStatus;
|
|
1230
|
+
evidence: string;
|
|
1231
|
+
gjcGoalJson?: string;
|
|
1232
|
+
qualityGateJson?: string;
|
|
1233
|
+
advanceNext?: boolean;
|
|
1234
|
+
retryFailed?: boolean;
|
|
1235
|
+
}): Promise<UltragoalCheckpointContinuation> {
|
|
1236
|
+
let plan = await checkpointUltragoalGoal(input);
|
|
1237
|
+
const checkpointedGoal = plan.goals.find(goal => goal.id === input.goalId);
|
|
1238
|
+
if (!checkpointedGoal) throw new Error(`No ultragoal goal found for ${input.goalId}.`);
|
|
1239
|
+
if (input.status === "complete" && input.advanceNext === true) {
|
|
1240
|
+
const beforeAdvance = getUltragoalRunCompletionState(plan, { retryFailed: input.retryFailed });
|
|
1241
|
+
if (beforeAdvance.nextGoal && beforeAdvance.nextGoal.status !== "active") {
|
|
1242
|
+
const started = await startNextUltragoalGoal({ cwd: input.cwd, retryFailed: input.retryFailed });
|
|
1243
|
+
plan = started.plan;
|
|
1244
|
+
const afterAdvance = getUltragoalRunCompletionState(plan, { retryFailed: input.retryFailed });
|
|
1245
|
+
return {
|
|
1246
|
+
plan,
|
|
1247
|
+
checkpointedGoal,
|
|
1248
|
+
nextGoal: started.goal,
|
|
1249
|
+
startedNext: Boolean(started.goal),
|
|
1250
|
+
allComplete: afterAdvance.allComplete,
|
|
1251
|
+
incompleteGoals: afterAdvance.incompleteGoals,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
const state = getUltragoalRunCompletionState(plan, { retryFailed: input.retryFailed });
|
|
1256
|
+
return {
|
|
1257
|
+
plan,
|
|
1258
|
+
checkpointedGoal,
|
|
1259
|
+
nextGoal: state.nextGoal,
|
|
1260
|
+
startedNext: false,
|
|
1261
|
+
allComplete: state.allComplete,
|
|
1262
|
+
incompleteGoals: state.incompleteGoals,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1150
1265
|
|
|
1151
1266
|
export async function addUltragoalSubgoal(input: {
|
|
1152
1267
|
cwd: string;
|
|
@@ -1235,6 +1350,8 @@ function hasFlag(args: readonly string[], flag: string): boolean {
|
|
|
1235
1350
|
return args.includes(flag);
|
|
1236
1351
|
}
|
|
1237
1352
|
|
|
1353
|
+
const HELP_FLAGS = new Set(["--help", "-h"]);
|
|
1354
|
+
|
|
1238
1355
|
const FLAGS_WITH_VALUES = new Set([
|
|
1239
1356
|
"--brief",
|
|
1240
1357
|
"--brief-file",
|
|
@@ -1250,6 +1367,10 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
1250
1367
|
"--rationale",
|
|
1251
1368
|
]);
|
|
1252
1369
|
|
|
1370
|
+
function isHelpArg(arg: string): boolean {
|
|
1371
|
+
return HELP_FLAGS.has(arg);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1253
1374
|
function commandName(args: readonly string[]): string {
|
|
1254
1375
|
let skipNext = false;
|
|
1255
1376
|
for (const arg of args) {
|
|
@@ -1261,11 +1382,62 @@ function commandName(args: readonly string[]): string {
|
|
|
1261
1382
|
skipNext = true;
|
|
1262
1383
|
continue;
|
|
1263
1384
|
}
|
|
1385
|
+
if (isHelpArg(arg)) continue;
|
|
1264
1386
|
if (!arg.startsWith("-")) return arg;
|
|
1265
1387
|
}
|
|
1266
1388
|
return "status";
|
|
1267
1389
|
}
|
|
1268
1390
|
|
|
1391
|
+
function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
1392
|
+
if (!args.some(isHelpArg) && args[0] !== "help") return null;
|
|
1393
|
+
const subject =
|
|
1394
|
+
args[0] === "help" ? args.find((arg, index) => index > 0 && !arg.startsWith("-")) : commandName(args);
|
|
1395
|
+
if (subject === "checkpoint") {
|
|
1396
|
+
return [
|
|
1397
|
+
"Run native GJC Ultragoal workflow commands",
|
|
1398
|
+
"",
|
|
1399
|
+
"USAGE",
|
|
1400
|
+
" $ gjc ultragoal checkpoint --goal-id <id> --status <status> --evidence <text> [FLAGS]",
|
|
1401
|
+
"",
|
|
1402
|
+
"FLAGS",
|
|
1403
|
+
" --goal-id=<value> Durable .gjc/ultragoal goal id, e.g. G001",
|
|
1404
|
+
" --status=<value> pending|active|complete|failed|blocked|review_blocked|superseded",
|
|
1405
|
+
" --evidence=<value> Completion or checkpoint evidence text",
|
|
1406
|
+
" --quality-gate-json=<value> JSON string or path for complete checkpoints",
|
|
1407
|
+
' --gjc-goal-json=<value> JSON string or path containing the current goal({"op":"get"}) snapshot',
|
|
1408
|
+
" --json Output a machine-readable receipt",
|
|
1409
|
+
"",
|
|
1410
|
+
"COMPLETE CHECKPOINT RECEIPTS",
|
|
1411
|
+
" --quality-gate-json must be an object with architectReview, executorQa, and iteration.",
|
|
1412
|
+
" executorQa.contractCoverage[] rows require an obligation field; description is not a substitute.",
|
|
1413
|
+
' --gjc-goal-json must contain the active GJC goal-mode snapshot from goal({"op":"get"}), not the .gjc/ultragoal/goals.json goal record.',
|
|
1414
|
+
" goal.updatedAt may be epoch milliseconds or an ISO timestamp and must be fresh.",
|
|
1415
|
+
"",
|
|
1416
|
+
"EXAMPLES",
|
|
1417
|
+
' $ gjc ultragoal checkpoint --goal-id G001 --status blocked --evidence "waiting on review"',
|
|
1418
|
+
' $ gjc ultragoal checkpoint --goal-id G001 --status complete --evidence "tests passed" --gjc-goal-json ./goal.json --quality-gate-json ./quality-gate.json --json',
|
|
1419
|
+
"",
|
|
1420
|
+
].join("\n");
|
|
1421
|
+
}
|
|
1422
|
+
return [
|
|
1423
|
+
"Run native GJC Ultragoal workflow commands",
|
|
1424
|
+
"",
|
|
1425
|
+
"USAGE",
|
|
1426
|
+
" $ gjc ultragoal <command> [FLAGS]",
|
|
1427
|
+
"",
|
|
1428
|
+
"COMMANDS",
|
|
1429
|
+
" status",
|
|
1430
|
+
" create-goals",
|
|
1431
|
+
" complete-goals",
|
|
1432
|
+
" checkpoint",
|
|
1433
|
+
" steer",
|
|
1434
|
+
" record-review-blockers",
|
|
1435
|
+
"",
|
|
1436
|
+
"Run `gjc ultragoal checkpoint --help` for complete checkpoint receipt requirements.",
|
|
1437
|
+
"",
|
|
1438
|
+
].join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1269
1441
|
async function readBrief(cwd: string, args: readonly string[]): Promise<string> {
|
|
1270
1442
|
const inline = flagValue(args, "--brief");
|
|
1271
1443
|
if (inline !== undefined) return inline;
|
|
@@ -1306,8 +1478,54 @@ function renderCompleteHandoff(
|
|
|
1306
1478
|
"",
|
|
1307
1479
|
].join("\n");
|
|
1308
1480
|
}
|
|
1481
|
+
function renderCheckpointContinuation(
|
|
1482
|
+
result: UltragoalCheckpointContinuation,
|
|
1483
|
+
status: UltragoalGoalStatus,
|
|
1484
|
+
json: boolean,
|
|
1485
|
+
cwd: string,
|
|
1486
|
+
): string {
|
|
1487
|
+
if (json)
|
|
1488
|
+
return renderCliWriteReceipt({
|
|
1489
|
+
ok: true,
|
|
1490
|
+
goal_id: result.checkpointedGoal.id,
|
|
1491
|
+
status,
|
|
1492
|
+
goals_path: getUltragoalPaths(cwd).goalsPath,
|
|
1493
|
+
completion_receipt_kind: result.checkpointedGoal.completionVerification?.receiptKind,
|
|
1494
|
+
quality_gate_hash: result.checkpointedGoal.completionVerification?.qualityGateHash,
|
|
1495
|
+
all_complete: result.allComplete,
|
|
1496
|
+
next_goal_id: result.nextGoal?.id,
|
|
1497
|
+
next_goal_status: result.nextGoal?.status,
|
|
1498
|
+
started_next: result.startedNext,
|
|
1499
|
+
incomplete_goal_ids: result.incompleteGoals.map(goal => goal.id),
|
|
1500
|
+
});
|
|
1501
|
+
const lines = [`Checkpointed ${result.checkpointedGoal.id} as ${status}.`];
|
|
1502
|
+
if (status === "complete") {
|
|
1503
|
+
if (result.allComplete) {
|
|
1504
|
+
lines.push("All ultragoal goals are complete.");
|
|
1505
|
+
} else if (result.nextGoal) {
|
|
1506
|
+
lines.push(`Next ultragoal goal: ${result.nextGoal.id} — ${result.nextGoal.title}`);
|
|
1507
|
+
lines.push(`Objective: ${result.nextGoal.objective}`);
|
|
1508
|
+
lines.push(`GJC objective: ${result.plan.gjcObjective}`);
|
|
1509
|
+
lines.push(
|
|
1510
|
+
result.startedNext
|
|
1511
|
+
? "The next ultragoal goal is active; continue the current aggregate GJC goal and checkpoint this story when verified."
|
|
1512
|
+
: "Run `gjc ultragoal complete-goals` to activate the next ultragoal story.",
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
} else if (status === "failed") {
|
|
1516
|
+
lines.push("Resume failed goals with `gjc ultragoal complete-goals --retry-failed` after the blocker is fixed.");
|
|
1517
|
+
} else if (status === "blocked" || status === "review_blocked") {
|
|
1518
|
+
lines.push(
|
|
1519
|
+
"Blocked ultragoal work must be resolved with explicit blocker work or steering before final completion.",
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
lines.push("");
|
|
1523
|
+
return lines.join("\n");
|
|
1524
|
+
}
|
|
1309
1525
|
|
|
1310
1526
|
async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<UltragoalCommandResult> {
|
|
1527
|
+
const help = renderUltragoalHelp(args);
|
|
1528
|
+
if (help) return { status: 0, stdout: help };
|
|
1311
1529
|
try {
|
|
1312
1530
|
const command = commandName(args);
|
|
1313
1531
|
const json = hasFlag(args, "--json");
|
|
@@ -1344,27 +1562,18 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
1344
1562
|
const goalId = flagValue(args, "--goal-id") ?? "";
|
|
1345
1563
|
const status = parseGoalStatus(flagValue(args, "--status"));
|
|
1346
1564
|
const evidence = flagValue(args, "--evidence") ?? "";
|
|
1347
|
-
const
|
|
1565
|
+
const result = await checkpointAndContinueUltragoalGoal({
|
|
1348
1566
|
cwd,
|
|
1349
1567
|
goalId,
|
|
1350
1568
|
status,
|
|
1351
1569
|
evidence,
|
|
1352
1570
|
gjcGoalJson: flagValue(args, "--gjc-goal-json"),
|
|
1353
1571
|
qualityGateJson: flagValue(args, "--quality-gate-json"),
|
|
1572
|
+
advanceNext: status === "complete",
|
|
1354
1573
|
});
|
|
1355
|
-
const goal = plan.goals.find(item => item.id === goalId);
|
|
1356
1574
|
return {
|
|
1357
1575
|
status: 0,
|
|
1358
|
-
stdout: json
|
|
1359
|
-
? renderCliWriteReceipt({
|
|
1360
|
-
ok: true,
|
|
1361
|
-
goal_id: goalId,
|
|
1362
|
-
status,
|
|
1363
|
-
goals_path: getUltragoalPaths(cwd).goalsPath,
|
|
1364
|
-
completion_receipt_kind: goal?.completionVerification?.receiptKind,
|
|
1365
|
-
quality_gate_hash: goal?.completionVerification?.qualityGateHash,
|
|
1366
|
-
})
|
|
1367
|
-
: `ultragoal checkpoint goal-id=${goalId} status=${status}\n`,
|
|
1576
|
+
stdout: renderCheckpointContinuation(result, status, json, cwd),
|
|
1368
1577
|
};
|
|
1369
1578
|
}
|
|
1370
1579
|
case "steer": {
|
|
@@ -1469,7 +1678,8 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
|
|
|
1469
1678
|
export async function runNativeUltragoalCommand(args: string[], cwd = process.cwd()): Promise<UltragoalCommandResult> {
|
|
1470
1679
|
const command = commandName(args);
|
|
1471
1680
|
const result = await dispatchUltragoalCommand(args, cwd);
|
|
1472
|
-
|
|
1681
|
+
const isHelp = args.some(isHelpArg) || args[0] === "help";
|
|
1682
|
+
if (!isHelp && result.status === 0 && RECONCILE_COMMANDS.has(command)) {
|
|
1473
1683
|
await reconcileUltragoalState(cwd);
|
|
1474
1684
|
}
|
|
1475
1685
|
return result;
|