@gajae-code/coding-agent 0.7.8 → 0.7.10
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 +40 -0
- package/README.md +8 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line.d.ts +3 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +26 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/cmux-workspace.d.ts +23 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +5 -0
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +3 -2
- package/src/cli.ts +1 -0
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +81 -5
- package/src/config/settings-schema.ts +14 -3
- package/src/coordinator-mcp/server.ts +4 -2
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +366 -48
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +150 -24
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/modes/components/custom-model-preset-wizard.ts +2 -4
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +83 -24
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +10 -2
- package/src/modes/components/status-line.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +130 -7
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +13 -4
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +69 -45
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +13 -6
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +147 -17
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/cmux-workspace.ts +106 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/search/index.ts +22 -10
- package/src/web/search/provider.ts +15 -0
- package/src/web/search/render.ts +3 -0
|
@@ -1931,7 +1931,22 @@ export function buildWorkerCommand(
|
|
|
1931
1931
|
const workspace = worker.worktree_path
|
|
1932
1932
|
? `Worker worktree: ${worker.worktree_path}.`
|
|
1933
1933
|
: `Worker cwd: ${config.leader.cwd}.`;
|
|
1934
|
-
|
|
1934
|
+
// The worker prompt body is dispatched into the pane through
|
|
1935
|
+
// `tmux send-keys`, which treats embedded LF characters as Enter
|
|
1936
|
+
// keypresses. A multi-line prompt therefore lands in the pane as
|
|
1937
|
+
// multiple prompt bodies followed by premature Enter presses, which on
|
|
1938
|
+
// Windows + psmux/ConPTY causes the worker CLI to bail out before the
|
|
1939
|
+
// startup ACK fires. Normalize the body to a single line by replacing
|
|
1940
|
+
// any LF/CRLF with a space, and strip a defensive U+FEFF in case a
|
|
1941
|
+
// caller managed to inject a UTF-8 BOM into the task text. Empty /
|
|
1942
|
+
// whitespace-only bodies fall back to a one-line placeholder so the
|
|
1943
|
+
// worker never sits idle at an empty prompt.
|
|
1944
|
+
const normalizePrompt = (raw: string): string =>
|
|
1945
|
+
raw
|
|
1946
|
+
.replace(/[\uFEFF\u200B]/g, "")
|
|
1947
|
+
.replace(/\r?\n+/g, " ")
|
|
1948
|
+
.trim();
|
|
1949
|
+
const rawPrompt = [
|
|
1935
1950
|
`You are ${worker.id} in gjc team ${config.team_name}.`,
|
|
1936
1951
|
`Team state root: ${config.state_root}.`,
|
|
1937
1952
|
workspace,
|
|
@@ -1940,6 +1955,7 @@ export function buildWorkerCommand(
|
|
|
1940
1955
|
`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.`,
|
|
1941
1956
|
`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.`,
|
|
1942
1957
|
].join("\n");
|
|
1958
|
+
const prompt = normalizePrompt(rawPrompt) || `Worker ${worker.id} ready.`;
|
|
1943
1959
|
const envLines = [
|
|
1944
1960
|
envAssignment("GJC_TEAM_WORKER", `${config.team_name}/${worker.id}`),
|
|
1945
1961
|
envAssignment("GJC_TEAM_INTERNAL_WORKER", `${config.team_name}/${worker.id}`),
|
|
@@ -1951,8 +1967,33 @@ export function buildWorkerCommand(
|
|
|
1951
1967
|
...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
|
|
1952
1968
|
];
|
|
1953
1969
|
const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
|
|
1954
|
-
|
|
1955
|
-
|
|
1970
|
+
// On Windows we wrap the worker command invocation in `& { & 'cmd' 'arg1' ... }`
|
|
1971
|
+
// so pwsh keeps the whole multi-statement body in command position. Two
|
|
1972
|
+
// failure modes this avoids:
|
|
1973
|
+
// 1. Bare `bun 'cli.ts' 'prompt'` after `$env:X = 'y'; ...` would be
|
|
1974
|
+
// parsed in expression position and PowerShell would reject the
|
|
1975
|
+
// second quoted token with "Unexpected token '<cli.ts>'".
|
|
1976
|
+
// 2. `& { 'cmd' 'arg1' 'arg2' }` (single & inside a block with adjacent
|
|
1977
|
+
// single-quoted tokens) is itself invalid because pwsh does not
|
|
1978
|
+
// concatenate adjacent single-quoted strings inside a script block.
|
|
1979
|
+
// The nested `&` inside the block forces command-position parsing for
|
|
1980
|
+
// the invocation. POSIX shells do not need this — they already treat
|
|
1981
|
+
// `cmd 'arg'` as a normal command invocation after `;`-separated
|
|
1982
|
+
// variable assignments.
|
|
1983
|
+
//
|
|
1984
|
+
// ASSUMPTION: this branch only fires when the worker pane is a PowerShell
|
|
1985
|
+
// shell. psmux launches pwsh by default on Windows, so the unset /
|
|
1986
|
+
// unset-and-replace cases are correct. If a user has explicitly set
|
|
1987
|
+
// `set -g default-shell "C:/Program Files/Git/bin/bash.exe"` (or any
|
|
1988
|
+
// other non-pwsh shell), this branch will send PowerShell syntax to a
|
|
1989
|
+
// bash pane and the worker will fail with a parse error. Detecting the
|
|
1990
|
+
// pane's shell at runtime and switching the quoting + invocation style
|
|
1991
|
+
// accordingly is a follow-up; for now the pwsh default is assumed.
|
|
1992
|
+
if (platform === "win32") {
|
|
1993
|
+
const invocation = `& ${config.worker_command} ${quote(prompt)}`;
|
|
1994
|
+
return `& { ${joined} ${invocation} }`;
|
|
1995
|
+
}
|
|
1996
|
+
return `${joined} ${config.worker_command} ${quote(prompt)}`;
|
|
1956
1997
|
}
|
|
1957
1998
|
interface GjcTeamInitialLane {
|
|
1958
1999
|
label: string;
|
|
@@ -2070,7 +2111,6 @@ async function startTmuxSession(
|
|
|
2070
2111
|
"#{pane_id}",
|
|
2071
2112
|
"-c",
|
|
2072
2113
|
worker.worktree_path ?? config.leader.cwd,
|
|
2073
|
-
buildWorkerCommand(config, worker),
|
|
2074
2114
|
],
|
|
2075
2115
|
{ stdout: "pipe", stderr: "pipe" },
|
|
2076
2116
|
);
|
|
@@ -2081,6 +2121,31 @@ async function startTmuxSession(
|
|
|
2081
2121
|
rollbackPaneIds.push(paneId);
|
|
2082
2122
|
if (worker.index === 1) rightStackRootPaneId = paneId;
|
|
2083
2123
|
workers.push({ ...worker, pane_id: paneId });
|
|
2124
|
+
// On psmux/ConPTY (Windows pwsh default-shell) panes, tmux writes the
|
|
2125
|
+
// split-window command argv to the new pane's stdin but pwsh's
|
|
2126
|
+
// readline never receives an Enter, so the worker never starts.
|
|
2127
|
+
// Create the pane without a command, then dispatch the worker
|
|
2128
|
+
// command through send-keys + Enter so it actually executes.
|
|
2129
|
+
// Two-step dispatch because tmux's `-l` (literal mode) flag is
|
|
2130
|
+
// global per send-keys invocation, not per arg: passing `-l
|
|
2131
|
+
// <body> Enter` would treat "Enter" as literal text too. Sending
|
|
2132
|
+
// the body in literal mode first and the Enter keypress second
|
|
2133
|
+
// keeps the body verbatim while still submitting the prompt as a
|
|
2134
|
+
// keystroke.
|
|
2135
|
+
const body = buildWorkerCommand(config, worker);
|
|
2136
|
+
Bun.spawnSync([config.tmux_command, "send-keys", "-l", "-t", paneId, body], {
|
|
2137
|
+
stdout: "ignore",
|
|
2138
|
+
stderr: "ignore",
|
|
2139
|
+
});
|
|
2140
|
+
const sendKeys = Bun.spawnSync([config.tmux_command, "send-keys", "-t", paneId, "Enter"], {
|
|
2141
|
+
stdout: "ignore",
|
|
2142
|
+
stderr: "ignore",
|
|
2143
|
+
});
|
|
2144
|
+
// void-cast the exit code so the linter does not flag an unused
|
|
2145
|
+
// expression; the value is intentionally discarded here because
|
|
2146
|
+
// the actual spawn outcome is recovered by the leader through
|
|
2147
|
+
// the worker startup-ack watcher, not via the spawn exit code.
|
|
2148
|
+
void sendKeys.exitCode;
|
|
2084
2149
|
}
|
|
2085
2150
|
Bun.spawnSync([config.tmux_command, "select-layout", "-t", config.tmux_target, "main-vertical"], {
|
|
2086
2151
|
stdout: "ignore",
|
|
@@ -69,9 +69,12 @@ function isKnownUltragoalObjective(currentObjective: string): boolean {
|
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
async function ultragoalReadPaths(
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
async function ultragoalReadPaths(
|
|
73
|
+
cwd: string,
|
|
74
|
+
options: { sessionId?: string | null } = {},
|
|
75
|
+
): Promise<{ paths: UltragoalPaths; sessionId: string | null }> {
|
|
76
|
+
const explicitSessionId = options.sessionId?.trim() || process.env.GJC_SESSION_ID?.trim();
|
|
77
|
+
if (explicitSessionId) return { paths: getUltragoalPaths(cwd, explicitSessionId), sessionId: explicitSessionId };
|
|
75
78
|
try {
|
|
76
79
|
const session = await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
|
|
77
80
|
return { paths: getUltragoalPaths(cwd, session.gjcSessionId), sessionId: session.gjcSessionId };
|
|
@@ -250,13 +253,6 @@ export function validateCompletionReceipt(input: {
|
|
|
250
253
|
goalId: input.goal.id,
|
|
251
254
|
};
|
|
252
255
|
}
|
|
253
|
-
if (hashStructuredValue(event.gjcGoalJson) !== receipt.gjcGoalSnapshotHash) {
|
|
254
|
-
return {
|
|
255
|
-
state: "active_stale_receipt",
|
|
256
|
-
message: `Ultragoal ${input.goal.id} receipt goal({"op":"get"}) snapshot hash does not match ledger.`,
|
|
257
|
-
goalId: input.goal.id,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
256
|
if (input.goal.updatedAt !== receipt.verifiedAt) {
|
|
261
257
|
return {
|
|
262
258
|
state: "active_stale_receipt",
|
|
@@ -273,15 +269,28 @@ export function validateCompletionReceipt(input: {
|
|
|
273
269
|
goalId: input.goal.id,
|
|
274
270
|
};
|
|
275
271
|
}
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
272
|
+
for (const priorGoal of requiredGoals(input.plan)) {
|
|
273
|
+
if (priorGoal.id === input.goal.id) continue;
|
|
274
|
+
if (!priorGoal.completionVerification) {
|
|
275
|
+
return {
|
|
276
|
+
state: "active_missing_receipt",
|
|
277
|
+
message: `Ultragoal final receipt is missing per-goal evidence for: ${priorGoal.id}.`,
|
|
278
|
+
goalId: input.goal.id,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const priorDiagnostic = validateCompletionReceipt({
|
|
282
|
+
plan: input.plan,
|
|
283
|
+
ledger: input.ledger,
|
|
284
|
+
goal: priorGoal,
|
|
285
|
+
receiptKind: "per-goal",
|
|
286
|
+
});
|
|
287
|
+
if (priorDiagnostic.state !== "active_verified_complete") {
|
|
288
|
+
return {
|
|
289
|
+
state: priorDiagnostic.state,
|
|
290
|
+
message: `Ultragoal final receipt requires a valid per-goal receipt for ${priorGoal.id}: ${priorDiagnostic.message}`,
|
|
291
|
+
goalId: input.goal.id,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
285
294
|
}
|
|
286
295
|
}
|
|
287
296
|
return {
|
|
@@ -372,11 +381,128 @@ export async function readUltragoalVerificationState(input: {
|
|
|
372
381
|
return receiptDiagnostic;
|
|
373
382
|
}
|
|
374
383
|
|
|
375
|
-
export async function
|
|
384
|
+
export async function verifyUltragoalDurableCompletionState(input: {
|
|
385
|
+
cwd: string;
|
|
386
|
+
sessionId?: string | null;
|
|
387
|
+
}): Promise<UltragoalGuardDiagnostic> {
|
|
388
|
+
let paths: UltragoalPaths;
|
|
389
|
+
let sessionId: string | null;
|
|
390
|
+
try {
|
|
391
|
+
({ paths, sessionId } = await ultragoalReadPaths(input.cwd, { sessionId: input.sessionId }));
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return {
|
|
394
|
+
state: "unreadable_fail_closed",
|
|
395
|
+
message: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (sessionId === null)
|
|
399
|
+
return { state: "inactive", message: "No active GJC session resolved; ultragoal is inactive." };
|
|
400
|
+
try {
|
|
401
|
+
await fs.stat(paths.dir);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (isEnoent(error)) return { state: "inactive", message: "No durable .gjc/ultragoal state exists." };
|
|
404
|
+
return {
|
|
405
|
+
state: "unreadable_fail_closed",
|
|
406
|
+
message: `Durable .gjc/ultragoal state is present but unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let plan: UltragoalPlan | null;
|
|
411
|
+
let ledger: UltragoalLedgerEvent[];
|
|
412
|
+
try {
|
|
413
|
+
plan = await readUltragoalPlan(input.cwd, sessionId);
|
|
414
|
+
ledger = await readUltragoalLedger(input.cwd, sessionId);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
state: "unreadable_fail_closed",
|
|
418
|
+
message: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (!plan) return { state: "inactive", message: "No Ultragoal plan exists." };
|
|
422
|
+
|
|
423
|
+
if (plan.goals.some(goal => goal.status === "review_blocked")) {
|
|
424
|
+
return {
|
|
425
|
+
state: "active_review_blocked_recorded",
|
|
426
|
+
message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const runState = getUltragoalRunCompletionState(plan);
|
|
431
|
+
if (runState.incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
|
|
432
|
+
return {
|
|
433
|
+
state: "active_dirty_quality_gate",
|
|
434
|
+
message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (plan.gjcGoalMode === "per-story") {
|
|
439
|
+
const incomplete = requiredGoals(plan).filter(goal => goal.status !== "complete");
|
|
440
|
+
if (incomplete.length > 0) {
|
|
441
|
+
return {
|
|
442
|
+
state: "active_missing_receipt",
|
|
443
|
+
message: `Ultragoal per-story completion requires all required stories to be complete; incomplete: ${incomplete.map(goal => goal.id).join(", ")}.`,
|
|
444
|
+
goalId: incomplete[0]?.id,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
for (const goal of requiredGoals(plan)) {
|
|
448
|
+
const diagnostic = validateCompletionReceipt({
|
|
449
|
+
plan,
|
|
450
|
+
ledger,
|
|
451
|
+
goal,
|
|
452
|
+
receiptKind: "per-goal",
|
|
453
|
+
});
|
|
454
|
+
if (diagnostic.state !== "active_verified_complete") return diagnostic;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
state: "active_verified_complete",
|
|
458
|
+
message: "Ultragoal per-story run is verified complete.",
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const ask = await isUltragoalAskBlocked(input.cwd, { sessionId });
|
|
463
|
+
if (!ask.active) {
|
|
464
|
+
return {
|
|
465
|
+
state: "active_verified_complete",
|
|
466
|
+
message: ask.reason,
|
|
467
|
+
goalId: ask.goalIds?.at(0),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (ask.source === "durable_state_unreadable") {
|
|
471
|
+
return {
|
|
472
|
+
state: "unreadable_fail_closed",
|
|
473
|
+
message: ask.reason,
|
|
474
|
+
goalId: ask.goalIds?.at(0),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (ask.reason.includes("recorded review blockers")) {
|
|
478
|
+
return {
|
|
479
|
+
state: "active_review_blocked_recorded",
|
|
480
|
+
message: ask.reason,
|
|
481
|
+
goalId: ask.goalIds?.at(0),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (ask.reason.includes("incomplete required goals")) {
|
|
485
|
+
return {
|
|
486
|
+
state: "active_missing_final_receipt",
|
|
487
|
+
message: ask.reason,
|
|
488
|
+
goalId: ask.goalIds?.at(0),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
state: "active_missing_final_receipt",
|
|
493
|
+
message: ask.reason,
|
|
494
|
+
goalId: ask.goalIds?.at(0),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export async function isUltragoalAskBlocked(
|
|
499
|
+
cwd: string,
|
|
500
|
+
options: { sessionId?: string | null } = {},
|
|
501
|
+
): Promise<UltragoalAskBlockDiagnostic> {
|
|
376
502
|
let paths: UltragoalPaths;
|
|
377
503
|
let sessionId: string | null;
|
|
378
504
|
try {
|
|
379
|
-
({ paths, sessionId } = await ultragoalReadPaths(cwd));
|
|
505
|
+
({ paths, sessionId } = await ultragoalReadPaths(cwd, options));
|
|
380
506
|
} catch (error) {
|
|
381
507
|
return activeAskDiagnostic({
|
|
382
508
|
reason: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -594,8 +720,8 @@ export async function assertCanCompleteCurrentGoal(input: {
|
|
|
594
720
|
sessionId?: string | null;
|
|
595
721
|
}): Promise<void> {
|
|
596
722
|
if (!input.cwd) return;
|
|
597
|
-
const diagnostic = await
|
|
598
|
-
if (["inactive", "
|
|
723
|
+
const diagnostic = await verifyUltragoalDurableCompletionState(input);
|
|
724
|
+
if (["inactive", "active_verified_complete"].includes(diagnostic.state)) return;
|
|
599
725
|
const nudge = await consumeUltragoalNudge({
|
|
600
726
|
cwd: input.cwd,
|
|
601
727
|
surface: "premature_complete",
|
|
@@ -604,7 +730,7 @@ export async function assertCanCompleteCurrentGoal(input: {
|
|
|
604
730
|
});
|
|
605
731
|
if (nudge.nudged) throw new Error(nudge.message);
|
|
606
732
|
throw new Error(
|
|
607
|
-
`${diagnostic.message} Run
|
|
733
|
+
`${diagnostic.message} Run \`gjc ultragoal checkpoint --status complete --quality-gate-json <file>\` first, or record review blockers and rerun verification.`,
|
|
608
734
|
);
|
|
609
735
|
}
|
|
610
736
|
|
|
@@ -2,12 +2,10 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { inflateSync } from "node:zlib";
|
|
5
|
-
import { normalizeGoal } from "../goals/state";
|
|
6
|
-
import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
|
|
7
5
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
8
6
|
import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
|
|
9
7
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
10
|
-
import { DEFAULT_ULTRAGOAL_OBJECTIVE
|
|
8
|
+
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
11
9
|
import { gjcRoot, sessionUltragoalDir } from "./session-layout";
|
|
12
10
|
import { resolveGjcSessionForRead, resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
13
11
|
import { renderUltragoalStatusMarkdown } from "./state-renderer";
|
|
@@ -68,7 +66,6 @@ export interface UltragoalCompletionVerification {
|
|
|
68
66
|
gjcGoalMode: UltragoalGjcGoalMode;
|
|
69
67
|
gjcObjective: string;
|
|
70
68
|
qualityGateHash: string;
|
|
71
|
-
gjcGoalSnapshotHash: string;
|
|
72
69
|
planGeneration: string;
|
|
73
70
|
basis: {
|
|
74
71
|
planHashBeforeCheckpoint: string;
|
|
@@ -174,10 +171,9 @@ const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
|
|
|
174
171
|
const MIN_SUBSTANTIVE_EVIDENCE_WORDS = 5;
|
|
175
172
|
const MIN_SUBSTANTIVE_EVIDENCE_CHARS = 32;
|
|
176
173
|
|
|
177
|
-
const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
|
|
178
|
-
const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
|
|
179
|
-
|
|
180
174
|
const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
|
|
175
|
+
const COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES = new Set<UltragoalGoalStatus>(["active", "failed"]);
|
|
176
|
+
|
|
181
177
|
const NATIVE_STEERING_KINDS = [
|
|
182
178
|
"add_subgoal",
|
|
183
179
|
"split_subgoal",
|
|
@@ -570,7 +566,6 @@ function buildCompletionReceipt(input: {
|
|
|
570
566
|
receiptKind: UltragoalReceiptKind;
|
|
571
567
|
beforeStatus: UltragoalGoalStatus;
|
|
572
568
|
qualityGateJson: JsonObject;
|
|
573
|
-
gjcGoalJson: JsonObject;
|
|
574
569
|
now: string;
|
|
575
570
|
checkpointLedgerEventId: string;
|
|
576
571
|
}): UltragoalCompletionVerification {
|
|
@@ -593,7 +588,6 @@ function buildCompletionReceipt(input: {
|
|
|
593
588
|
gjcGoalMode: input.plan.gjcGoalMode,
|
|
594
589
|
gjcObjective: input.plan.gjcObjective,
|
|
595
590
|
qualityGateHash: hashStructuredValue(input.qualityGateJson),
|
|
596
|
-
gjcGoalSnapshotHash: hashStructuredValue(input.gjcGoalJson),
|
|
597
591
|
planGeneration: generation.planGeneration,
|
|
598
592
|
basis: generation.basis,
|
|
599
593
|
checkpointLedgerEventId: input.checkpointLedgerEventId,
|
|
@@ -2470,93 +2464,24 @@ async function readRequiredCompletionQualityGate(
|
|
|
2470
2464
|
return gate;
|
|
2471
2465
|
}
|
|
2472
2466
|
|
|
2473
|
-
function
|
|
2474
|
-
if (
|
|
2475
|
-
if (
|
|
2476
|
-
const trimmed = value.trim();
|
|
2477
|
-
if (/^\d+$/.test(trimmed)) {
|
|
2478
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
2479
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
2480
|
-
}
|
|
2481
|
-
const parsed = Date.parse(trimmed);
|
|
2482
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
function singleSessionLeafId(entries: readonly SessionEntry[]): string | undefined {
|
|
2486
|
-
if (entries.length === 0) return undefined;
|
|
2487
|
-
const parentIds = new Set(
|
|
2488
|
-
entries.map(entry => entry.parentId).filter((parentId): parentId is string => typeof parentId === "string"),
|
|
2489
|
-
);
|
|
2490
|
-
const leafIds = entries.map(entry => entry.id).filter(id => !parentIds.has(id));
|
|
2491
|
-
return leafIds.length === 1 ? leafIds[0] : undefined;
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
async function readCurrentSessionGjcGoalSnapshot(): Promise<unknown | undefined> {
|
|
2495
|
-
const sessionFile = process.env[GJC_SESSION_FILE_ENV]?.trim();
|
|
2496
|
-
if (!sessionFile) return undefined;
|
|
2497
|
-
const fileEntries = await loadEntriesFromFile(sessionFile);
|
|
2498
|
-
const entries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session");
|
|
2499
|
-
const leafId = singleSessionLeafId(entries);
|
|
2500
|
-
if (!leafId) return undefined;
|
|
2501
|
-
const context = buildSessionContext(entries, leafId);
|
|
2502
|
-
if (context.mode !== "goal" && context.mode !== "goal_paused") return undefined;
|
|
2503
|
-
const goal = normalizeGoal(context.modeData?.goal);
|
|
2504
|
-
return goal ? { goal } : undefined;
|
|
2505
|
-
}
|
|
2506
|
-
async function readGjcGoalSnapshot(input: {
|
|
2507
|
-
cwd: string;
|
|
2508
|
-
value: string | undefined;
|
|
2509
|
-
plan: UltragoalPlan;
|
|
2510
|
-
goal?: UltragoalGoal;
|
|
2511
|
-
required: boolean;
|
|
2512
|
-
errorPrefix: string;
|
|
2513
|
-
allowCompletedLegacyBlocker?: boolean;
|
|
2514
|
-
}): Promise<unknown> {
|
|
2515
|
-
const snapshot = input.value?.trim()
|
|
2516
|
-
? await readStructuredValue(input.cwd, input.value)
|
|
2517
|
-
: await readCurrentSessionGjcGoalSnapshot();
|
|
2518
|
-
if (snapshot === undefined) {
|
|
2519
|
-
if (!input.required) return undefined;
|
|
2520
|
-
throw new Error(
|
|
2521
|
-
`${input.errorPrefix} require an active GJC goal-mode snapshot from the current session or --gjc-goal-json; this is the GJC goal-mode receipt, not the .gjc/ultragoal/goals.json goal record`,
|
|
2522
|
-
);
|
|
2523
|
-
}
|
|
2524
|
-
const snapshotObject = qualityGateObject(snapshot);
|
|
2525
|
-
const detailsObject = qualityGateObject(snapshotObject?.details);
|
|
2526
|
-
const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
|
|
2527
|
-
if (!goalObject)
|
|
2528
|
-
throw new Error(
|
|
2529
|
-
`${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`,
|
|
2530
|
-
);
|
|
2531
|
-
const updatedAt = snapshotUpdatedAtMilliseconds(goalObject.updatedAt);
|
|
2532
|
-
if (!updatedAt)
|
|
2467
|
+
function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
|
|
2468
|
+
if (COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES.has(goal.status)) return;
|
|
2469
|
+
if (goal.status === "pending") {
|
|
2533
2470
|
throw new Error(
|
|
2534
|
-
|
|
2471
|
+
`Cannot checkpoint ${goal.id} as complete while its durable goals.json status is pending; start the goal before completing it.`,
|
|
2535
2472
|
);
|
|
2536
|
-
const nowMilliseconds = Date.now();
|
|
2537
|
-
if (updatedAt < nowMilliseconds - GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS) {
|
|
2538
|
-
throw new Error(`${input.errorPrefix} require a fresh --gjc-goal-json snapshot`);
|
|
2539
|
-
}
|
|
2540
|
-
if (updatedAt > nowMilliseconds + GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS) {
|
|
2541
|
-
throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.updatedAt that is not from the future`);
|
|
2542
|
-
}
|
|
2543
|
-
const objective = typeof goalObject.objective === "string" ? goalObject.objective : "";
|
|
2544
|
-
const expectedObjectives = new Set([input.plan.gjcObjective, ...(input.plan.gjcObjectiveAliases ?? [])]);
|
|
2545
|
-
if (input.plan.gjcGoalMode === "per-story" && input.goal?.objective) {
|
|
2546
|
-
expectedObjectives.add(input.goal.objective);
|
|
2547
|
-
}
|
|
2548
|
-
if (input.allowCompletedLegacyBlocker && goalObject.status === "complete" && !expectedObjectives.has(objective)) {
|
|
2549
|
-
return snapshot;
|
|
2550
2473
|
}
|
|
2551
|
-
if (
|
|
2474
|
+
if (goal.status === "complete") {
|
|
2552
2475
|
throw new Error(
|
|
2553
|
-
|
|
2476
|
+
`Cannot checkpoint ${goal.id} as complete with different evidence because its durable goals.json status is already complete.`,
|
|
2554
2477
|
);
|
|
2555
2478
|
}
|
|
2556
|
-
if (
|
|
2557
|
-
throw new Error(
|
|
2479
|
+
if (goal.status === "superseded") {
|
|
2480
|
+
throw new Error(`Cannot checkpoint ${goal.id} as complete because its durable goals.json status is superseded.`);
|
|
2558
2481
|
}
|
|
2559
|
-
|
|
2482
|
+
throw new Error(
|
|
2483
|
+
`Cannot checkpoint ${goal.id} as complete while its durable goals.json status is ${goal.status}; only active or retryable failed goals can be completed.`,
|
|
2484
|
+
);
|
|
2560
2485
|
}
|
|
2561
2486
|
|
|
2562
2487
|
export async function checkpointUltragoalGoal(input: {
|
|
@@ -2564,7 +2489,6 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
2564
2489
|
goalId: string;
|
|
2565
2490
|
status: UltragoalGoalStatus;
|
|
2566
2491
|
evidence: string;
|
|
2567
|
-
gjcGoalJson?: string;
|
|
2568
2492
|
qualityGateJson?: string;
|
|
2569
2493
|
}): Promise<UltragoalPlan> {
|
|
2570
2494
|
const plan = await readUltragoalPlan(input.cwd);
|
|
@@ -2593,6 +2517,7 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
2593
2517
|
// instead of silently dropping it.
|
|
2594
2518
|
return plan;
|
|
2595
2519
|
}
|
|
2520
|
+
if (input.status === "complete") validateCompleteCheckpointTargetGoal(goal);
|
|
2596
2521
|
const changeSet = input.status === "complete" ? await computeCheckpointChangeSet(input.cwd) : undefined;
|
|
2597
2522
|
const qualityGateJson =
|
|
2598
2523
|
input.status === "complete"
|
|
@@ -2615,25 +2540,6 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
2615
2540
|
}
|
|
2616
2541
|
}
|
|
2617
2542
|
const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, goal, input.status) : null;
|
|
2618
|
-
const gjcGoalJson =
|
|
2619
|
-
input.status === "complete"
|
|
2620
|
-
? await readGjcGoalSnapshot({
|
|
2621
|
-
cwd: input.cwd,
|
|
2622
|
-
value: input.gjcGoalJson,
|
|
2623
|
-
plan,
|
|
2624
|
-
goal,
|
|
2625
|
-
required: true,
|
|
2626
|
-
errorPrefix: "complete checkpoints",
|
|
2627
|
-
})
|
|
2628
|
-
: await readGjcGoalSnapshot({
|
|
2629
|
-
cwd: input.cwd,
|
|
2630
|
-
value: input.gjcGoalJson,
|
|
2631
|
-
plan,
|
|
2632
|
-
goal,
|
|
2633
|
-
required: false,
|
|
2634
|
-
errorPrefix: `${input.status} checkpoints`,
|
|
2635
|
-
allowCompletedLegacyBlocker: input.status === "blocked",
|
|
2636
|
-
});
|
|
2637
2543
|
const pendingCheckpointEventId = crypto.randomUUID();
|
|
2638
2544
|
if (input.status === "complete" && receiptKind && qualityGateJson && !Array.isArray(qualityGateJson)) {
|
|
2639
2545
|
goal.completionVerification = buildCompletionReceipt({
|
|
@@ -2643,7 +2549,6 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
2643
2549
|
receiptKind,
|
|
2644
2550
|
beforeStatus,
|
|
2645
2551
|
qualityGateJson: qualityGateJson as JsonObject,
|
|
2646
|
-
gjcGoalJson: gjcGoalJson as JsonObject,
|
|
2647
2552
|
now,
|
|
2648
2553
|
checkpointLedgerEventId: pendingCheckpointEventId,
|
|
2649
2554
|
});
|
|
@@ -2662,7 +2567,6 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
2662
2567
|
goalId: goal.id,
|
|
2663
2568
|
status: input.status,
|
|
2664
2569
|
evidence,
|
|
2665
|
-
gjcGoalJson,
|
|
2666
2570
|
qualityGateJson,
|
|
2667
2571
|
completionVerification: goal.completionVerification,
|
|
2668
2572
|
});
|
|
@@ -2682,7 +2586,6 @@ export async function checkpointAndContinueUltragoalGoal(input: {
|
|
|
2682
2586
|
goalId: string;
|
|
2683
2587
|
status: UltragoalGoalStatus;
|
|
2684
2588
|
evidence: string;
|
|
2685
|
-
gjcGoalJson?: string;
|
|
2686
2589
|
qualityGateJson?: string;
|
|
2687
2590
|
advanceNext?: boolean;
|
|
2688
2591
|
retryFailed?: boolean;
|
|
@@ -3091,19 +2994,14 @@ export async function recordUltragoalReviewBlockers(input: {
|
|
|
3091
2994
|
title: string;
|
|
3092
2995
|
objective: string;
|
|
3093
2996
|
evidence: string;
|
|
3094
|
-
gjcGoalJson?: string;
|
|
3095
2997
|
}): Promise<UltragoalPlan> {
|
|
3096
2998
|
const objective = input.objective.trim();
|
|
3097
2999
|
if (!objective) throw new Error("record-review-blockers --objective is required");
|
|
3098
|
-
if (!input.gjcGoalJson?.trim()) {
|
|
3099
|
-
throw new Error('record-review-blockers require --gjc-goal-json from a fresh active goal({"op":"get"}) snapshot');
|
|
3100
|
-
}
|
|
3101
3000
|
const plan = await checkpointUltragoalGoal({
|
|
3102
3001
|
cwd: input.cwd,
|
|
3103
3002
|
goalId: input.goalId,
|
|
3104
3003
|
status: "review_blocked",
|
|
3105
3004
|
evidence: input.evidence,
|
|
3106
|
-
gjcGoalJson: input.gjcGoalJson,
|
|
3107
3005
|
});
|
|
3108
3006
|
const persistedPlan = await readUltragoalPlan(input.cwd);
|
|
3109
3007
|
if (persistedPlan?.state_revision !== undefined) plan.state_revision = persistedPlan.state_revision;
|
|
@@ -3551,7 +3449,6 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
3551
3449
|
"--goal-id",
|
|
3552
3450
|
"--status",
|
|
3553
3451
|
"--evidence",
|
|
3554
|
-
"--gjc-goal-json",
|
|
3555
3452
|
"--quality-gate-json",
|
|
3556
3453
|
"--executor-qa-json",
|
|
3557
3454
|
"--executor-qa",
|
|
@@ -3605,15 +3502,12 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
3605
3502
|
" --status=<value> pending|active|complete|failed|blocked|review_blocked|superseded",
|
|
3606
3503
|
" --evidence=<value> Completion or checkpoint evidence text",
|
|
3607
3504
|
" --quality-gate-json=<value> JSON string or path for complete checkpoints",
|
|
3608
|
-
" --gjc-goal-json=<value> Optional JSON/path override for current goal snapshot; omitted complete checkpoints read current session goal state",
|
|
3609
3505
|
" --json Output a machine-readable receipt",
|
|
3610
3506
|
"",
|
|
3611
3507
|
"COMPLETE CHECKPOINT RECEIPTS",
|
|
3612
3508
|
" --quality-gate-json must be an object with architectReview, executorQa, and iteration.",
|
|
3613
3509
|
" executorQa.contractCoverage[] rows require an obligation field; description is not a substitute.",
|
|
3614
|
-
" Complete checkpoints
|
|
3615
|
-
" Explicit --gjc-goal-json values must contain an active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record.",
|
|
3616
|
-
" goal.updatedAt may be epoch milliseconds or an ISO timestamp and must be fresh.",
|
|
3510
|
+
" Complete checkpoints validate the target durable goals.json record before writing a receipt.",
|
|
3617
3511
|
"",
|
|
3618
3512
|
"EXAMPLES",
|
|
3619
3513
|
' $ gjc ultragoal checkpoint --goal-id G001 --status blocked --evidence "waiting on review"',
|
|
@@ -3949,7 +3843,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
3949
3843
|
goalId,
|
|
3950
3844
|
status,
|
|
3951
3845
|
evidence,
|
|
3952
|
-
gjcGoalJson: flagValue(args, "--gjc-goal-json"),
|
|
3953
3846
|
qualityGateJson: flagValue(args, "--quality-gate-json"),
|
|
3954
3847
|
advanceNext: status === "complete",
|
|
3955
3848
|
});
|
|
@@ -3981,7 +3874,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
3981
3874
|
title: flagValue(args, "--title") ?? "Resolve final code-review blockers",
|
|
3982
3875
|
objective: flagValue(args, "--objective") ?? "",
|
|
3983
3876
|
evidence: flagValue(args, "--evidence") ?? "",
|
|
3984
|
-
gjcGoalJson: flagValue(args, "--gjc-goal-json"),
|
|
3985
3877
|
});
|
|
3986
3878
|
const goal = plan.goals.at(-1);
|
|
3987
3879
|
return {
|
|
@@ -1479,14 +1479,6 @@
|
|
|
1479
1479
|
"required": true,
|
|
1480
1480
|
"type": "string"
|
|
1481
1481
|
},
|
|
1482
|
-
{
|
|
1483
|
-
"appliesToVerbs": [
|
|
1484
|
-
"checkpoint",
|
|
1485
|
-
"record-review-blockers"
|
|
1486
|
-
],
|
|
1487
|
-
"name": "gjc-goal-json",
|
|
1488
|
-
"type": "string"
|
|
1489
|
-
},
|
|
1490
1482
|
{
|
|
1491
1483
|
"appliesToVerbs": [
|
|
1492
1484
|
"checkpoint"
|
|
@@ -271,7 +271,6 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
271
271
|
required: true,
|
|
272
272
|
appliesToVerbs: ["checkpoint", "record-review-blockers", "steer", "classify-blocker"],
|
|
273
273
|
},
|
|
274
|
-
{ name: "gjc-goal-json", type: "string", appliesToVerbs: ["checkpoint", "record-review-blockers"] },
|
|
275
274
|
{ name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint"] },
|
|
276
275
|
{ name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
|
|
277
276
|
{ name: "goal-id", type: "string", appliesToVerbs: ["classify-blocker"] },
|