@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
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
type EmergencyCompactionSample,
|
|
45
45
|
emergencyCompactionReason,
|
|
46
46
|
estimateMessageTokensHeuristic,
|
|
47
|
-
estimateTokens,
|
|
48
47
|
generateBranchSummary,
|
|
49
48
|
generateHandoff,
|
|
50
49
|
prepareCompaction,
|
|
@@ -217,6 +216,11 @@ import { MCPManager } from "../runtime-mcp/manager";
|
|
|
217
216
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
218
217
|
import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
|
|
219
218
|
import { buildSkcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../skc-runtime/goal-mode-request";
|
|
219
|
+
import {
|
|
220
|
+
assertNonEmptySkcSessionId,
|
|
221
|
+
modeStatePath as sessionModeStatePath,
|
|
222
|
+
sessionStateDir,
|
|
223
|
+
} from "../skc-runtime/session-layout";
|
|
220
224
|
import { persistCoordinatorRuntimeStateFromEvent } from "../skc-runtime/session-state-sidecar";
|
|
221
225
|
import { writeArtifact } from "../skc-runtime/state-writer";
|
|
222
226
|
import { requestSkcWorkerIntegrationAttempt } from "../skc-runtime/team-runtime";
|
|
@@ -225,7 +229,7 @@ import {
|
|
|
225
229
|
readVisibleSkillActiveState,
|
|
226
230
|
syncSkillActiveState,
|
|
227
231
|
} from "../skill-state/active-state";
|
|
228
|
-
import {
|
|
232
|
+
import { assertWorkflowMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
|
|
229
233
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
230
234
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
231
235
|
import {
|
|
@@ -313,13 +317,6 @@ export type AgentSessionEvent =
|
|
|
313
317
|
| { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
|
|
314
318
|
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
315
319
|
|
|
316
|
-
/**
|
|
317
|
-
* Safe path component pattern used to validate session-id segments before
|
|
318
|
-
* joining them into `.skc/state` paths. Mirrors the regex used by the
|
|
319
|
-
* `skc state` runtime selector resolver.
|
|
320
|
-
*/
|
|
321
|
-
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
322
|
-
|
|
323
320
|
function isUnderProjectSkc(cwd: string, targetPath: string): boolean {
|
|
324
321
|
const relative = path.relative(path.join(path.resolve(cwd), ".skc"), path.resolve(targetPath));
|
|
325
322
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -1371,21 +1368,17 @@ export class AgentSession {
|
|
|
1371
1368
|
getActiveSkillPhase(): string | undefined {
|
|
1372
1369
|
const active = this.#activeSkillState;
|
|
1373
1370
|
if (!active) return undefined;
|
|
1374
|
-
// Path safety: refuse to read mode-state files when the skill or
|
|
1375
|
-
// session-id are not safe path components. The `skill` tool
|
|
1376
|
-
// interprets undefined as a non-terminal phase, so chaining is
|
|
1377
|
-
// refused — there is no risk of bypassing the guard via a custom
|
|
1378
|
-
// skill name with `..` or a session-id with separators.
|
|
1379
1371
|
if (!isCanonicalSkcWorkflowSkill(active.skill)) return undefined;
|
|
1380
|
-
|
|
1381
|
-
return undefined;
|
|
1382
|
-
}
|
|
1372
|
+
const sessionId = active.sessionId ?? this.sessionManager.getSessionId();
|
|
1383
1373
|
try {
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const filePath = path.join(
|
|
1374
|
+
assertNonEmptySkcSessionId(sessionId, "AgentSession.getActiveSkillPhase");
|
|
1375
|
+
// Keep the session-state-dir construction explicit here so the chain guard
|
|
1376
|
+
// refuses to fall back to a legacy root `.skc/state` read.
|
|
1377
|
+
const stateDir = sessionStateDir(this.sessionManager.getCwd(), sessionId);
|
|
1378
|
+
const filePath = path.join(
|
|
1379
|
+
stateDir,
|
|
1380
|
+
path.basename(sessionModeStatePath(this.sessionManager.getCwd(), sessionId, active.skill)),
|
|
1381
|
+
);
|
|
1389
1382
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1390
1383
|
const parsed = JSON.parse(raw) as { current_phase?: unknown };
|
|
1391
1384
|
return typeof parsed.current_phase === "string" ? parsed.current_phase : undefined;
|
|
@@ -1475,7 +1468,7 @@ export class AgentSession {
|
|
|
1475
1468
|
}
|
|
1476
1469
|
const sanitized = sanitizeMessage(providerMessages[i]!);
|
|
1477
1470
|
if (!sanitized) continue;
|
|
1478
|
-
const messageTokens =
|
|
1471
|
+
const messageTokens = estimateMessageTokensHeuristic(sanitized);
|
|
1479
1472
|
if (maxTokens > 0 && approximateTokens + messageTokens > maxTokens) {
|
|
1480
1473
|
recordSkip("token-limit");
|
|
1481
1474
|
continue;
|
|
@@ -3764,7 +3757,7 @@ export class AgentSession {
|
|
|
3764
3757
|
* prompts or tool execution can run.
|
|
3765
3758
|
*/
|
|
3766
3759
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3767
|
-
if (!["edit", "write", "ast_edit"
|
|
3760
|
+
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
3768
3761
|
return new Proxy(tool, {
|
|
3769
3762
|
get: (target, prop) => {
|
|
3770
3763
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -3775,7 +3768,7 @@ export class AgentSession {
|
|
|
3775
3768
|
onUpdate: never,
|
|
3776
3769
|
ctx: never,
|
|
3777
3770
|
) => {
|
|
3778
|
-
await
|
|
3771
|
+
await assertWorkflowMutationAllowed({
|
|
3779
3772
|
cwd: this.sessionManager.getCwd(),
|
|
3780
3773
|
sessionId: this.sessionManager.getSessionId(),
|
|
3781
3774
|
tool: target,
|
|
@@ -9901,9 +9894,9 @@ export class AgentSession {
|
|
|
9901
9894
|
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
9902
9895
|
tokens: number;
|
|
9903
9896
|
} {
|
|
9904
|
-
const estimate = this.#estimateContextTokensWith(message => this.#
|
|
9897
|
+
const estimate = this.#estimateContextTokensWith(message => this.#estimateMessageCompactionDeltaTokens(message));
|
|
9905
9898
|
return {
|
|
9906
|
-
tokens: estimate.tokens + this.#
|
|
9899
|
+
tokens: estimate.tokens + this.#estimateMessagesCompactionDeltaTokens(pendingMessages),
|
|
9907
9900
|
};
|
|
9908
9901
|
}
|
|
9909
9902
|
|
|
@@ -9949,10 +9942,10 @@ export class AgentSession {
|
|
|
9949
9942
|
};
|
|
9950
9943
|
}
|
|
9951
9944
|
|
|
9952
|
-
#
|
|
9945
|
+
#estimateMessagesCompactionDeltaTokens(messages: readonly AgentMessage[]): number {
|
|
9953
9946
|
let tokens = 0;
|
|
9954
9947
|
for (const message of messages) {
|
|
9955
|
-
tokens += this.#
|
|
9948
|
+
tokens += this.#estimateMessageCompactionDeltaTokens(message);
|
|
9956
9949
|
}
|
|
9957
9950
|
return tokens;
|
|
9958
9951
|
}
|
|
@@ -9965,11 +9958,17 @@ export class AgentSession {
|
|
|
9965
9958
|
return tokens;
|
|
9966
9959
|
}
|
|
9967
9960
|
|
|
9968
|
-
|
|
9961
|
+
/**
|
|
9962
|
+
* Conservative inflation applied to the native-free chars/4 estimate of the
|
|
9963
|
+
* UNSENT context delta. chars/4 undercounts dense code/CJK, so we bias high
|
|
9964
|
+
* to compact slightly early rather than overflow the model window before the
|
|
9965
|
+
* next provider response re-anchors the exact count.
|
|
9966
|
+
*/
|
|
9967
|
+
#compactionDeltaInflation = 1.2;
|
|
9968
|
+
#compactionDeltaTokenCache = new WeakMap<AgentMessage, { len: number; tokens: number }>();
|
|
9969
9969
|
|
|
9970
|
-
/** Cheap content-size signal to invalidate the native token cache on mutation (growth). */
|
|
9971
9970
|
/**
|
|
9972
|
-
* Cheap content-size signal to invalidate the
|
|
9971
|
+
* Cheap content-size signal to invalidate the compaction-delta token cache on mutation. Recursively
|
|
9973
9972
|
* sums string lengths across the whole message (depth-bounded), so it covers every
|
|
9974
9973
|
* provider-visible shape (text/thinking/tool args, toolResult output, tool names, etc.)
|
|
9975
9974
|
* without allocating a serialized copy. A size-preserving in-place edit yields only a
|
|
@@ -9992,19 +9991,22 @@ export class AgentSession {
|
|
|
9992
9991
|
return 0;
|
|
9993
9992
|
}
|
|
9994
9993
|
|
|
9995
|
-
#
|
|
9996
|
-
//
|
|
9997
|
-
//
|
|
9998
|
-
//
|
|
9999
|
-
//
|
|
9994
|
+
#estimateMessageCompactionDeltaTokens(message: AgentMessage): number {
|
|
9995
|
+
// Provider usage anchors the already-sent context (see calculatePromptTokens); this
|
|
9996
|
+
// estimates only the UNSENT delta with the native-free chars/4 heuristic, inflated by
|
|
9997
|
+
// #compactionDeltaInflation so dense input cannot undercount us past the compaction
|
|
9998
|
+
// threshold before the next provider response re-anchors the exact count. Cached per
|
|
9999
|
+
// message object, invalidated by a cheap content-size signal; a rare size-preserving
|
|
10000
|
+
// in-place edit yields only a benign estimate drift, never wrong output.
|
|
10000
10001
|
const len = this.#messageTokenSize(message);
|
|
10001
|
-
const cached = this.#
|
|
10002
|
+
const cached = this.#compactionDeltaTokenCache.get(message);
|
|
10002
10003
|
if (cached && cached.len === len) return cached.tokens;
|
|
10003
|
-
let
|
|
10004
|
+
let heuristic = 0;
|
|
10004
10005
|
for (const llmMessage of convertToLlm([message])) {
|
|
10005
|
-
|
|
10006
|
+
heuristic += estimateMessageTokensHeuristic(llmMessage);
|
|
10006
10007
|
}
|
|
10007
|
-
|
|
10008
|
+
const tokens = Math.ceil(heuristic * this.#compactionDeltaInflation);
|
|
10009
|
+
this.#compactionDeltaTokenCache.set(message, { len, tokens });
|
|
10008
10010
|
return tokens;
|
|
10009
10011
|
}
|
|
10010
10012
|
|
|
@@ -1129,6 +1129,23 @@ function formatTimeAgo(date: Date): string {
|
|
|
1129
1129
|
return date.toLocaleDateString();
|
|
1130
1130
|
}
|
|
1131
1131
|
|
|
1132
|
+
async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
|
|
1133
|
+
try {
|
|
1134
|
+
await fs.promises.rename(source, destination);
|
|
1135
|
+
return;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (!hasFsCode(error, "EXDEV")) throw error;
|
|
1138
|
+
}
|
|
1139
|
+
const stat = await fs.promises.stat(source);
|
|
1140
|
+
if (stat.isDirectory()) {
|
|
1141
|
+
await fs.promises.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
1142
|
+
await fs.promises.rm(source, { recursive: true, force: false });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
|
|
1146
|
+
await fs.promises.unlink(source);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1132
1149
|
const MAX_PERSIST_CHARS = 500_000;
|
|
1133
1150
|
const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
1134
1151
|
/** Minimum base64 length to externalize to blob store (skip tiny inline images) */
|
|
@@ -2498,14 +2515,14 @@ export class SessionManager {
|
|
|
2498
2515
|
try {
|
|
2499
2516
|
// Guard: session file may not exist yet (no assistant messages persisted)
|
|
2500
2517
|
if (hadSessionFile) {
|
|
2501
|
-
await
|
|
2518
|
+
await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
|
|
2502
2519
|
movedSessionFile = true;
|
|
2503
2520
|
}
|
|
2504
2521
|
|
|
2505
2522
|
try {
|
|
2506
2523
|
const stat = await fs.promises.stat(oldArtifactDir);
|
|
2507
2524
|
if (stat.isDirectory()) {
|
|
2508
|
-
await
|
|
2525
|
+
await movePathAcrossDevicesSafe(oldArtifactDir, newArtifactDir);
|
|
2509
2526
|
movedArtifactDir = true;
|
|
2510
2527
|
}
|
|
2511
2528
|
} catch (err) {
|
|
@@ -29,6 +29,14 @@ The Hermes bridge does not choose a model/provider. Generated setup configures `
|
|
|
29
29
|
|
|
30
30
|
Provider-specific commands are examples only, never product defaults.
|
|
31
31
|
|
|
32
|
+
## Visible routed-session fallback
|
|
33
|
+
|
|
34
|
+
If a Hermes/OpenClaw/Clawhip-style operator needs a human-visible, channel-routed SKC pane instead of a pure Coordinator MCP session, use the visible session pattern in [`docs/skc-session-clawhip-routing.md`](../../../../../../docs/skc-session-clawhip-routing.md).
|
|
35
|
+
|
|
36
|
+
Use that pattern only when the router must watch tmux output, send stale-session alerts, or inject follow-up prompts into the same visible pane. The short version is: prepare a dedicated worktree, register a stable tmux session through the host router, start interactive `skc`, wait for TUI readiness, inject the task prompt separately, and verify actual tool/work activity before reporting acceptance.
|
|
37
|
+
|
|
38
|
+
Do not put private channel ids, mention targets, socket names, tokens, or local routing policy into portable setup output. Keep those in the host/operator deployment.
|
|
39
|
+
|
|
32
40
|
## Safety
|
|
33
41
|
|
|
34
42
|
- Mutating tools require bridge startup mutation classes and per-call consent.
|
|
@@ -404,7 +404,7 @@ async function installConfig(spec: CoordinatorSetupSpec, force: boolean): Promis
|
|
|
404
404
|
|
|
405
405
|
async function runSmoke(spec: CoordinatorSetupSpec): Promise<HermesSetupResult["smoke"]> {
|
|
406
406
|
const requiredTools = [...COORDINATOR_MCP_TOOL_NAMES];
|
|
407
|
-
const server = createCoordinatorMcpServer({ env:
|
|
407
|
+
const server = createCoordinatorMcpServer({ env: renderHermesServerBlock(spec).env as NodeJS.ProcessEnv });
|
|
408
408
|
const listed = await server.handleJsonRpc({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
|
|
409
409
|
const listedResult = isRecord(listed.result) ? listed.result : {};
|
|
410
410
|
const tools = Array.isArray(listedResult.tools) ? listedResult.tools : [];
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
normalizeDeepInterviewEnvelope,
|
|
12
12
|
questionHash,
|
|
13
13
|
} from "./deep-interview-state";
|
|
14
|
-
import {
|
|
14
|
+
import { writeSessionActivityMarker } from "./session-resolution";
|
|
15
|
+
import { readExistingStateForMutation, writeGuardedWorkflowEnvelopeAtomic } from "./state-writer";
|
|
15
16
|
|
|
16
17
|
export * from "./deep-interview-state";
|
|
17
18
|
|
|
@@ -298,6 +299,12 @@ async function readEnvelope(statePath: string): Promise<DeepInterviewStateEnvelo
|
|
|
298
299
|
return ensureDeepInterviewStateShape(undefined);
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
function existingStateRevision(value: unknown): number | undefined {
|
|
303
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
304
|
+
const revision = (value as Record<string, unknown>).state_revision;
|
|
305
|
+
return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
301
308
|
function interviewIdOf(envelope: DeepInterviewStateEnvelope): string | undefined {
|
|
302
309
|
const inner = (envelope.state ?? {}) as Record<string, unknown>;
|
|
303
310
|
return typeof inner.interview_id === "string" ? inner.interview_id : undefined;
|
|
@@ -310,6 +317,7 @@ async function persistEnvelope(
|
|
|
310
317
|
sessionId: string | undefined,
|
|
311
318
|
command: string,
|
|
312
319
|
): Promise<void> {
|
|
320
|
+
if (!sessionId) throw new Error("deep-interview recorder requires a session id");
|
|
313
321
|
const now = new Date().toISOString();
|
|
314
322
|
const payload: Record<string, unknown> = { ...normalizeDeepInterviewEnvelope(envelope), updated_at: now };
|
|
315
323
|
// Guarantee RequiredOnWriteEnvelopeSchema fields for the fresh/absent fallback;
|
|
@@ -318,11 +326,22 @@ async function persistEnvelope(
|
|
|
318
326
|
payload.version ??= WORKFLOW_STATE_VERSION;
|
|
319
327
|
payload.active ??= true;
|
|
320
328
|
payload.current_phase ??= "interviewing";
|
|
321
|
-
|
|
329
|
+
const expectedRevision = existingStateRevision(envelope);
|
|
330
|
+
const writeResult = await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
|
|
322
331
|
cwd,
|
|
332
|
+
policy: "source",
|
|
333
|
+
expectedRevision,
|
|
323
334
|
receipt: { cwd, skill: "deep-interview", owner: "skc-runtime", command, sessionId, nowIso: now },
|
|
324
|
-
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "deep-interview" },
|
|
335
|
+
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "deep-interview", sessionId },
|
|
325
336
|
});
|
|
337
|
+
// Reflect the freshly written revision back onto the in-memory envelope so a
|
|
338
|
+
// follow-up HUD sync derives its `sourceRevision` from the persisted revision
|
|
339
|
+
// (not the stale pre-write value), otherwise the active-state writer treats the
|
|
340
|
+
// newer HUD as stale and skips it (e.g. dropping the ambiguity chip after scoring).
|
|
341
|
+
if (writeResult.written && typeof expectedRevision === "number") {
|
|
342
|
+
(envelope as Record<string, unknown>).state_revision = expectedRevision + 1;
|
|
343
|
+
}
|
|
344
|
+
await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-recorder", path: statePath });
|
|
326
345
|
}
|
|
327
346
|
|
|
328
347
|
/**
|
|
@@ -335,20 +354,17 @@ async function syncRecorderHud(
|
|
|
335
354
|
envelope: DeepInterviewStateEnvelope,
|
|
336
355
|
sessionId: string | undefined,
|
|
337
356
|
): Promise<void> {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} catch {
|
|
350
|
-
// HUD sync is best-effort cache maintenance and must not change record semantics.
|
|
351
|
-
}
|
|
357
|
+
const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
|
|
358
|
+
await syncSkillActiveState({
|
|
359
|
+
cwd,
|
|
360
|
+
skill: "deep-interview",
|
|
361
|
+
active: phase !== "complete",
|
|
362
|
+
phase,
|
|
363
|
+
sessionId,
|
|
364
|
+
source: "skc-runtime-deep-interview-recorder",
|
|
365
|
+
hud: deriveDeepInterviewHud(envelope as Record<string, unknown>, { phase }),
|
|
366
|
+
sourceRevision: existingStateRevision(envelope),
|
|
367
|
+
});
|
|
352
368
|
}
|
|
353
369
|
|
|
354
370
|
/**
|
|
@@ -360,6 +376,19 @@ async function repairRecorderHudFromPersisted(
|
|
|
360
376
|
cwd: string,
|
|
361
377
|
statePath: string,
|
|
362
378
|
sessionId: string | undefined,
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
try {
|
|
381
|
+
await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
|
|
382
|
+
} catch {
|
|
383
|
+
// HUD sync is best-effort cache maintenance and must not change record semantics.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Refresh the best-effort HUD cache from persisted deep-interview state. */
|
|
388
|
+
export async function syncDeepInterviewRecorderHud(
|
|
389
|
+
cwd: string,
|
|
390
|
+
statePath: string,
|
|
391
|
+
sessionId: string | undefined,
|
|
363
392
|
): Promise<void> {
|
|
364
393
|
const read = await readExistingStateForMutation(statePath);
|
|
365
394
|
if (read.kind !== "valid") return;
|
|
@@ -384,7 +413,11 @@ export async function appendOrMergeDeepInterviewRound(
|
|
|
384
413
|
}
|
|
385
414
|
(envelope.state as Record<string, unknown>).rounds = result.rounds;
|
|
386
415
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "skc deep-interview record-answer");
|
|
387
|
-
|
|
416
|
+
try {
|
|
417
|
+
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
418
|
+
} catch {
|
|
419
|
+
// HUD sync is best-effort cache maintenance and must not change record semantics.
|
|
420
|
+
}
|
|
388
421
|
return { action: result.action, record: result.record };
|
|
389
422
|
}
|
|
390
423
|
|
|
@@ -7,6 +7,8 @@ import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
|
7
7
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
8
8
|
import { normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
9
9
|
import { runNativeRalplanCommand } from "./ralplan-runtime";
|
|
10
|
+
import { modeStatePath, sessionSpecsDir } from "./session-layout";
|
|
11
|
+
import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
10
12
|
import { runNativeStateCommand } from "./state-runtime";
|
|
11
13
|
import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
|
|
12
14
|
|
|
@@ -76,10 +78,6 @@ function assertSafePathComponent(value: string, label: string): void {
|
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function encodeSessionSegment(value: string): string {
|
|
80
|
-
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
81
|
function defaultSpecSlug(now: Date = new Date()): string {
|
|
84
82
|
const yyyy = now.getUTCFullYear().toString().padStart(4, "0");
|
|
85
83
|
const mm = (now.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
@@ -89,14 +87,10 @@ function defaultSpecSlug(now: Date = new Date()): string {
|
|
|
89
87
|
return `${yyyy}-${mm}-${dd}-${hh}${min}-${randomBytes(2).toString("hex")}`;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function deepInterviewStatePath(cwd: string, sessionId: string | undefined): string {
|
|
99
|
-
return path.join(stateDirFor(cwd, sessionId), "deep-interview-state.json");
|
|
90
|
+
export function deepInterviewStatePath(cwd: string, sessionId?: string): string {
|
|
91
|
+
const resolvedSessionId = sessionId?.trim() || process.env.SKC_SESSION_ID?.trim();
|
|
92
|
+
if (!resolvedSessionId) throw new Error("deep-interview state path requires a session id");
|
|
93
|
+
return modeStatePath(cwd, resolvedSessionId, "deep-interview");
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string> {
|
|
@@ -117,7 +111,7 @@ interface ResolvedDeepInterviewArgs {
|
|
|
117
111
|
resolution: DeepInterviewResolution;
|
|
118
112
|
threshold: number;
|
|
119
113
|
thresholdSource: string;
|
|
120
|
-
sessionId
|
|
114
|
+
sessionId: string;
|
|
121
115
|
idea: string;
|
|
122
116
|
language?: DeepInterviewLanguagePreference;
|
|
123
117
|
json: boolean;
|
|
@@ -134,7 +128,7 @@ export interface ResolvedDeepInterviewSpecWriteArgs {
|
|
|
134
128
|
stage: "final";
|
|
135
129
|
slug: string;
|
|
136
130
|
spec: string;
|
|
137
|
-
sessionId
|
|
131
|
+
sessionId: string;
|
|
138
132
|
json: boolean;
|
|
139
133
|
deliberate: boolean;
|
|
140
134
|
handoff?: "ralplan";
|
|
@@ -277,8 +271,12 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
|
|
|
277
271
|
throw new DeepInterviewCommandError(2, "--spec is required for deep-interview --write");
|
|
278
272
|
}
|
|
279
273
|
|
|
280
|
-
const
|
|
281
|
-
|
|
274
|
+
const session = resolveSkcSessionForWrite(cwd, {
|
|
275
|
+
flagValue: flagValue(args, "--session-id"),
|
|
276
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
277
|
+
});
|
|
278
|
+
const sessionId = session.skcSessionId;
|
|
279
|
+
assertSafePathComponent(sessionId, "session-id");
|
|
282
280
|
|
|
283
281
|
const rawHandoff = flagValue(args, "--handoff")?.trim() || undefined;
|
|
284
282
|
if (rawHandoff && rawHandoff !== "ralplan") {
|
|
@@ -324,8 +322,12 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
|
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
|
|
327
|
-
const
|
|
328
|
-
|
|
325
|
+
const session = resolveSkcSessionForWrite(cwd, {
|
|
326
|
+
flagValue: flagValue(args, "--session-id"),
|
|
327
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
328
|
+
});
|
|
329
|
+
const sessionId = session.skcSessionId;
|
|
330
|
+
assertSafePathComponent(sessionId, "session-id");
|
|
329
331
|
|
|
330
332
|
const explicitResolutions = (["quick", "standard", "deep"] as const).filter(name => hasFlag(args, `--${name}`));
|
|
331
333
|
if (explicitResolutions.length > 1) {
|
|
@@ -402,19 +404,34 @@ export async function persistDeepInterviewSpec(
|
|
|
402
404
|
}
|
|
403
405
|
const existing = existingRead.kind === "valid" ? existingRead.value : {};
|
|
404
406
|
|
|
405
|
-
const specPath = path.join(cwd,
|
|
407
|
+
const specPath = path.join(sessionSpecsDir(cwd, resolved.sessionId), `deep-interview-${resolved.slug}.md`);
|
|
406
408
|
const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
|
|
407
409
|
await writeArtifact(specPath, content, {
|
|
408
410
|
cwd,
|
|
409
|
-
audit: {
|
|
411
|
+
audit: {
|
|
412
|
+
category: "artifact",
|
|
413
|
+
verb: "write",
|
|
414
|
+
owner: "skc-runtime",
|
|
415
|
+
skill: "deep-interview",
|
|
416
|
+
sessionId: resolved.sessionId,
|
|
417
|
+
},
|
|
410
418
|
});
|
|
411
419
|
|
|
412
420
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
413
421
|
const createdAt = new Date().toISOString();
|
|
414
422
|
await appendJsonl(
|
|
415
|
-
path.join(cwd,
|
|
423
|
+
path.join(sessionSpecsDir(cwd, resolved.sessionId), "deep-interview-index.jsonl"),
|
|
416
424
|
{ slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 },
|
|
417
|
-
{
|
|
425
|
+
{
|
|
426
|
+
cwd,
|
|
427
|
+
audit: {
|
|
428
|
+
category: "ledger",
|
|
429
|
+
verb: "append",
|
|
430
|
+
owner: "skc-runtime",
|
|
431
|
+
skill: "deep-interview",
|
|
432
|
+
sessionId: resolved.sessionId,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
418
435
|
);
|
|
419
436
|
|
|
420
437
|
const payload = normalizeDeepInterviewEnvelope({
|
|
@@ -446,9 +463,11 @@ export async function persistDeepInterviewSpec(
|
|
|
446
463
|
verb: "write",
|
|
447
464
|
owner: "skc-runtime",
|
|
448
465
|
skill: "deep-interview",
|
|
466
|
+
sessionId: resolved.sessionId,
|
|
449
467
|
forced: resolved.force,
|
|
450
468
|
},
|
|
451
469
|
});
|
|
470
|
+
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "deep-interview-runtime", path: statePath });
|
|
452
471
|
await syncDeepInterviewHud({
|
|
453
472
|
cwd,
|
|
454
473
|
sessionId: resolved.sessionId,
|
|
@@ -503,8 +522,15 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
|
|
|
503
522
|
sessionId: resolved.sessionId,
|
|
504
523
|
nowIso: now,
|
|
505
524
|
},
|
|
506
|
-
audit: {
|
|
525
|
+
audit: {
|
|
526
|
+
category: "state",
|
|
527
|
+
verb: "write",
|
|
528
|
+
owner: "skc-runtime",
|
|
529
|
+
skill: "deep-interview",
|
|
530
|
+
sessionId: resolved.sessionId,
|
|
531
|
+
},
|
|
507
532
|
});
|
|
533
|
+
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "deep-interview-runtime", path: statePath });
|
|
508
534
|
await syncDeepInterviewHud({ cwd, sessionId: resolved.sessionId, payload, phase: "interviewing" });
|
|
509
535
|
return statePath;
|
|
510
536
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type ModeChangeEntry,
|
|
9
9
|
type SessionEntry,
|
|
10
10
|
} from "../session/session-manager";
|
|
11
|
+
import { sessionStateDir, sessionUltragoalDir } from "./session-layout";
|
|
12
|
+
import { resolveSkcSessionForRead, resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
11
13
|
import { removeFileAudited, writeJsonAtomic } from "./state-writer";
|
|
12
14
|
|
|
13
15
|
export const SKC_SESSION_FILE_ENV = "SKC_SESSION_FILE";
|
|
@@ -48,12 +50,12 @@ function isEnoent(error: unknown): boolean {
|
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function requestPath(cwd: string): string {
|
|
52
|
-
return path.join(cwd,
|
|
53
|
+
function requestPath(cwd: string, sessionId: string): string {
|
|
54
|
+
return path.join(sessionStateDir(cwd, sessionId), "goal-mode-request.json");
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function ultragoalGoalsPath(cwd: string): string {
|
|
56
|
-
return path.join(cwd,
|
|
57
|
+
function ultragoalGoalsPath(cwd: string, sessionId: string): string {
|
|
58
|
+
return path.join(sessionUltragoalDir(cwd, sessionId), "goals.json");
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function isCreateGoalsArg(value: string): boolean {
|
|
@@ -65,8 +67,14 @@ export function isUltragoalCreateGoalsInvocation(args: readonly string[]): boole
|
|
|
65
67
|
return command !== undefined && isCreateGoalsArg(command);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
export async function readUltragoalSkcObjective(
|
|
69
|
-
|
|
70
|
+
export async function readUltragoalSkcObjective(
|
|
71
|
+
cwd: string,
|
|
72
|
+
sessionId?: string | null,
|
|
73
|
+
): Promise<{ objective: string; goalsPath: string }> {
|
|
74
|
+
const session = sessionId?.trim()
|
|
75
|
+
? { skcSessionId: sessionId.trim() }
|
|
76
|
+
: await resolveSkcSessionForRead(cwd, { envSessionId: process.env.SKC_SESSION_ID });
|
|
77
|
+
const goalsPath = ultragoalGoalsPath(cwd, session.skcSessionId);
|
|
70
78
|
try {
|
|
71
79
|
const plan = (await Bun.file(goalsPath).json()) as UltragoalPlanShape;
|
|
72
80
|
const objective = typeof plan.skcObjective === "string" ? plan.skcObjective.trim() : "";
|
|
@@ -87,7 +95,10 @@ export async function writePendingGoalModeRequest(input: {
|
|
|
87
95
|
}): Promise<PendingGoalModeRequest> {
|
|
88
96
|
const objective = input.objective.trim();
|
|
89
97
|
if (!objective) throw new Error("goal objective is required");
|
|
90
|
-
const
|
|
98
|
+
const resolvedSessionId =
|
|
99
|
+
input.sessionId?.trim() ||
|
|
100
|
+
resolveSkcSessionForWrite(input.cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId;
|
|
101
|
+
const sessionId = resolvedSessionId;
|
|
91
102
|
const request: PendingGoalModeRequest = {
|
|
92
103
|
version: REQUEST_VERSION,
|
|
93
104
|
kind: "goal_mode_request",
|
|
@@ -97,11 +108,12 @@ export async function writePendingGoalModeRequest(input: {
|
|
|
97
108
|
goalsPath: input.goalsPath,
|
|
98
109
|
...(sessionId ? { sessionId } : {}),
|
|
99
110
|
};
|
|
100
|
-
const filePath = requestPath(input.cwd);
|
|
111
|
+
const filePath = requestPath(input.cwd, sessionId);
|
|
101
112
|
await writeJsonAtomic(filePath, request, {
|
|
102
113
|
cwd: input.cwd,
|
|
103
|
-
audit: { category: "state", verb: "write", owner: "skc-runtime" },
|
|
114
|
+
audit: { category: "state", verb: "write", owner: "skc-runtime", sessionId },
|
|
104
115
|
});
|
|
116
|
+
await writeSessionActivityMarker(input.cwd, sessionId, { writer: "goal-mode-request", path: filePath });
|
|
105
117
|
return request;
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -175,7 +187,10 @@ export async function consumePendingGoalModeRequest(
|
|
|
175
187
|
cwd: string,
|
|
176
188
|
currentSessionId?: string | null,
|
|
177
189
|
): Promise<PendingGoalModeRequest | null> {
|
|
178
|
-
const
|
|
190
|
+
const session = currentSessionId?.trim()
|
|
191
|
+
? { skcSessionId: currentSessionId.trim() }
|
|
192
|
+
: await resolveSkcSessionForRead(cwd, { envSessionId: process.env.SKC_SESSION_ID });
|
|
193
|
+
const filePath = requestPath(cwd, session.skcSessionId);
|
|
179
194
|
let raw: unknown;
|
|
180
195
|
try {
|
|
181
196
|
raw = await Bun.file(filePath).json();
|
|
@@ -203,7 +218,7 @@ export async function consumePendingGoalModeRequest(
|
|
|
203
218
|
}
|
|
204
219
|
await removeFileAudited(filePath, {
|
|
205
220
|
cwd,
|
|
206
|
-
audit: { category: "prune", verb: "remove", owner: "skc-runtime" },
|
|
221
|
+
audit: { category: "prune", verb: "remove", owner: "skc-runtime", sessionId: session.skcSessionId },
|
|
207
222
|
}).catch(error => {
|
|
208
223
|
if (!isEnoent(error)) throw error;
|
|
209
224
|
});
|