@gajae-code/coding-agent 0.6.1 → 0.6.4
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 +54 -0
- package/README.md +73 -1
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/lsp/startup-events.d.ts +1 -0
- package/dist/types/modes/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +0 -7
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli/update-cli.ts +53 -3
- package/src/commands/launch.ts +1 -1
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +17 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -0
- package/src/exec/bash-executor.ts +3 -1
- package/src/gjc-runtime/launch-tmux.ts +62 -14
- package/src/gjc-runtime/state-runtime.ts +22 -14
- package/src/gjc-runtime/state-writer.ts +21 -1
- package/src/gjc-runtime/tmux-sessions.ts +36 -1
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/lsp/startup-events.ts +24 -0
- package/src/modes/components/welcome.ts +42 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +27 -19
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- package/src/session/agent-session.ts +28 -20
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/skill-state/active-state.ts +53 -30
- package/src/skill-state/deep-interview-mutation-guard.ts +238 -30
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- package/src/tools/ast-edit.ts +2 -2
- package/src/utils/edit-mode.ts +1 -1
|
@@ -1065,6 +1065,23 @@ export const SETTINGS_SCHEMA = {
|
|
|
1065
1065
|
},
|
|
1066
1066
|
},
|
|
1067
1067
|
|
|
1068
|
+
"startup.welcomeBannerMode": {
|
|
1069
|
+
type: "enum",
|
|
1070
|
+
values: ["auto", "unicode", "square", "ascii"] as const,
|
|
1071
|
+
default: "auto",
|
|
1072
|
+
ui: {
|
|
1073
|
+
tab: "interaction",
|
|
1074
|
+
label: "Welcome Banner Mode",
|
|
1075
|
+
description: "Logo style for the startup welcome screen",
|
|
1076
|
+
options: [
|
|
1077
|
+
{ value: "auto", label: "Auto", description: "Use the rounded Unicode logo" },
|
|
1078
|
+
{ value: "unicode", label: "Unicode", description: "Force the rounded Unicode logo" },
|
|
1079
|
+
{ value: "square", label: "Square Unicode", description: "Force the square-corner Unicode fallback" },
|
|
1080
|
+
{ value: "ascii", label: "ASCII", description: "Force the ASCII-safe logo" },
|
|
1081
|
+
],
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
|
|
1068
1085
|
"startup.checkUpdate": {
|
|
1069
1086
|
type: "boolean",
|
|
1070
1087
|
default: true,
|
|
@@ -123,6 +123,8 @@ Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThreshold
|
|
|
123
123
|
- Final specs MUST resolve to `.gjc/specs/deep-interview-{slug}.md` exactly.
|
|
124
124
|
- Write final specs and all ephemeral interview artifacts through the active GJC workflow/state CLI when available.
|
|
125
125
|
- Direct `.gjc/` file edits are forbidden unless an explicit force override is active; do not use `write`, `edit`, or `ast_edit` against `.gjc/specs`, `.gjc/plans`, `.gjc/state`, or other `.gjc/` paths during normal workflow operation.
|
|
126
|
+
- Preferred: pass the spec markdown **inline** to the native deep-interview write command (`--write … --spec "<markdown>"`) — no scratch file is needed. The CLI is the only sanctioned writer for `.gjc/specs`.
|
|
127
|
+
- Only if a spec is too large to pass inline, stage it with the `write` tool to a system temp directory (`os.tmpdir()`/`$TMPDIR`, `/tmp`, `/var/tmp`) outside the project tree, then pass that path to `--spec`. The planning phase-boundary block tolerates these neutral temp writes; never stage interview artifacts inside the repo or under `.gjc/`, and do not improvise repo-relative scratch files.
|
|
126
128
|
|
|
127
129
|
4. **Initialize state** via `gjc state write`:
|
|
128
130
|
|
|
@@ -495,7 +497,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
|
|
|
495
497
|
- Apply `language.instruction` when present so user-facing prose in the spec preserves the session language; keep code identifiers, file paths, commands, JSON/settings keys, and quoted source text unchanged.
|
|
496
498
|
- Apply the self-proofread once to newly generated spec prose before persistence, including generated natural-language table cells such as coverage notes, while preserving transcript answers, quoted/source text, code identifiers, file paths, commands, JSON/settings keys, table structure/fixed labels, and `.gjc/specs/deep-interview-{slug}.md` unchanged.
|
|
497
499
|
2. **Write the final spec through the workflow CLI**: persist the artifact at `.gjc/specs/deep-interview-{slug}.md`
|
|
498
|
-
- Always use this exact final spec path.
|
|
500
|
+
- Always use this exact final spec path. Prefer passing the spec markdown **inline** as the `--spec` value; only when it is too large to pass inline, stage it as a file in a system temp directory (`os.tmpdir()`/`$TMPDIR`, `/tmp`, `/var/tmp`) outside the project tree and pass that path — never write scratch specs to the repo root, the project tree, or `.gjc/`.
|
|
499
501
|
- Use the native deep-interview write command with `--write --stage final --slug {slug} --spec <markdown-or-path> [--json]` for artifact and state persistence; direct `.gjc/` file edits are forbidden unless an explicit force override is active.
|
|
500
502
|
- Persist the final `spec_path` in state when available so downstream skills and resumed sessions can pass the artifact path explicitly.
|
|
501
503
|
- If the user preselected the deliberate ralplan path, use the native deep-interview write command with `--write --stage final --slug {slug} --spec <markdown-or-path> --deliberate [--json]` so the final spec is persisted before deep-interview hands off to ralplan.
|
|
@@ -45,6 +45,8 @@ gjc ralplan --write --stage <type> --stage_n <N> --artifact "markdown file path
|
|
|
45
45
|
|
|
46
46
|
Use stage values that match the producer or artifact kind, such as `planner`, `architect`, `critic`, `revision`, `adr`, or `final`. Increment `--stage_n` for each consensus-loop pass. The `--artifact` value may be either a markdown file path prepared outside `.gjc/` for ingestion or the markdown content string itself. The native `--write` handler persists markdown under `.gjc/plans/ralplan/<run-id>/stage-<NN>-<stage>.md`, maintains an `index.jsonl` audit log, and for `final` stages additionally writes a `pending-approval.md` copy. Direct `write`, `edit`, or `ast_edit` calls against `.gjc/specs`, `.gjc/plans`, `.gjc/state`, or any other `.gjc/` path are forbidden unless an explicit force override is active.
|
|
47
47
|
|
|
48
|
+
While ralplan is active it is a pre-approval planning phase: product-code mutation tools (`write`/`edit`/`ast_edit`) and product-mutating `bash` (e.g. `tee src/...`, redirects into the project tree) are blocked, exactly like deep-interview. Prefer passing the `--artifact` markdown **inline** (the content string) so no scratch file is needed; this is mandatory for restricted role agents (see below). Only the leader, and only when an artifact is too large to pass inline, may stage it as a file in a system temp directory (`os.tmpdir()`/`$TMPDIR`, `/tmp`, `/var/tmp`) outside the project tree and pass that path — never write scratch files into the repo or `.gjc/`. Product code is mutated only after the plan is approved and execution begins.
|
|
49
|
+
|
|
48
50
|
Restricted read-only role agents (`planner`, `architect`, and `critic`) must pass markdown content directly in `--artifact`; their restricted bash environment intentionally disables artifact file-path ingestion so a verdict command cannot persist arbitrary file contents.
|
|
49
51
|
|
|
50
52
|
After a role agent persists a stage artifact, its model-facing response to the caller SHOULD be receipt-only: return the `gjc ralplan --write --json` receipt (`run_id`, `path`, `stage`, `stage_n`, `sha256`, `created_at`) plus the minimal verdict/status fields the caller needs for routing, and do **not** paste the full persisted markdown back into the parent conversation. Downstream reviewers should receive the artifact path/receipt and read the persisted file themselves when they actually need the body. This preserves the audit trail while preventing Planner/Architect/Critic verdict bodies from being duplicated into the main-agent context.
|
|
@@ -66,7 +66,9 @@ export interface BashResult {
|
|
|
66
66
|
const shellSessions = new Map<string, Shell>();
|
|
67
67
|
const brokenShellSessions = new Set<string>();
|
|
68
68
|
const retiringShellSessions = new Set<Shell>();
|
|
69
|
-
|
|
69
|
+
// Cover pi-shell's normal cancellation kill waves without turning a stalled
|
|
70
|
+
// native cleanup into a multi-second JavaScript tool stall.
|
|
71
|
+
const CANCEL_CLEANUP_WAIT_MS = 400;
|
|
70
72
|
|
|
71
73
|
/** Number of persistent shell sessions currently retained (owner gauge). */
|
|
72
74
|
export function getShellSessionCount(): number {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { safeStderrWrite } from "@gajae-code/utils";
|
|
3
4
|
import type { Args } from "../cli/args";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
type GjcTmuxProfileCommand,
|
|
15
16
|
resolveGjcTmuxCommand,
|
|
16
17
|
} from "./tmux-common";
|
|
17
|
-
import {
|
|
18
|
+
import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope } from "./tmux-sessions";
|
|
18
19
|
|
|
19
20
|
export {
|
|
20
21
|
buildGjcTmuxProfileCommands,
|
|
@@ -83,6 +84,20 @@ export interface TmuxLaunchPlan {
|
|
|
83
84
|
sessionStateFile?: string | null;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
88
|
+
return env.GJC_TMUX_SESSION?.trim() || undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findExistingSessionForLaunch(context: {
|
|
92
|
+
env: NodeJS.ProcessEnv;
|
|
93
|
+
project: string;
|
|
94
|
+
branch?: string | null;
|
|
95
|
+
}): string | undefined {
|
|
96
|
+
const explicit = explicitTmuxSessionName(context.env);
|
|
97
|
+
if (explicit) return findGjcTmuxSessionByName(explicit, context.env)?.name;
|
|
98
|
+
return findGjcTmuxSessionByScope(context.project, context.branch, context.env)?.name;
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
export interface GjcTmuxProfileResult {
|
|
87
102
|
skipped: boolean;
|
|
88
103
|
commands: GjcTmuxProfileCommand[];
|
|
@@ -107,6 +122,7 @@ interface CommandResolutionContext {
|
|
|
107
122
|
argv: string[];
|
|
108
123
|
execPath: string;
|
|
109
124
|
extraEnv?: Record<string, string>;
|
|
125
|
+
platform?: NodeJS.Platform;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
|
|
@@ -148,6 +164,26 @@ function buildEnvAssignments(values: Record<string, string> | undefined): string
|
|
|
148
164
|
const entries = Object.entries(values ?? {});
|
|
149
165
|
return entries.length === 0 ? "" : ` ${entries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}`;
|
|
150
166
|
}
|
|
167
|
+
function powershellQuote(value: string): string {
|
|
168
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
169
|
+
}
|
|
170
|
+
function stripRootTmuxFlag(rawArgs: string[]): string[] {
|
|
171
|
+
return rawArgs.filter(arg => arg !== "--tmux");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
175
|
+
const command = resolveCurrentGjcCommand(context);
|
|
176
|
+
const envLines = Object.entries({ [GJC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
|
|
177
|
+
([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
|
|
178
|
+
);
|
|
179
|
+
const invocation = ["&", ...command.map(powershellQuote), ...stripRootTmuxFlag(rawArgs).map(powershellQuote)].join(
|
|
180
|
+
" ",
|
|
181
|
+
);
|
|
182
|
+
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
183
|
+
const script = [...envLines, invocation, exitLine].join("\n");
|
|
184
|
+
const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
|
|
185
|
+
return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
|
|
186
|
+
}
|
|
151
187
|
|
|
152
188
|
export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
|
|
153
189
|
const env = context.env ?? process.env;
|
|
@@ -177,16 +213,26 @@ function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
|
177
213
|
if (isBunVirtualPath(entrypoint)) {
|
|
178
214
|
return isBunVirtualPath(context.execPath) ? ["gjc"] : [context.execPath];
|
|
179
215
|
}
|
|
180
|
-
const
|
|
216
|
+
const pathModule = pathModuleForPlatform(context.platform);
|
|
217
|
+
const resolvedEntrypoint = pathModule.isAbsolute(entrypoint)
|
|
218
|
+
? entrypoint
|
|
219
|
+
: pathModule.resolve(context.cwd, entrypoint);
|
|
181
220
|
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
|
|
182
221
|
return [context.execPath, resolvedEntrypoint];
|
|
183
222
|
}
|
|
184
223
|
return [resolvedEntrypoint];
|
|
185
224
|
}
|
|
225
|
+
function isWindowsPlatform(platform: NodeJS.Platform | undefined): boolean {
|
|
226
|
+
return platform === "win32";
|
|
227
|
+
}
|
|
228
|
+
function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path {
|
|
229
|
+
return isWindowsPlatform(platform) ? path.win32 : path;
|
|
230
|
+
}
|
|
186
231
|
|
|
187
232
|
function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
233
|
+
if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
|
|
188
234
|
const command = resolveCurrentGjcCommand(context);
|
|
189
|
-
const quoted = [...command, ...rawArgs].map(shellQuote).join(" ");
|
|
235
|
+
const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
|
|
190
236
|
return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
|
|
191
237
|
}
|
|
192
238
|
|
|
@@ -305,7 +351,6 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
305
351
|
if (!context.parsed.tmux || policy === "direct") return undefined;
|
|
306
352
|
if (env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return undefined;
|
|
307
353
|
const platform = context.platform ?? process.platform;
|
|
308
|
-
if (platform === "win32") return undefined;
|
|
309
354
|
const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
|
|
310
355
|
if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
|
|
311
356
|
|
|
@@ -320,12 +365,14 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
320
365
|
path.join(cwd, ".gjc", "runtime", "tmux-sessions", `${buildGjcTmuxSessionSlug(sessionName)}.json`);
|
|
321
366
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
322
367
|
if (!tmuxAvailable) return undefined;
|
|
323
|
-
const
|
|
368
|
+
const existingSessionName =
|
|
324
369
|
"existingBranchSessionName" in context
|
|
325
370
|
? (context.existingBranchSessionName ?? undefined)
|
|
326
|
-
:
|
|
327
|
-
|
|
328
|
-
|
|
371
|
+
: findExistingSessionForLaunch({
|
|
372
|
+
env,
|
|
373
|
+
project,
|
|
374
|
+
branch,
|
|
375
|
+
});
|
|
329
376
|
const innerCommand = buildInnerCommand(
|
|
330
377
|
{
|
|
331
378
|
cwd,
|
|
@@ -335,6 +382,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
335
382
|
[GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
336
383
|
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
337
384
|
},
|
|
385
|
+
platform,
|
|
338
386
|
},
|
|
339
387
|
context.rawArgs,
|
|
340
388
|
);
|
|
@@ -348,7 +396,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
348
396
|
project,
|
|
349
397
|
sessionId,
|
|
350
398
|
sessionStateFile,
|
|
351
|
-
attachSessionName:
|
|
399
|
+
attachSessionName: existingSessionName,
|
|
352
400
|
};
|
|
353
401
|
}
|
|
354
402
|
|
|
@@ -405,19 +453,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
405
453
|
sessionId: plan.sessionId ?? null,
|
|
406
454
|
sessionStateFile: plan.sessionStateFile ?? null,
|
|
407
455
|
});
|
|
408
|
-
|
|
456
|
+
const ownershipFailure = profile.failures.find(item => item.command.args.includes("@gjc-profile"));
|
|
457
|
+
if (ownershipFailure) {
|
|
409
458
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
410
|
-
const failure =
|
|
411
|
-
profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
|
|
412
459
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
413
|
-
formatTmuxLaunchDiagnostic("profile tagging failed",
|
|
460
|
+
formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
|
|
414
461
|
);
|
|
415
462
|
return true;
|
|
416
463
|
}
|
|
417
464
|
}
|
|
418
465
|
if (created.exitCode !== 0) return false;
|
|
419
|
-
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
466
|
+
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
|
|
420
467
|
if (attached.exitCode === 0) return true;
|
|
468
|
+
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
421
469
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
422
470
|
return true;
|
|
423
471
|
}
|
|
@@ -297,12 +297,26 @@ async function resolveSelectors(
|
|
|
297
297
|
}
|
|
298
298
|
if (mode) assertKnownMode(mode);
|
|
299
299
|
|
|
300
|
+
const sessionId = resolveSessionIdFromArgs(args, payload);
|
|
301
|
+
|
|
302
|
+
const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
|
|
303
|
+
if (threadId) assertSafePathComponent(threadId, "thread-id");
|
|
304
|
+
const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
|
|
305
|
+
if (turnId) assertSafePathComponent(turnId, "turn-id");
|
|
306
|
+
|
|
307
|
+
return { mode: mode as CanonicalGjcWorkflowSkill | undefined, sessionId, threadId, turnId, payload };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Session-id resolution order: explicit --session-id flag, then payload
|
|
311
|
+
// session_id, then GJC_SESSION_ID env var (set by AgentSession.sdk for
|
|
312
|
+
// agent-initiated CLI invocations). The env-var default keeps shell
|
|
313
|
+
// snippets in skill docs short while still routing state commands to the
|
|
314
|
+
// caller's session-scoped state files.
|
|
315
|
+
function resolveSessionIdFromArgs(
|
|
316
|
+
args: readonly string[],
|
|
317
|
+
payload: Record<string, unknown> | undefined,
|
|
318
|
+
): string | undefined {
|
|
300
319
|
const explicitSessionId = flagValue(args, "--session-id");
|
|
301
|
-
// Session-id resolution order: explicit --session-id flag, then payload
|
|
302
|
-
// session_id, then GJC_SESSION_ID env var (set by AgentSession.sdk for
|
|
303
|
-
// agent-initiated CLI invocations). The env-var default keeps shell
|
|
304
|
-
// snippets in skill docs short while still routing writes/reads to the
|
|
305
|
-
// caller's session-scoped state files.
|
|
306
320
|
let sessionId = explicitSessionId !== undefined ? explicitSessionId.trim() || undefined : undefined;
|
|
307
321
|
if (!sessionId && payload && typeof payload.session_id === "string") {
|
|
308
322
|
sessionId = payload.session_id.trim() || undefined;
|
|
@@ -312,13 +326,7 @@ async function resolveSelectors(
|
|
|
312
326
|
if (envSessionId) sessionId = envSessionId;
|
|
313
327
|
}
|
|
314
328
|
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
315
|
-
|
|
316
|
-
const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
|
|
317
|
-
if (threadId) assertSafePathComponent(threadId, "thread-id");
|
|
318
|
-
const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
|
|
319
|
-
if (turnId) assertSafePathComponent(turnId, "turn-id");
|
|
320
|
-
|
|
321
|
-
return { mode: mode as CanonicalGjcWorkflowSkill | undefined, sessionId, threadId, turnId, payload };
|
|
329
|
+
return sessionId;
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
async function inferModeFromActiveState(
|
|
@@ -718,8 +726,8 @@ async function handleDoctor(
|
|
|
718
726
|
): Promise<StateCommandResult> {
|
|
719
727
|
const rawSkill = flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim();
|
|
720
728
|
if (rawSkill) assertKnownMode(rawSkill);
|
|
721
|
-
const
|
|
722
|
-
|
|
729
|
+
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
730
|
+
const sessionId = resolveSessionIdFromArgs(args, payload);
|
|
723
731
|
const summary = await collectDoctorSummary(cwd, rawSkill as CanonicalGjcWorkflowSkill | undefined, sessionId);
|
|
724
732
|
return {
|
|
725
733
|
status: summary.ok ? 0 : 1,
|
|
@@ -298,8 +298,28 @@ function flattenActiveSubskills(entries: SkillActiveEntry[]): ActiveSubskillEntr
|
|
|
298
298
|
return [...deduped.values()];
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
const CANONICAL_PIPELINE_RANK = new Map<string, number>([
|
|
302
|
+
["deep-interview", 0],
|
|
303
|
+
["ralplan", 1],
|
|
304
|
+
["ultragoal", 2],
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
function canonicalPipelineRank(skill: string): number | undefined {
|
|
308
|
+
return CANONICAL_PIPELINE_RANK.get(skill);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function compareActiveEntryPrimary(a: SkillActiveEntry, b: SkillActiveEntry): number {
|
|
312
|
+
const aRank = canonicalPipelineRank(a.skill);
|
|
313
|
+
const bRank = canonicalPipelineRank(b.skill);
|
|
314
|
+
if (aRank !== undefined || bRank !== undefined) return (bRank ?? -1) - (aRank ?? -1);
|
|
315
|
+
const aTime = Date.parse(safeString(a.updated_at));
|
|
316
|
+
const bTime = Date.parse(safeString(b.updated_at));
|
|
317
|
+
if (Number.isFinite(aTime) || Number.isFinite(bTime)) return (bTime || 0) - (aTime || 0);
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
301
321
|
function buildActiveSnapshot(entries: SkillActiveEntry[]): SkillActiveState {
|
|
302
|
-
const visible = entries.filter(entry => entry.active !== false);
|
|
322
|
+
const visible = entries.filter(entry => entry.active !== false).toSorted(compareActiveEntryPrimary);
|
|
303
323
|
const primary = visible[0];
|
|
304
324
|
return {
|
|
305
325
|
version: 1,
|
|
@@ -137,6 +137,8 @@ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[]
|
|
|
137
137
|
export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
|
|
138
138
|
return listSessionLines(env)
|
|
139
139
|
.map(parseSessionLine)
|
|
140
|
+
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
141
|
+
.map(session => hydrateSessionFromExactOptions(session, env))
|
|
140
142
|
.filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
|
|
141
143
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
144
|
}
|
|
@@ -145,7 +147,8 @@ export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTm
|
|
|
145
147
|
export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
|
|
146
148
|
const sessions = listSessionLines(env)
|
|
147
149
|
.map(parseSessionLine)
|
|
148
|
-
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
150
|
+
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
151
|
+
.map(session => hydrateSessionFromExactOptions(session, env));
|
|
149
152
|
const tagged = sessions
|
|
150
153
|
.filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
|
|
151
154
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -179,6 +182,22 @@ export function findGjcTmuxSessionByBranch(
|
|
|
179
182
|
);
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
export function findGjcTmuxSessionByName(
|
|
186
|
+
sessionName: string,
|
|
187
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
188
|
+
): GjcTmuxSessionStatus | undefined {
|
|
189
|
+
return listGjcTmuxSessions(env).find(session => session.name === sessionName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function findGjcTmuxSessionByScope(
|
|
193
|
+
project: string,
|
|
194
|
+
branch: string | null | undefined,
|
|
195
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
196
|
+
): GjcTmuxSessionStatus | undefined {
|
|
197
|
+
return listGjcTmuxSessions(env).find(
|
|
198
|
+
session => session.project === project && (branch ? session.branch === branch : session.branch === undefined),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
182
201
|
export function statusGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
|
|
183
202
|
const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
|
|
184
203
|
if (session) return session;
|
|
@@ -227,6 +246,22 @@ function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.P
|
|
|
227
246
|
}
|
|
228
247
|
}
|
|
229
248
|
|
|
249
|
+
function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: NodeJS.ProcessEnv): GjcTmuxSessionStatus {
|
|
250
|
+
if (session.profile === GJC_TMUX_PROFILE_VALUE) return session;
|
|
251
|
+
const profile = readExactOptionForGc(session.name, GJC_TMUX_PROFILE_OPTION, env);
|
|
252
|
+
if (profile !== GJC_TMUX_PROFILE_VALUE) return session;
|
|
253
|
+
return {
|
|
254
|
+
...session,
|
|
255
|
+
profile,
|
|
256
|
+
branch: session.branch ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_OPTION, env),
|
|
257
|
+
branchSlug: session.branchSlug ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_SLUG_OPTION, env),
|
|
258
|
+
project: session.project ?? readExactOptionForGc(session.name, GJC_TMUX_PROJECT_OPTION, env),
|
|
259
|
+
sessionId: session.sessionId ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, env),
|
|
260
|
+
sessionStateFile:
|
|
261
|
+
session.sessionStateFile ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
/** @internal */
|
|
231
266
|
export function readTmuxSessionTagsForGc(
|
|
232
267
|
sessionName: string,
|