@sayknow-cli/coding-agent 0.2.3 → 0.2.5
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 +15 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -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/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/i18n/messages/en.d.ts +14 -0
- package/dist/types/lsp/startup-events.d.ts +1 -0
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- 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/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/skc-runtime/session-layout.d.ts +59 -0
- package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/skc-runtime/state-graph.d.ts +1 -1
- package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/skc-runtime/state-schema.d.ts +2 -0
- package/dist/types/skc-runtime/state-writer.d.ts +36 -7
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/package.json +7 -7
- package/scripts/build-binary.ts +0 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli/update-cli.ts +53 -3
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/launch.ts +1 -1
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- 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/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
- package/src/defaults/skc/skills/team/SKILL.md +51 -47
- package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
- package/src/exec/bash-executor.ts +3 -1
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/skc-plugins/injection.ts +23 -4
- package/src/extensibility/skc-plugins/state.ts +16 -1
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/i18n/messages/de.ts +14 -0
- package/src/i18n/messages/en.ts +14 -0
- package/src/i18n/messages/es.ts +14 -0
- package/src/i18n/messages/fr.ts +14 -0
- package/src/i18n/messages/ja.ts +14 -0
- package/src/i18n/messages/ko.ts +14 -0
- package/src/i18n/messages/zh.ts +14 -0
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/startup-events.ts +24 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/model-selector.ts +42 -18
- package/src/modes/components/welcome.ts +20 -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/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +43 -41
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skc-runtime/deep-interview-recorder.ts +51 -18
- package/src/skc-runtime/deep-interview-runtime.ts +49 -23
- package/src/skc-runtime/goal-mode-request.ts +26 -11
- package/src/skc-runtime/launch-tmux.ts +68 -15
- package/src/skc-runtime/ralplan-runtime.ts +79 -50
- package/src/skc-runtime/session-layout.ts +180 -0
- package/src/skc-runtime/session-resolution.ts +217 -0
- package/src/skc-runtime/state-graph.ts +1 -2
- package/src/skc-runtime/state-migrations.ts +1 -0
- package/src/skc-runtime/state-runtime.ts +237 -114
- package/src/skc-runtime/state-schema.ts +2 -0
- package/src/skc-runtime/state-writer.ts +310 -42
- package/src/skc-runtime/team-runtime.ts +43 -19
- package/src/skc-runtime/tmux-sessions.ts +43 -2
- package/src/skc-runtime/ultragoal-guard.ts +45 -2
- package/src/skc-runtime/ultragoal-runtime.ts +121 -41
- package/src/skc-runtime/workflow-command-ref.ts +1 -2
- package/src/skc-runtime/workflow-manifest.ts +1 -2
- package/src/skill-state/active-state.ts +116 -129
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- package/src/task/agents.ts +1 -22
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +34 -12
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/computer.ts +58 -4
- package/src/utils/edit-mode.ts +1 -1
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { safeStderrWrite } from "@sayknow-cli/utils";
|
|
3
4
|
import type { Args } from "../cli/args";
|
|
5
|
+
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
4
6
|
import { SKC_COORDINATOR_SESSION_ID_ENV, SKC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
5
7
|
import {
|
|
6
8
|
buildSkcTmuxProfileCommands,
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
SKC_TMUX_SESSION_PREFIX,
|
|
15
17
|
type SkcTmuxProfileCommand,
|
|
16
18
|
} from "./tmux-common";
|
|
17
|
-
import {
|
|
19
|
+
import { findSkcTmuxSessionByName, findSkcTmuxSessionByScope } from "./tmux-sessions";
|
|
18
20
|
|
|
19
21
|
export {
|
|
20
22
|
buildSkcTmuxProfileCommands,
|
|
@@ -83,6 +85,20 @@ export interface TmuxLaunchPlan {
|
|
|
83
85
|
sessionStateFile?: string | null;
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
89
|
+
return env.SKC_TMUX_SESSION?.trim() || undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findExistingSessionForLaunch(context: {
|
|
93
|
+
env: NodeJS.ProcessEnv;
|
|
94
|
+
project: string;
|
|
95
|
+
branch?: string | null;
|
|
96
|
+
}): string | undefined {
|
|
97
|
+
const explicit = explicitTmuxSessionName(context.env);
|
|
98
|
+
if (explicit) return findSkcTmuxSessionByName(explicit, context.env)?.name;
|
|
99
|
+
return findSkcTmuxSessionByScope(context.project, context.branch, context.env)?.name;
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
export interface SkcTmuxProfileResult {
|
|
87
103
|
skipped: boolean;
|
|
88
104
|
commands: SkcTmuxProfileCommand[];
|
|
@@ -107,6 +123,7 @@ interface CommandResolutionContext {
|
|
|
107
123
|
argv: string[];
|
|
108
124
|
execPath: string;
|
|
109
125
|
extraEnv?: Record<string, string>;
|
|
126
|
+
platform?: NodeJS.Platform;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
|
|
@@ -148,6 +165,26 @@ function buildEnvAssignments(values: Record<string, string> | undefined): string
|
|
|
148
165
|
const entries = Object.entries(values ?? {});
|
|
149
166
|
return entries.length === 0 ? "" : ` ${entries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}`;
|
|
150
167
|
}
|
|
168
|
+
function powershellQuote(value: string): string {
|
|
169
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
170
|
+
}
|
|
171
|
+
function stripRootTmuxFlag(rawArgs: string[]): string[] {
|
|
172
|
+
return rawArgs.filter(arg => arg !== "--tmux");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
176
|
+
const command = resolveCurrentSkcCommand(context);
|
|
177
|
+
const envLines = Object.entries({ [SKC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
|
|
178
|
+
([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
|
|
179
|
+
);
|
|
180
|
+
const invocation = ["&", ...command.map(powershellQuote), ...stripRootTmuxFlag(rawArgs).map(powershellQuote)].join(
|
|
181
|
+
" ",
|
|
182
|
+
);
|
|
183
|
+
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
184
|
+
const script = [...envLines, invocation, exitLine].join("\n");
|
|
185
|
+
const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
|
|
186
|
+
return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
|
|
187
|
+
}
|
|
151
188
|
|
|
152
189
|
export function applySkcTmuxProfile(context: SkcTmuxProfileContext): SkcTmuxProfileResult {
|
|
153
190
|
const env = context.env ?? process.env;
|
|
@@ -177,16 +214,26 @@ function resolveCurrentSkcCommand(context: CommandResolutionContext): string[] {
|
|
|
177
214
|
if (isBunVirtualPath(entrypoint)) {
|
|
178
215
|
return isBunVirtualPath(context.execPath) ? ["skc"] : [context.execPath];
|
|
179
216
|
}
|
|
180
|
-
const
|
|
217
|
+
const pathModule = pathModuleForPlatform(context.platform);
|
|
218
|
+
const resolvedEntrypoint = pathModule.isAbsolute(entrypoint)
|
|
219
|
+
? entrypoint
|
|
220
|
+
: pathModule.resolve(context.cwd, entrypoint);
|
|
181
221
|
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
|
|
182
222
|
return [context.execPath, resolvedEntrypoint];
|
|
183
223
|
}
|
|
184
224
|
return [resolvedEntrypoint];
|
|
185
225
|
}
|
|
226
|
+
function isWindowsPlatform(platform: NodeJS.Platform | undefined): boolean {
|
|
227
|
+
return platform === "win32";
|
|
228
|
+
}
|
|
229
|
+
function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path {
|
|
230
|
+
return isWindowsPlatform(platform) ? path.win32 : path;
|
|
231
|
+
}
|
|
186
232
|
|
|
187
233
|
function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
234
|
+
if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
|
|
188
235
|
const command = resolveCurrentSkcCommand(context);
|
|
189
|
-
const quoted = [...command, ...rawArgs].map(shellQuote).join(" ");
|
|
236
|
+
const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
|
|
190
237
|
return `exec env ${SKC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
|
|
191
238
|
}
|
|
192
239
|
|
|
@@ -305,7 +352,6 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
305
352
|
if (!context.parsed.tmux || policy === "direct") return undefined;
|
|
306
353
|
if (env.TMUX || env[SKC_TMUX_LAUNCHED_ENV] === "1") return undefined;
|
|
307
354
|
const platform = context.platform ?? process.platform;
|
|
308
|
-
if (platform === "win32") return undefined;
|
|
309
355
|
const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
|
|
310
356
|
if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
|
|
311
357
|
|
|
@@ -315,17 +361,23 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
315
361
|
const sessionName = buildSkcTmuxSessionName(env, { branch });
|
|
316
362
|
const tmuxCommand = resolveSkcTmuxCommand(env);
|
|
317
363
|
const sessionId = env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
|
|
364
|
+
// The session ROOT is keyed by the active SKC session (SKC_SESSION_ID), NOT the
|
|
365
|
+
// coordinator/tmux identity. Fall back to the coordinator id only for standalone
|
|
366
|
+
// tmux launches with no SKC session context.
|
|
367
|
+
const skcSessionId = env.SKC_SESSION_ID?.trim() || sessionId;
|
|
318
368
|
const sessionStateFile =
|
|
319
369
|
env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
|
|
320
|
-
|
|
370
|
+
tmuxRuntimeSessionPath(cwd, skcSessionId, buildSkcTmuxSessionSlug(sessionName));
|
|
321
371
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
322
372
|
if (!tmuxAvailable) return undefined;
|
|
323
|
-
const
|
|
373
|
+
const existingSessionName =
|
|
324
374
|
"existingBranchSessionName" in context
|
|
325
375
|
? (context.existingBranchSessionName ?? undefined)
|
|
326
|
-
:
|
|
327
|
-
|
|
328
|
-
|
|
376
|
+
: findExistingSessionForLaunch({
|
|
377
|
+
env,
|
|
378
|
+
project,
|
|
379
|
+
branch,
|
|
380
|
+
});
|
|
329
381
|
const innerCommand = buildInnerCommand(
|
|
330
382
|
{
|
|
331
383
|
cwd,
|
|
@@ -335,6 +387,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
335
387
|
[SKC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
336
388
|
[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
337
389
|
},
|
|
390
|
+
platform,
|
|
338
391
|
},
|
|
339
392
|
context.rawArgs,
|
|
340
393
|
);
|
|
@@ -348,7 +401,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
348
401
|
project,
|
|
349
402
|
sessionId,
|
|
350
403
|
sessionStateFile,
|
|
351
|
-
attachSessionName:
|
|
404
|
+
attachSessionName: existingSessionName,
|
|
352
405
|
};
|
|
353
406
|
}
|
|
354
407
|
|
|
@@ -405,19 +458,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
405
458
|
sessionId: plan.sessionId ?? null,
|
|
406
459
|
sessionStateFile: plan.sessionStateFile ?? null,
|
|
407
460
|
});
|
|
408
|
-
|
|
461
|
+
const ownershipFailure = profile.failures.find(item => item.command.args.includes("@skc-profile"));
|
|
462
|
+
if (ownershipFailure) {
|
|
409
463
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
410
|
-
const failure =
|
|
411
|
-
profile.failures.find(item => item.command.args.includes("@skc-profile")) ?? profile.failures[0];
|
|
412
464
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
413
|
-
formatTmuxLaunchDiagnostic("profile tagging failed",
|
|
465
|
+
formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
|
|
414
466
|
);
|
|
415
467
|
return true;
|
|
416
468
|
}
|
|
417
469
|
}
|
|
418
470
|
if (created.exitCode !== 0) return false;
|
|
419
|
-
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
471
|
+
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
|
|
420
472
|
if (attached.exitCode === 0) return true;
|
|
473
|
+
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
421
474
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
422
475
|
return true;
|
|
423
476
|
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
summarizeRalplanIndex,
|
|
13
13
|
} from "./ledger-event-renderer";
|
|
14
14
|
import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
15
|
+
import { modeStatePath, sessionPlansDir } from "./session-layout";
|
|
16
|
+
import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
15
17
|
import { migrateWorkflowState } from "./state-migrations";
|
|
16
18
|
import { runNativeStateCommand } from "./state-runtime";
|
|
17
19
|
import {
|
|
@@ -169,18 +171,15 @@ interface ResolvedArtifactArgs {
|
|
|
169
171
|
stageN: number;
|
|
170
172
|
runId: string;
|
|
171
173
|
artifact: string;
|
|
172
|
-
sessionId: string
|
|
174
|
+
sessionId: string;
|
|
173
175
|
json: boolean;
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
function ralplanStatePath(cwd: string, sessionId: string
|
|
177
|
-
|
|
178
|
-
? path.join(cwd, ".skc", "state", "sessions", encodeSessionSegment(sessionId))
|
|
179
|
-
: path.join(cwd, ".skc", "state");
|
|
180
|
-
return path.join(stateDir, "ralplan-state.json");
|
|
178
|
+
function ralplanStatePath(cwd: string, sessionId: string): string {
|
|
179
|
+
return modeStatePath(cwd, sessionId, "ralplan");
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
async function readActiveRunId(cwd: string, sessionId: string
|
|
182
|
+
async function readActiveRunId(cwd: string, sessionId: string): Promise<string | undefined> {
|
|
184
183
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
185
184
|
const existingRead = await readExistingStateForMutation(statePath);
|
|
186
185
|
if (existingRead.kind === "absent") return undefined;
|
|
@@ -221,12 +220,7 @@ function advanceCurrentPhase(existingPhase: unknown, stage: RalplanStage): strin
|
|
|
221
220
|
return stage;
|
|
222
221
|
}
|
|
223
222
|
|
|
224
|
-
async function persistActiveRunId(
|
|
225
|
-
cwd: string,
|
|
226
|
-
sessionId: string | undefined,
|
|
227
|
-
runId: string,
|
|
228
|
-
stage: RalplanStage,
|
|
229
|
-
): Promise<void> {
|
|
223
|
+
async function persistActiveRunId(cwd: string, sessionId: string, runId: string, stage: RalplanStage): Promise<void> {
|
|
230
224
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
231
225
|
const existingRead = await readExistingStateForMutation(statePath);
|
|
232
226
|
if (existingRead.kind === "corrupt") {
|
|
@@ -261,7 +255,7 @@ async function persistActiveRunId(
|
|
|
261
255
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
262
256
|
cwd,
|
|
263
257
|
receipt: { cwd, skill: "ralplan", owner: "skc-runtime", command: "skc ralplan persist-run-id", sessionId },
|
|
264
|
-
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan" },
|
|
258
|
+
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan", sessionId },
|
|
265
259
|
});
|
|
266
260
|
}
|
|
267
261
|
|
|
@@ -384,11 +378,7 @@ function plannerStatePayload(update: PlannerStateUpdate): Record<string, unknown
|
|
|
384
378
|
* audit/routing hint only — it records what the caller has already proven and is
|
|
385
379
|
* NOT a durable cross-process subagent registry.
|
|
386
380
|
*/
|
|
387
|
-
async function applyPlannerStateUpdate(
|
|
388
|
-
cwd: string,
|
|
389
|
-
sessionId: string | undefined,
|
|
390
|
-
update: PlannerStateUpdate,
|
|
391
|
-
): Promise<void> {
|
|
381
|
+
async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: PlannerStateUpdate): Promise<void> {
|
|
392
382
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
393
383
|
const existingRead = await readExistingStateForMutation(statePath);
|
|
394
384
|
if (existingRead.kind === "corrupt") {
|
|
@@ -407,7 +397,7 @@ async function applyPlannerStateUpdate(
|
|
|
407
397
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
408
398
|
cwd,
|
|
409
399
|
receipt: { cwd, skill: "ralplan", owner: "skc-runtime", command: "skc ralplan planner-state", sessionId },
|
|
410
|
-
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan" },
|
|
400
|
+
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan", sessionId },
|
|
411
401
|
});
|
|
412
402
|
}
|
|
413
403
|
|
|
@@ -423,9 +413,13 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
423
413
|
throw new RalplanCommandError(2, "--artifact is required for ralplan --write");
|
|
424
414
|
}
|
|
425
415
|
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
416
|
+
const session = resolveSkcSessionForWrite(cwd, {
|
|
417
|
+
flagValue: flagValue(args, "--session-id"),
|
|
418
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
419
|
+
});
|
|
420
|
+
const sessionId = session.skcSessionId;
|
|
421
|
+
assertSafePathComponent(sessionId, "session-id");
|
|
422
|
+
const sessionIdRaw = sessionId;
|
|
429
423
|
|
|
430
424
|
// Precedence for run_id:
|
|
431
425
|
// 1. explicit --run-id flag
|
|
@@ -469,13 +463,19 @@ async function persistArtifact(
|
|
|
469
463
|
content: string,
|
|
470
464
|
sha256: string,
|
|
471
465
|
): Promise<PersistedArtifact> {
|
|
472
|
-
const runDir = path.join(cwd,
|
|
466
|
+
const runDir = path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId);
|
|
473
467
|
|
|
474
468
|
const fileName = `stage-${pad2(resolved.stageN)}-${resolved.stage}.md`;
|
|
475
469
|
const filePath = path.join(runDir, fileName);
|
|
476
470
|
await writeArtifact(filePath, content, {
|
|
477
471
|
cwd,
|
|
478
|
-
audit: {
|
|
472
|
+
audit: {
|
|
473
|
+
category: "artifact",
|
|
474
|
+
verb: "write",
|
|
475
|
+
owner: "skc-runtime",
|
|
476
|
+
skill: "ralplan",
|
|
477
|
+
sessionId: resolved.sessionId,
|
|
478
|
+
},
|
|
479
479
|
});
|
|
480
480
|
|
|
481
481
|
const createdAt = new Date().toISOString();
|
|
@@ -488,7 +488,13 @@ async function persistArtifact(
|
|
|
488
488
|
};
|
|
489
489
|
await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
|
|
490
490
|
cwd,
|
|
491
|
-
audit: {
|
|
491
|
+
audit: {
|
|
492
|
+
category: "ledger",
|
|
493
|
+
verb: "append",
|
|
494
|
+
owner: "skc-runtime",
|
|
495
|
+
skill: "ralplan",
|
|
496
|
+
sessionId: resolved.sessionId,
|
|
497
|
+
},
|
|
492
498
|
key: ralplanIndexKey,
|
|
493
499
|
});
|
|
494
500
|
|
|
@@ -497,7 +503,13 @@ async function persistArtifact(
|
|
|
497
503
|
pendingApprovalPath = path.join(runDir, "pending-approval.md");
|
|
498
504
|
await writeArtifact(pendingApprovalPath, content, {
|
|
499
505
|
cwd,
|
|
500
|
-
audit: {
|
|
506
|
+
audit: {
|
|
507
|
+
category: "artifact",
|
|
508
|
+
verb: "write",
|
|
509
|
+
owner: "skc-runtime",
|
|
510
|
+
skill: "ralplan",
|
|
511
|
+
sessionId: resolved.sessionId,
|
|
512
|
+
},
|
|
501
513
|
});
|
|
502
514
|
}
|
|
503
515
|
|
|
@@ -528,11 +540,12 @@ interface ExistingStageArtifact {
|
|
|
528
540
|
*/
|
|
529
541
|
async function findExistingStageArtifact(
|
|
530
542
|
cwd: string,
|
|
543
|
+
sessionId: string,
|
|
531
544
|
runId: string,
|
|
532
545
|
stage: RalplanStage,
|
|
533
546
|
stageN: number,
|
|
534
547
|
): Promise<ExistingStageArtifact | undefined> {
|
|
535
|
-
const indexPath = path.join(cwd,
|
|
548
|
+
const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
|
|
536
549
|
let text: string;
|
|
537
550
|
try {
|
|
538
551
|
text = await fs.readFile(indexPath, "utf8");
|
|
@@ -566,9 +579,9 @@ async function findExistingStageArtifact(
|
|
|
566
579
|
* Read and parse the run's `index.jsonl` rows. Best-effort: returns [] when the
|
|
567
580
|
* file is absent or unreadable so HUD sync never fails on a missing index.
|
|
568
581
|
*/
|
|
569
|
-
async function readRalplanIndexRows(cwd: string, runId: string): Promise<RalplanIndexRow[]> {
|
|
582
|
+
async function readRalplanIndexRows(cwd: string, sessionId: string, runId: string): Promise<RalplanIndexRow[]> {
|
|
570
583
|
try {
|
|
571
|
-
const indexPath = path.join(cwd,
|
|
584
|
+
const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
|
|
572
585
|
const text = await fs.readFile(indexPath, "utf8");
|
|
573
586
|
const rows: RalplanIndexRow[] = [];
|
|
574
587
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -583,7 +596,7 @@ async function readRalplanIndexRows(cwd: string, runId: string): Promise<Ralplan
|
|
|
583
596
|
|
|
584
597
|
async function syncRalplanHud(options: {
|
|
585
598
|
cwd: string;
|
|
586
|
-
sessionId
|
|
599
|
+
sessionId: string;
|
|
587
600
|
stage: string;
|
|
588
601
|
pendingApproval: boolean;
|
|
589
602
|
iteration?: number;
|
|
@@ -612,11 +625,12 @@ async function buildRalplanHud(options: {
|
|
|
612
625
|
iteration?: number;
|
|
613
626
|
latestSummary?: string;
|
|
614
627
|
runId?: string;
|
|
628
|
+
sessionId?: string;
|
|
615
629
|
}) {
|
|
616
630
|
let iterationFromIndex: number | undefined;
|
|
617
631
|
let stages: string | undefined;
|
|
618
|
-
if (options.runId) {
|
|
619
|
-
const rows = await readRalplanIndexRows(options.cwd, options.runId);
|
|
632
|
+
if (options.runId && options.sessionId) {
|
|
633
|
+
const rows = await readRalplanIndexRows(options.cwd, options.sessionId, options.runId);
|
|
620
634
|
if (rows.length > 0) {
|
|
621
635
|
const summary = summarizeRalplanIndex(rows);
|
|
622
636
|
iterationFromIndex = summary.iteration;
|
|
@@ -643,7 +657,13 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
643
657
|
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
644
658
|
// silently clobber the artifact or append a duplicate ledger row. Classify before any
|
|
645
659
|
// state mutation so a conflict never regresses run-state phase.
|
|
646
|
-
const existingArtifact = await findExistingStageArtifact(
|
|
660
|
+
const existingArtifact = await findExistingStageArtifact(
|
|
661
|
+
cwd,
|
|
662
|
+
resolved.sessionId,
|
|
663
|
+
resolved.runId,
|
|
664
|
+
resolved.stage,
|
|
665
|
+
resolved.stageN,
|
|
666
|
+
);
|
|
647
667
|
if (existingArtifact) {
|
|
648
668
|
if (existingArtifact.sha256 !== sha256) {
|
|
649
669
|
throw new RalplanCommandError(
|
|
@@ -660,6 +680,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
660
680
|
if (plannerState) {
|
|
661
681
|
await applyPlannerStateUpdate(cwd, resolved.sessionId, plannerState);
|
|
662
682
|
}
|
|
683
|
+
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
|
|
663
684
|
await syncRalplanHud({
|
|
664
685
|
cwd,
|
|
665
686
|
sessionId: resolved.sessionId,
|
|
@@ -706,7 +727,12 @@ function buildDeduplicatedResult(
|
|
|
706
727
|
deduplicated: true,
|
|
707
728
|
};
|
|
708
729
|
if (resolved.stage === "final") {
|
|
709
|
-
payload.pending_approval_path = path.join(
|
|
730
|
+
payload.pending_approval_path = path.join(
|
|
731
|
+
sessionPlansDir(cwd, resolved.sessionId),
|
|
732
|
+
"ralplan",
|
|
733
|
+
resolved.runId,
|
|
734
|
+
"pending-approval.md",
|
|
735
|
+
);
|
|
710
736
|
}
|
|
711
737
|
const stdout = resolved.json
|
|
712
738
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
@@ -721,7 +747,7 @@ interface ConsensusHandoffArgs {
|
|
|
721
747
|
deliberate: boolean;
|
|
722
748
|
architectKind?: string;
|
|
723
749
|
criticKind?: string;
|
|
724
|
-
sessionId
|
|
750
|
+
sessionId: string;
|
|
725
751
|
task: string;
|
|
726
752
|
json: boolean;
|
|
727
753
|
}
|
|
@@ -747,7 +773,7 @@ function extractPositionalTask(args: readonly string[]): string {
|
|
|
747
773
|
return parts.join(" ").trim();
|
|
748
774
|
}
|
|
749
775
|
|
|
750
|
-
function resolveConsensusArgs(args: readonly string[]): ConsensusHandoffArgs {
|
|
776
|
+
function resolveConsensusArgs(args: readonly string[], cwd: string): ConsensusHandoffArgs {
|
|
751
777
|
const architectKind = flagValue(args, "--architect")?.trim() || undefined;
|
|
752
778
|
if (architectKind && !KNOWN_ARCHITECT_KINDS.has(architectKind)) {
|
|
753
779
|
throw new RalplanCommandError(
|
|
@@ -762,8 +788,12 @@ function resolveConsensusArgs(args: readonly string[]): ConsensusHandoffArgs {
|
|
|
762
788
|
`unknown --critic kind: ${criticKind}. Expected one of: ${[...KNOWN_CRITIC_KINDS].join(", ")}.`,
|
|
763
789
|
);
|
|
764
790
|
}
|
|
765
|
-
const
|
|
766
|
-
|
|
791
|
+
const session = resolveSkcSessionForWrite(cwd, {
|
|
792
|
+
flagValue: flagValue(args, "--session-id"),
|
|
793
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
794
|
+
});
|
|
795
|
+
const sessionId = session.skcSessionId;
|
|
796
|
+
assertSafePathComponent(sessionId, "session-id");
|
|
767
797
|
const task = extractPositionalTask(args);
|
|
768
798
|
return {
|
|
769
799
|
interactive: hasFlag(args, "--interactive"),
|
|
@@ -776,19 +806,11 @@ function resolveConsensusArgs(args: readonly string[]): ConsensusHandoffArgs {
|
|
|
776
806
|
};
|
|
777
807
|
}
|
|
778
808
|
|
|
779
|
-
function encodeSessionSegment(value: string): string {
|
|
780
|
-
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
781
|
-
}
|
|
782
|
-
|
|
783
809
|
async function seedRalplanState(
|
|
784
810
|
cwd: string,
|
|
785
811
|
resolved: ConsensusHandoffArgs,
|
|
786
812
|
): Promise<{ statePath: string; runId: string }> {
|
|
787
|
-
const
|
|
788
|
-
? path.join(cwd, ".skc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
|
|
789
|
-
: path.join(cwd, ".skc", "state");
|
|
790
|
-
|
|
791
|
-
const statePath = path.join(stateDir, "ralplan-state.json");
|
|
813
|
+
const statePath = ralplanStatePath(cwd, resolved.sessionId);
|
|
792
814
|
// Reuse an existing run id when present so a re-invocation of `skc ralplan "task"` doesn't
|
|
793
815
|
// orphan in-progress artifacts under a fresh run id.
|
|
794
816
|
const existingRunId = await readActiveRunId(cwd, resolved.sessionId);
|
|
@@ -818,13 +840,20 @@ async function seedRalplanState(
|
|
|
818
840
|
command: "skc ralplan seed",
|
|
819
841
|
sessionId: resolved.sessionId,
|
|
820
842
|
},
|
|
821
|
-
audit: {
|
|
843
|
+
audit: {
|
|
844
|
+
category: "state",
|
|
845
|
+
verb: "write",
|
|
846
|
+
owner: "skc-runtime",
|
|
847
|
+
skill: "ralplan",
|
|
848
|
+
sessionId: resolved.sessionId,
|
|
849
|
+
},
|
|
822
850
|
});
|
|
851
|
+
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: statePath });
|
|
823
852
|
return { statePath, runId };
|
|
824
853
|
}
|
|
825
854
|
|
|
826
855
|
async function handleConsensusHandoff(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
827
|
-
const resolved = resolveConsensusArgs(args);
|
|
856
|
+
const resolved = resolveConsensusArgs(args, cwd);
|
|
828
857
|
if (!resolved.task) {
|
|
829
858
|
throw new RalplanCommandError(2, 'skc ralplan requires a task description, e.g. `skc ralplan "<task>"`.');
|
|
830
859
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure path layout for session-scoped SKC workflow state.
|
|
3
|
+
*
|
|
4
|
+
* Every generated/runtime artifact for a SKC session lives under
|
|
5
|
+
* `<cwd>/.skc/_session-{encodedSessionId}/...`. The `_session-` prefix is what
|
|
6
|
+
* discriminates a session directory from shared, user-authored/installed config
|
|
7
|
+
* (settings.json, secrets.yml, agents/, skc-plugins/, agent/, python-env/, user
|
|
8
|
+
* skills/commands), which always stays at the `.skc/` root.
|
|
9
|
+
*
|
|
10
|
+
* This module is PURE and acyclic: every export is a deterministic function of
|
|
11
|
+
* its arguments. It never reads `process.env` and never touches the filesystem.
|
|
12
|
+
* Session resolution (flag/payload/env/latest-activity-marker) and any
|
|
13
|
+
* filesystem scanning live in `session-resolution.ts`, the boundary module.
|
|
14
|
+
*/
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
|
|
17
|
+
export const SKC_DIR = ".skc";
|
|
18
|
+
export const SKC_SESSION_PREFIX = "_session-";
|
|
19
|
+
export const SKC_SESSION_ACTIVITY_FILE = ".session-activity.json";
|
|
20
|
+
|
|
21
|
+
/** Source that produced a resolved SKC session id, for audit/diagnostics. */
|
|
22
|
+
export type SkcSessionSource = "flag" | "payload" | "env" | "latest";
|
|
23
|
+
|
|
24
|
+
export interface SkcSessionContext {
|
|
25
|
+
skcSessionId: string;
|
|
26
|
+
sessionRoot: string;
|
|
27
|
+
source: SkcSessionSource;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encode a session id into a single safe path segment. Matches the historical
|
|
32
|
+
* encoding used across the runtimes so ids round-trip identically:
|
|
33
|
+
* `encodeURIComponent` plus dot-escaping (dots are legal in filenames but we
|
|
34
|
+
* avoid `.`/`..` traversal ambiguity).
|
|
35
|
+
*/
|
|
36
|
+
export function encodeSessionSegment(value: string): string {
|
|
37
|
+
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Inverse of {@link encodeSessionSegment}. */
|
|
41
|
+
export function decodeSessionSegment(segment: string): string {
|
|
42
|
+
return decodeURIComponent(segment.replaceAll("%2E", "."));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Throw when a session id is missing or blank; never let blank suppress callers. */
|
|
46
|
+
export function assertNonEmptySkcSessionId(value: string | undefined, source: string): asserts value is string {
|
|
47
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
48
|
+
throw new Error(`a non-empty SKC session id is required (${source})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Assert a value is safe to use as a single path segment: non-blank and free of
|
|
54
|
+
* path separators or `.`/`..` traversal. Use for already-safe identifiers
|
|
55
|
+
* (skill modes, slugs) where we want identical filenames but fail closed on
|
|
56
|
+
* traversal rather than silently normalizing out of the intended directory.
|
|
57
|
+
*/
|
|
58
|
+
export function assertSafePathComponent(value: string, label: string): void {
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
if (trimmed === "") throw new Error(`${label} is required`);
|
|
61
|
+
if (trimmed === "." || trimmed === ".." || /[/\\]/.test(trimmed)) {
|
|
62
|
+
throw new Error(`${label} must be a safe path component (no separators or traversal): ${value}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The shared `.skc/` root (holds shared config; never session-scoped). */
|
|
67
|
+
export function skcRoot(cwd: string): string {
|
|
68
|
+
return path.join(cwd, SKC_DIR);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The per-session root directory: `<cwd>/.skc/_session-{encodedId}`. */
|
|
72
|
+
export function sessionRoot(cwd: string, skcSessionId: string): string {
|
|
73
|
+
assertNonEmptySkcSessionId(skcSessionId, "sessionRoot");
|
|
74
|
+
return path.join(skcRoot(cwd), `${SKC_SESSION_PREFIX}${encodeSessionSegment(skcSessionId)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Directory name (no path) for a session id, e.g. `_session-abc`. */
|
|
78
|
+
export function sessionDirName(skcSessionId: string): string {
|
|
79
|
+
assertNonEmptySkcSessionId(skcSessionId, "sessionDirName");
|
|
80
|
+
return `${SKC_SESSION_PREFIX}${encodeSessionSegment(skcSessionId)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Return the decoded session id for a `_session-*` directory name, else undefined. */
|
|
84
|
+
export function sessionIdFromDirName(name: string): string | undefined {
|
|
85
|
+
if (!name.startsWith(SKC_SESSION_PREFIX)) return undefined;
|
|
86
|
+
const suffix = name.slice(SKC_SESSION_PREFIX.length);
|
|
87
|
+
if (suffix === "") return undefined;
|
|
88
|
+
let decoded: string;
|
|
89
|
+
try {
|
|
90
|
+
decoded = decodeSessionSegment(suffix);
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return decoded.trim() === "" ? undefined : decoded;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Authoritative per-session activity marker path. */
|
|
98
|
+
export function sessionActivityPath(cwd: string, skcSessionId: string): string {
|
|
99
|
+
return path.join(sessionRoot(cwd, skcSessionId), SKC_SESSION_ACTIVITY_FILE);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- Top-level per-category subdir resolvers ----
|
|
103
|
+
|
|
104
|
+
export function sessionStateDir(cwd: string, skcSessionId: string): string {
|
|
105
|
+
return path.join(sessionRoot(cwd, skcSessionId), "state");
|
|
106
|
+
}
|
|
107
|
+
export function sessionSpecsDir(cwd: string, skcSessionId: string): string {
|
|
108
|
+
return path.join(sessionRoot(cwd, skcSessionId), "specs");
|
|
109
|
+
}
|
|
110
|
+
export function sessionPlansDir(cwd: string, skcSessionId: string): string {
|
|
111
|
+
return path.join(sessionRoot(cwd, skcSessionId), "plans");
|
|
112
|
+
}
|
|
113
|
+
export function sessionUltragoalDir(cwd: string, skcSessionId: string): string {
|
|
114
|
+
return path.join(sessionRoot(cwd, skcSessionId), "ultragoal");
|
|
115
|
+
}
|
|
116
|
+
export function sessionAuditDir(cwd: string, skcSessionId: string): string {
|
|
117
|
+
return path.join(sessionRoot(cwd, skcSessionId), "audit");
|
|
118
|
+
}
|
|
119
|
+
export function sessionReportsDir(cwd: string, skcSessionId: string): string {
|
|
120
|
+
return path.join(sessionRoot(cwd, skcSessionId), "reports");
|
|
121
|
+
}
|
|
122
|
+
export function sessionLogsDir(cwd: string, skcSessionId: string): string {
|
|
123
|
+
return path.join(sessionRoot(cwd, skcSessionId), "logs");
|
|
124
|
+
}
|
|
125
|
+
export function sessionRuntimeDir(cwd: string, skcSessionId: string): string {
|
|
126
|
+
return path.join(sessionRoot(cwd, skcSessionId), "runtime");
|
|
127
|
+
}
|
|
128
|
+
export function sessionRlmDir(cwd: string, skcSessionId: string): string {
|
|
129
|
+
return path.join(sessionRoot(cwd, skcSessionId), "rlm");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- Nested resolvers under <sessionRoot>/state ----
|
|
133
|
+
|
|
134
|
+
export function activeStateDir(cwd: string, skcSessionId: string): string {
|
|
135
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "active");
|
|
136
|
+
}
|
|
137
|
+
export function activeSnapshotPath(cwd: string, skcSessionId: string): string {
|
|
138
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "skill-active-state.json");
|
|
139
|
+
}
|
|
140
|
+
export function activeEntryPath(cwd: string, skcSessionId: string, skill: string): string {
|
|
141
|
+
const normalized = skill.trim();
|
|
142
|
+
if (normalized === "") throw new Error("skill is required");
|
|
143
|
+
return path.join(activeStateDir(cwd, skcSessionId), `${encodeSessionSegment(normalized)}.json`);
|
|
144
|
+
}
|
|
145
|
+
export function modeStatePath(cwd: string, skcSessionId: string, mode: string): string {
|
|
146
|
+
const normalized = mode.trim();
|
|
147
|
+
assertSafePathComponent(normalized, "mode");
|
|
148
|
+
return path.join(sessionStateDir(cwd, skcSessionId), `${normalized}-state.json`);
|
|
149
|
+
}
|
|
150
|
+
export function auditPath(cwd: string, skcSessionId: string): string {
|
|
151
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "audit.jsonl");
|
|
152
|
+
}
|
|
153
|
+
export function transactionJournalPath(cwd: string, skcSessionId: string, mutationId: string): string {
|
|
154
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "transactions", `${encodeSessionSegment(mutationId)}.json`);
|
|
155
|
+
}
|
|
156
|
+
export function teamStateRoot(cwd: string, skcSessionId: string): string {
|
|
157
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "team");
|
|
158
|
+
}
|
|
159
|
+
export function workflowGatePath(cwd: string, skcSessionId: string, gateId: string): string {
|
|
160
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "workflow-gates", `${encodeSessionSegment(gateId)}.json`);
|
|
161
|
+
}
|
|
162
|
+
export function harnessStateRoot(cwd: string, skcSessionId: string): string {
|
|
163
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "harness");
|
|
164
|
+
}
|
|
165
|
+
export function coordinatorMcpStateRoot(cwd: string, skcSessionId: string): string {
|
|
166
|
+
return path.join(sessionStateDir(cwd, skcSessionId), "coordinator-mcp");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- Nested resolvers under other top-level categories ----
|
|
170
|
+
|
|
171
|
+
export function tmuxRuntimeSessionPath(cwd: string, skcSessionId: string, slug: string): string {
|
|
172
|
+
const normalized = slug.trim();
|
|
173
|
+
assertSafePathComponent(normalized, "slug");
|
|
174
|
+
return path.join(sessionRuntimeDir(cwd, skcSessionId), "tmux-sessions", `${normalized}.json`);
|
|
175
|
+
}
|
|
176
|
+
export function rlmArtifactRoot(cwd: string, skcSessionId: string, rlmSessionId: string): string {
|
|
177
|
+
const normalized = rlmSessionId.trim();
|
|
178
|
+
if (normalized === "") throw new Error("rlmSessionId is required");
|
|
179
|
+
return path.join(sessionRlmDir(cwd, skcSessionId), encodeSessionSegment(normalized));
|
|
180
|
+
}
|