@gajae-code/coding-agent 0.7.6 → 0.7.8
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 +26 -1
- package/README.md +8 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +8 -3
- package/dist/types/config/models-config-schema.d.ts +5 -5
- package/dist/types/config/settings-schema.d.ts +123 -9
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +7 -7
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +0 -1
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/update-cli.ts +155 -13
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +18 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +58 -7
- package/src/config/models-config-schema.ts +10 -1
- package/src/config/settings-schema.ts +127 -10
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/gjc-runtime/launch-tmux.ts +34 -8
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/state-renderer.ts +13 -0
- package/src/gjc-runtime/team-runtime.ts +21 -6
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/gjc-runtime/ultragoal-guard.ts +167 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/model-selector.ts +182 -5
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +491 -5
- package/src/modes/components/status-line/segments.ts +7 -18
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +69 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +18 -4
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/telegram-daemon-cli.ts +112 -9
- package/src/notifications/telegram-daemon.ts +4 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +118 -5
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/index.ts +2 -8
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
|
@@ -1113,17 +1113,6 @@
|
|
|
1113
1113
|
return html;
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
-
function renderInspectImage(name, args, result, ctx) {
|
|
1117
|
-
const p = str(args.path == null ? args.url : args.path) || '';
|
|
1118
|
-
let html = toolHead('inspect_image', escapeHtml(shortenPath(p)));
|
|
1119
|
-
if (result) {
|
|
1120
|
-
html += ctx.renderResultImages();
|
|
1121
|
-
const output = ctx.getResultText();
|
|
1122
|
-
if (output) html += formatExpandableOutput(output, 8);
|
|
1123
|
-
}
|
|
1124
|
-
return html;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
1116
|
function renderGenerateImage(name, args, result, ctx) {
|
|
1128
1117
|
const subject = str(args.subject) || '';
|
|
1129
1118
|
const badges = args.aspect_ratio ? [String(args.aspect_ratio)] : null;
|
|
@@ -1544,7 +1533,6 @@
|
|
|
1544
1533
|
debug: renderDebug,
|
|
1545
1534
|
puppeteer: renderBrowser,
|
|
1546
1535
|
browser: renderBrowser,
|
|
1547
|
-
inspect_image: renderInspectImage,
|
|
1548
1536
|
generate_image: renderGenerateImage,
|
|
1549
1537
|
ask: renderAsk,
|
|
1550
1538
|
resolve: renderResolve,
|
|
@@ -6,6 +6,7 @@ import type { Args } from "../cli/args";
|
|
|
6
6
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
7
7
|
import { GJC_COORDINATOR_SESSION_ID_ENV, GJC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
8
8
|
import {
|
|
9
|
+
buildGjcTmuxExactSessionTarget,
|
|
9
10
|
buildGjcTmuxProfileCommands,
|
|
10
11
|
buildGjcTmuxSessionName,
|
|
11
12
|
buildGjcTmuxSessionSlug,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope, type GjcTmuxSessionStatus } from "./tmux-sessions";
|
|
22
23
|
|
|
23
24
|
export {
|
|
25
|
+
buildGjcTmuxExactSessionTarget,
|
|
24
26
|
buildGjcTmuxProfileCommands,
|
|
25
27
|
GJC_DEFAULT_TMUX_SESSION,
|
|
26
28
|
GJC_TMUX_COMMAND_ENV,
|
|
@@ -201,12 +203,24 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
|
|
|
201
203
|
const envLines = Object.entries({ [GJC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
|
|
202
204
|
([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
|
|
203
205
|
);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
// The inner `&` invocation must wrap the resolved gjc command in a
|
|
207
|
+
// script-block so the trailing PowerShell exec-policy flags from the
|
|
208
|
+
// outer invocation are not forwarded into the bun / node / .bat
|
|
209
|
+
// binary the gjc command resolves to. Without the wrapping, the flags
|
|
210
|
+
// reach the runtime binary and cause an immediate failure that closes
|
|
211
|
+
// the psmux pane before the gjc --tmux attach can land.
|
|
212
|
+
const resolvedCommand = command.map(powershellQuote).join(" ");
|
|
213
|
+
const innerArgs = stripRootTmuxFlag(rawArgs).map(powershellQuote).join(" ");
|
|
214
|
+
const invocation = `& { ${resolvedCommand} ${innerArgs} }`;
|
|
207
215
|
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
208
216
|
const script = [...envLines, invocation, exitLine].join("\n");
|
|
209
|
-
|
|
217
|
+
// PowerShell -EncodedCommand requires a UTF-16LE BOM (0xFF 0xFE) at the
|
|
218
|
+
// start of the decoded buffer; without it pwsh may misinterpret the
|
|
219
|
+
// leading bytes and reject the script with a parse error, which would
|
|
220
|
+
// kill the psmux pane before the gjc --tmux attach could land.
|
|
221
|
+
const bom = Buffer.from([0xff, 0xfe]);
|
|
222
|
+
const body = Buffer.from(script, "utf16le");
|
|
223
|
+
const encodedCommand = Buffer.concat([bom, body]).toString("base64");
|
|
210
224
|
return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
|
|
211
225
|
}
|
|
212
226
|
|
|
@@ -395,7 +409,11 @@ function readCurrentBranch(cwd: string): string | null {
|
|
|
395
409
|
}
|
|
396
410
|
|
|
397
411
|
function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
|
|
398
|
-
spawnSync(
|
|
412
|
+
spawnSync(
|
|
413
|
+
plan.tmuxCommand,
|
|
414
|
+
["kill-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env: options.env })],
|
|
415
|
+
options,
|
|
416
|
+
);
|
|
399
417
|
}
|
|
400
418
|
function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
401
419
|
if (result.signalCode === "SIGHUP") return true;
|
|
@@ -499,7 +517,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
499
517
|
};
|
|
500
518
|
|
|
501
519
|
if (plan.attachSessionName) {
|
|
502
|
-
const attached = spawnSync(
|
|
520
|
+
const attached = spawnSync(
|
|
521
|
+
plan.tmuxCommand,
|
|
522
|
+
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.attachSessionName, { env })],
|
|
523
|
+
options,
|
|
524
|
+
);
|
|
503
525
|
if (attached.exitCode === 0) return true;
|
|
504
526
|
}
|
|
505
527
|
|
|
@@ -510,7 +532,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
510
532
|
buildGjcTmuxWindowTitle(plan.project ?? plan.cwd, plan.branch),
|
|
511
533
|
spawnSync,
|
|
512
534
|
options,
|
|
513
|
-
|
|
535
|
+
buildGjcTmuxExactSessionTarget(plan.sessionName, { env }),
|
|
514
536
|
);
|
|
515
537
|
|
|
516
538
|
const profile = applyGjcTmuxProfile({
|
|
@@ -535,7 +557,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
535
557
|
}
|
|
536
558
|
}
|
|
537
559
|
if (created.exitCode !== 0) return false;
|
|
538
|
-
const attached = spawnSync(
|
|
560
|
+
const attached = spawnSync(
|
|
561
|
+
plan.tmuxCommand,
|
|
562
|
+
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
563
|
+
options,
|
|
564
|
+
);
|
|
539
565
|
if (attached.exitCode === 0) return true;
|
|
540
566
|
if (isTmuxAttachDisconnectError(attached)) {
|
|
541
567
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
|
|
@@ -118,6 +118,20 @@ function findWorktreeByPath(entries: GitWorktreeEntry[], worktreePath: string):
|
|
|
118
118
|
return entries.find(entry => path.resolve(entry.path) === resolved) ?? null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function describeWorktreeEntry(entry: GitWorktreeEntry): string {
|
|
122
|
+
return entry.detached ? `detached HEAD ${entry.head}` : (entry.branchRef ?? `HEAD ${entry.head}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatWorktreeTargetMismatch(plan: GjcLaunchWorktreePlan, existing: GitWorktreeEntry): string {
|
|
126
|
+
const expected = plan.detached ? `detached HEAD ${plan.baseRef}` : `branch refs/heads/${plan.branchName ?? ""}`;
|
|
127
|
+
return [
|
|
128
|
+
`worktree_target_mismatch:${plan.worktreePath}`,
|
|
129
|
+
`GJC launch worktree target is already registered for ${describeWorktreeEntry(existing)}, but this launch expects ${expected}.`,
|
|
130
|
+
`Path: ${plan.worktreePath}`,
|
|
131
|
+
"Refusing to delete or reuse the conflicting worktree automatically. Safe remediation: inspect the path, commit/stash any work, then remove or prune the stale worktree with git worktree remove <path> when it is no longer needed, or choose a different --worktree name.",
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
function hasBranchInUse(entries: GitWorktreeEntry[], branchName: string, worktreePath: string): boolean {
|
|
122
136
|
const expectedRef = `refs/heads/${branchName}`;
|
|
123
137
|
const resolvedPath = path.resolve(worktreePath);
|
|
@@ -228,7 +242,7 @@ export function ensureLaunchWorktree(
|
|
|
228
242
|
let dirty = isWorktreeDirty(plan.worktreePath);
|
|
229
243
|
if (plan.detached) {
|
|
230
244
|
if (!existingAtPath.detached) {
|
|
231
|
-
throw new Error(
|
|
245
|
+
throw new Error(formatWorktreeTargetMismatch(plan, existingAtPath));
|
|
232
246
|
}
|
|
233
247
|
if (existingAtPath.head !== plan.baseRef) {
|
|
234
248
|
if (dirty) throw new Error(`worktree_dirty:${plan.worktreePath}`);
|
|
@@ -236,7 +250,7 @@ export function ensureLaunchWorktree(
|
|
|
236
250
|
dirty = false;
|
|
237
251
|
}
|
|
238
252
|
} else if (existingAtPath.branchRef !== expectedBranchRef) {
|
|
239
|
-
throw new Error(
|
|
253
|
+
throw new Error(formatWorktreeTargetMismatch(plan, existingAtPath));
|
|
240
254
|
}
|
|
241
255
|
return {
|
|
242
256
|
...plan,
|
|
@@ -256,6 +256,11 @@ export function renderUltragoalStatusMarkdown(summary: {
|
|
|
256
256
|
currentGoal?: { id: string; status: string; title?: string; objective?: string };
|
|
257
257
|
counts: Record<string, number>;
|
|
258
258
|
goals: unknown[];
|
|
259
|
+
nudgeBudget?: number;
|
|
260
|
+
nudgeCount?: number;
|
|
261
|
+
nudgeRemaining?: number;
|
|
262
|
+
nudgeGoalId?: string;
|
|
263
|
+
nudgeTargetKind?: string;
|
|
259
264
|
}): string {
|
|
260
265
|
if (!summary.exists)
|
|
261
266
|
return `# ultragoal status\n\n- status: missing\n- No ultragoal plan found at ${summary.paths.goalsPath}. Run \`gjc ultragoal create-goals --brief "..."\` first.\n`;
|
|
@@ -270,6 +275,14 @@ export function renderUltragoalStatusMarkdown(summary: {
|
|
|
270
275
|
];
|
|
271
276
|
if (summary.gjcObjective) lines.push(`- objective: ${summary.gjcObjective}`);
|
|
272
277
|
if (summary.currentGoal) lines.push(`- current: ${summary.currentGoal.id} (${summary.currentGoal.status})`);
|
|
278
|
+
if (summary.nudgeBudget !== undefined && summary.nudgeGoalId) {
|
|
279
|
+
const used = summary.nudgeCount ?? 0;
|
|
280
|
+
const remaining = summary.nudgeRemaining ?? Math.max(0, summary.nudgeBudget - used);
|
|
281
|
+
const target = summary.nudgeTargetKind ? ` target=${summary.nudgeTargetKind}` : "";
|
|
282
|
+
lines.push(
|
|
283
|
+
`- nudge: ${summary.nudgeGoalId} ${used}/${summary.nudgeBudget} used (${remaining} remaining)${target}`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
273
286
|
lines.push(`- goals_path: ${summary.paths.goalsPath}`);
|
|
274
287
|
if (summary.paths.ledgerPath) lines.push(`- ledger_path: ${summary.paths.ledgerPath}`);
|
|
275
288
|
return `${lines.join("\n")}\n`;
|
|
@@ -1896,13 +1896,27 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
|
|
|
1896
1896
|
}
|
|
1897
1897
|
return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
|
|
1898
1898
|
}
|
|
1899
|
-
export function resolveGjcWorkerCommand(
|
|
1899
|
+
export function resolveGjcWorkerCommand(
|
|
1900
|
+
cwd = process.cwd(),
|
|
1901
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1902
|
+
platform: NodeJS.Platform = process.platform,
|
|
1903
|
+
argv: string[] = process.argv,
|
|
1904
|
+
execPath = process.execPath,
|
|
1905
|
+
): string {
|
|
1900
1906
|
const explicit = env.GJC_TEAM_WORKER_COMMAND?.trim();
|
|
1901
1907
|
if (explicit) return explicit;
|
|
1902
|
-
const entrypoint =
|
|
1903
|
-
if (entrypoint
|
|
1904
|
-
|
|
1905
|
-
|
|
1908
|
+
const entrypoint = argv[1];
|
|
1909
|
+
if (!entrypoint) return "gjc";
|
|
1910
|
+
const pathModule = platform === "win32" ? path.win32 : path;
|
|
1911
|
+
const resolvedEntrypoint = pathModule.isAbsolute(entrypoint) ? entrypoint : pathModule.resolve(cwd, entrypoint);
|
|
1912
|
+
if (platform === "win32") {
|
|
1913
|
+
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs"))
|
|
1914
|
+
return `${powershellQuote(execPath)} ${powershellQuote(resolvedEntrypoint)}`;
|
|
1915
|
+
if (pathModule.basename(entrypoint).startsWith("gjc")) return powershellQuote(resolvedEntrypoint);
|
|
1916
|
+
return "gjc";
|
|
1917
|
+
}
|
|
1918
|
+
if (entrypoint.endsWith(".ts")) return `${shellQuote(execPath)} ${shellQuote(resolvedEntrypoint)}`;
|
|
1919
|
+
if (path.basename(entrypoint).startsWith("gjc")) return shellQuote(path.resolve(cwd, entrypoint));
|
|
1906
1920
|
return "gjc";
|
|
1907
1921
|
}
|
|
1908
1922
|
/** @internal Exported for unit tests. */
|
|
@@ -1937,7 +1951,8 @@ export function buildWorkerCommand(
|
|
|
1937
1951
|
...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
|
|
1938
1952
|
];
|
|
1939
1953
|
const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
|
|
1940
|
-
|
|
1954
|
+
const invocation = platform === "win32" ? `& ${config.worker_command}` : config.worker_command;
|
|
1955
|
+
return `${joined} ${invocation} ${quote(prompt)}`;
|
|
1941
1956
|
}
|
|
1942
1957
|
interface GjcTeamInitialLane {
|
|
1943
1958
|
label: string;
|
|
@@ -86,6 +86,22 @@ export function buildGjcTmuxExactOptionTarget(
|
|
|
86
86
|
return `=${sessionName}:`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Build the exact-session target for tmux *session-scoped* commands such as
|
|
91
|
+
* `attach-session` and `kill-session`. Native tmux accepts `=NAME` for an
|
|
92
|
+
* exact session match, but Windows psmux 3.3.x rejects that target form for
|
|
93
|
+
* session commands even though the bare `NAME` resolves. Keep native tmux on
|
|
94
|
+
* exact targets and intentionally use the bare session name for psmux.
|
|
95
|
+
*/
|
|
96
|
+
export function buildGjcTmuxExactSessionTarget(
|
|
97
|
+
sessionName: string,
|
|
98
|
+
opts: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; binary?: ResolvedTmuxBinary } = {},
|
|
99
|
+
): string {
|
|
100
|
+
const binary = opts.binary ?? resolveGjcTmuxBinary({ env: opts.env, platform: opts.platform });
|
|
101
|
+
if (binary.isPsmux) return sessionName;
|
|
102
|
+
return `=${sessionName}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
export const GJC_TMUX_UNTAGGED_REASON = "gjc_tmux_session_untagged";
|
|
90
106
|
|
|
91
107
|
export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveGjcTmuxBinary } from "./psmux-detect";
|
|
2
2
|
import {
|
|
3
3
|
buildGjcTmuxExactOptionTarget,
|
|
4
|
+
buildGjcTmuxExactSessionTarget,
|
|
4
5
|
buildGjcTmuxProfileCommands,
|
|
5
6
|
buildGjcTmuxSessionName,
|
|
6
7
|
buildGjcTmuxUntaggedSessionError,
|
|
@@ -60,7 +61,7 @@ function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
|
|
|
60
61
|
|
|
61
62
|
function tryKillSession(sessionName: string, env: NodeJS.ProcessEnv): void {
|
|
62
63
|
try {
|
|
63
|
-
runTmux(["kill-session", "-t",
|
|
64
|
+
runTmux(["kill-session", "-t", buildGjcTmuxExactSessionTarget(sessionName, { env })], env);
|
|
64
65
|
} catch {
|
|
65
66
|
// Best-effort cleanup only; preserve the original create/tag failure.
|
|
66
67
|
}
|
|
@@ -360,7 +361,7 @@ export function removeGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv
|
|
|
360
361
|
if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE) {
|
|
361
362
|
throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
|
|
362
363
|
}
|
|
363
|
-
runTmux(["kill-session", "-t",
|
|
364
|
+
runTmux(["kill-session", "-t", buildGjcTmuxExactSessionTarget(session.name, { env })], env);
|
|
364
365
|
return session;
|
|
365
366
|
}
|
|
366
367
|
|
|
@@ -404,18 +405,21 @@ export function forceCloseGjcTmuxSession(
|
|
|
404
405
|
}
|
|
405
406
|
}
|
|
406
407
|
// Intentionally NOT refusing live/attached panes — force-close is hard-kill.
|
|
407
|
-
runTmux(["kill-session", "-t",
|
|
408
|
+
runTmux(["kill-session", "-t", buildGjcTmuxExactSessionTarget(session.name, { env })], env);
|
|
408
409
|
return session;
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
export function attachGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
|
|
412
413
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
413
414
|
const tmuxCommand = resolveGjcTmuxCommand(env);
|
|
414
|
-
const result = Bun.spawnSync(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
415
|
+
const result = Bun.spawnSync(
|
|
416
|
+
[tmuxCommand, "attach-session", "-t", buildGjcTmuxExactSessionTarget(session.name, { env })],
|
|
417
|
+
{
|
|
418
|
+
stdin: "inherit",
|
|
419
|
+
stdout: "inherit",
|
|
420
|
+
stderr: "inherit",
|
|
421
|
+
env,
|
|
422
|
+
},
|
|
423
|
+
);
|
|
420
424
|
process.exit(result.exitCode ?? 1);
|
|
421
425
|
}
|
|
@@ -8,9 +8,13 @@ import {
|
|
|
8
8
|
hashStructuredValue,
|
|
9
9
|
readUltragoalLedger,
|
|
10
10
|
readUltragoalPlan,
|
|
11
|
+
recordUltragoalNudgeIfBudgetRemaining,
|
|
12
|
+
resolveUltragoalNudgeBudget,
|
|
13
|
+
selectUltragoalNudgeTarget,
|
|
11
14
|
type UltragoalCompletionVerification,
|
|
12
15
|
type UltragoalGoal,
|
|
13
16
|
type UltragoalLedgerEvent,
|
|
17
|
+
type UltragoalNudgeSurface,
|
|
14
18
|
type UltragoalPaths,
|
|
15
19
|
type UltragoalPlan,
|
|
16
20
|
type UltragoalReceiptKind,
|
|
@@ -494,6 +498,96 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
|
|
|
494
498
|
});
|
|
495
499
|
}
|
|
496
500
|
|
|
501
|
+
const NUDGE_SURFACE_LABEL: Record<UltragoalNudgeSurface, string> = {
|
|
502
|
+
pause: "pausing the goal",
|
|
503
|
+
drop: "dropping the run",
|
|
504
|
+
ask: "asking the user",
|
|
505
|
+
premature_complete: "finishing the story early",
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const NUDGE_SURFACE_REASON: Record<UltragoalNudgeSurface, string> = {
|
|
509
|
+
pause: "an active story still has resolvable work",
|
|
510
|
+
drop: "the aggregate run still has unfinished stories",
|
|
511
|
+
ask: "the decision can be resolved as durable story work",
|
|
512
|
+
premature_complete: "story verification is not yet satisfied",
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Escalating per-attempt refusal text. Deliberately avoids every
|
|
517
|
+
* `isUltragoalBypassPrompt` trigger: no `update_goal(`, no "skip/weaken verification",
|
|
518
|
+
* no "mark ... complete", no "--status complete", and the word "complete" never
|
|
519
|
+
* appears (so the `goal` ... `complete` proximity rule cannot match).
|
|
520
|
+
*/
|
|
521
|
+
export function formatUltragoalNudgeMessage(input: {
|
|
522
|
+
surface: UltragoalNudgeSurface;
|
|
523
|
+
attempt: number;
|
|
524
|
+
budget: number;
|
|
525
|
+
goalId: string;
|
|
526
|
+
}): string {
|
|
527
|
+
const label = NUDGE_SURFACE_LABEL[input.surface];
|
|
528
|
+
const reason = NUDGE_SURFACE_REASON[input.surface];
|
|
529
|
+
return [
|
|
530
|
+
`Ultragoal try-harder nudge (${input.attempt}/${input.budget}) for ${input.goalId}: ${label} was refused before the normal gate.`,
|
|
531
|
+
`Resolving this is part of the goal, not a reason to stop. Try a different approach first: inspect the failure, run a focused test or replay, find local credentials/config if access is the blocker, split the obstacle with \`gjc ultragoal steer --kind add_subgoal\`, delegate an executor, or record concrete review blockers.`,
|
|
532
|
+
`Reason: ${reason}.`,
|
|
533
|
+
].join("\n");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Consume one nudge for a guarded give-up attempt. MUST only be called from assert/
|
|
538
|
+
* consume paths, never from read-style `is...Blocked` diagnostics. Returns a nudge
|
|
539
|
+
* message while the per-story budget remains; otherwise reports not-nudged so the
|
|
540
|
+
* caller falls through to today's gate.
|
|
541
|
+
*/
|
|
542
|
+
async function consumeUltragoalNudge(input: {
|
|
543
|
+
cwd: string;
|
|
544
|
+
surface: UltragoalNudgeSurface;
|
|
545
|
+
currentGoal?: CurrentGoalLike | null;
|
|
546
|
+
sessionId?: string | null;
|
|
547
|
+
}): Promise<{ nudged: true; message: string } | { nudged: false }> {
|
|
548
|
+
const sessionId = input.sessionId?.trim() || (await ultragoalReadPaths(input.cwd)).sessionId;
|
|
549
|
+
if (!sessionId) return { nudged: false };
|
|
550
|
+
const plan = await readUltragoalPlan(input.cwd, sessionId);
|
|
551
|
+
if (!plan) return { nudged: false };
|
|
552
|
+
const target = selectUltragoalNudgeTarget(plan, { currentGoalObjective: input.currentGoal?.objective });
|
|
553
|
+
if (!target) return { nudged: false };
|
|
554
|
+
const { budget } = await resolveUltragoalNudgeBudget(input.cwd);
|
|
555
|
+
const outcome = await recordUltragoalNudgeIfBudgetRemaining({
|
|
556
|
+
cwd: input.cwd,
|
|
557
|
+
sessionId,
|
|
558
|
+
target,
|
|
559
|
+
surface: input.surface,
|
|
560
|
+
budget,
|
|
561
|
+
reason: NUDGE_SURFACE_REASON[input.surface],
|
|
562
|
+
...(input.currentGoal?.objective ? { currentGoalObjective: input.currentGoal.objective } : {}),
|
|
563
|
+
});
|
|
564
|
+
if (outcome.nudged) {
|
|
565
|
+
return {
|
|
566
|
+
nudged: true,
|
|
567
|
+
message: formatUltragoalNudgeMessage({
|
|
568
|
+
surface: input.surface,
|
|
569
|
+
attempt: outcome.attempt,
|
|
570
|
+
budget: outcome.budget,
|
|
571
|
+
goalId: outcome.goalId,
|
|
572
|
+
}),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return { nudged: false };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Assert-path entry for the `ask` surface (the ask guard lives in another module).
|
|
580
|
+
* Resolves the active (leader) Ultragoal session so subagent/headless asks consume
|
|
581
|
+
* the leader run's budget rather than a fresh child ledger.
|
|
582
|
+
*/
|
|
583
|
+
export async function consumeUltragoalAskNudge(
|
|
584
|
+
cwd: string,
|
|
585
|
+
sessionId?: string | null,
|
|
586
|
+
): Promise<{ nudged: true; message: string } | { nudged: false }> {
|
|
587
|
+
if (!cwd) return { nudged: false };
|
|
588
|
+
return consumeUltragoalNudge({ cwd, surface: "ask", sessionId });
|
|
589
|
+
}
|
|
590
|
+
|
|
497
591
|
export async function assertCanCompleteCurrentGoal(input: {
|
|
498
592
|
cwd: string;
|
|
499
593
|
currentGoal?: CurrentGoalLike | null;
|
|
@@ -502,6 +596,13 @@ export async function assertCanCompleteCurrentGoal(input: {
|
|
|
502
596
|
if (!input.cwd) return;
|
|
503
597
|
const diagnostic = await readUltragoalVerificationState(input);
|
|
504
598
|
if (["inactive", "unrelated_goal", "active_verified_complete"].includes(diagnostic.state)) return;
|
|
599
|
+
const nudge = await consumeUltragoalNudge({
|
|
600
|
+
cwd: input.cwd,
|
|
601
|
+
surface: "premature_complete",
|
|
602
|
+
currentGoal: input.currentGoal,
|
|
603
|
+
sessionId: input.sessionId,
|
|
604
|
+
});
|
|
605
|
+
if (nudge.nudged) throw new Error(nudge.message);
|
|
505
606
|
throw new Error(
|
|
506
607
|
`${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.`,
|
|
507
608
|
);
|
|
@@ -558,6 +659,10 @@ export async function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPau
|
|
|
558
659
|
}
|
|
559
660
|
|
|
560
661
|
export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
|
|
662
|
+
if (cwd) {
|
|
663
|
+
const nudge = await consumeUltragoalNudge({ cwd, surface: "pause" });
|
|
664
|
+
if (nudge.nudged) throw new Error(nudge.message);
|
|
665
|
+
}
|
|
561
666
|
const diagnostic = await isUltragoalPauseBlocked(cwd);
|
|
562
667
|
if (!diagnostic.blocked) return;
|
|
563
668
|
throw new Error(
|
|
@@ -568,3 +673,65 @@ export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
|
|
|
568
673
|
].join("\n"),
|
|
569
674
|
);
|
|
570
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Guard `goal({"op":"drop"})` during an active Ultragoal run. A *real give-up* (an
|
|
678
|
+
* aggregate run still mid-flight with incomplete required stories) is nudged while
|
|
679
|
+
* budget remains; once exhausted it falls through to today's drop behavior. Legitimate
|
|
680
|
+
* aggregate-reset drops — no durable run, unrelated goal, an already dropped/stale
|
|
681
|
+
* aggregate, or an all-stories-complete run — are never nudged. If durable state
|
|
682
|
+
* exists but cannot be read to classify the drop, fail closed.
|
|
683
|
+
*/
|
|
684
|
+
export async function assertUltragoalDropAllowed(input: {
|
|
685
|
+
cwd: string;
|
|
686
|
+
currentGoal?: CurrentGoalLike | null;
|
|
687
|
+
sessionId?: string | null;
|
|
688
|
+
}): Promise<void> {
|
|
689
|
+
if (!input.cwd) return;
|
|
690
|
+
let paths: UltragoalPaths;
|
|
691
|
+
let sessionId: string | null;
|
|
692
|
+
try {
|
|
693
|
+
({ paths, sessionId } = await ultragoalReadPaths(input.cwd));
|
|
694
|
+
} catch (error) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Unable to classify Ultragoal drop (durable state unreadable): ${error instanceof Error ? error.message : String(error)}`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (sessionId === null) return;
|
|
700
|
+
try {
|
|
701
|
+
await fs.stat(paths.dir);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (isEnoent(error)) return;
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Unable to classify Ultragoal drop (durable state present but unreadable): ${error instanceof Error ? error.message : String(error)}`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
let plan: UltragoalPlan | null;
|
|
709
|
+
try {
|
|
710
|
+
plan = await readUltragoalPlan(input.cwd, sessionId);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`Unable to classify Ultragoal drop (goals.json unreadable): ${error instanceof Error ? error.message : String(error)}`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (!plan) {
|
|
717
|
+
throw new Error("Unable to classify Ultragoal drop: durable state exists but goals.json is missing or empty.");
|
|
718
|
+
}
|
|
719
|
+
// Out of scope: per-story mode drops keep today's behavior.
|
|
720
|
+
if (plan.gjcGoalMode !== "aggregate") return;
|
|
721
|
+
// A real give-up requires an active aggregate goal to actually be abandoned. With no
|
|
722
|
+
// current goal-mode goal (or a non-active one), `drop` is a no-op/reset before a fresh
|
|
723
|
+
// `create`, never a give-up — so it is left un-nudged.
|
|
724
|
+
if (!input.currentGoal) return;
|
|
725
|
+
if (input.currentGoal.status !== "active") return;
|
|
726
|
+
// Unrelated active goal: not this aggregate run.
|
|
727
|
+
if (!objectiveMatches(input.currentGoal.objective, plan)) return;
|
|
728
|
+
// All required stories complete: a legitimate reset, not a give-up.
|
|
729
|
+
if (getUltragoalRunCompletionState(plan).allComplete) return;
|
|
730
|
+
const nudge = await consumeUltragoalNudge({
|
|
731
|
+
cwd: input.cwd,
|
|
732
|
+
surface: "drop",
|
|
733
|
+
currentGoal: input.currentGoal,
|
|
734
|
+
sessionId,
|
|
735
|
+
});
|
|
736
|
+
if (nudge.nudged) throw new Error(nudge.message);
|
|
737
|
+
}
|