@sayknow-cli/coding-agent 0.3.0 → 0.3.2
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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +2 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/defaults/skc-defaults.d.ts +2 -13
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +9 -1
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +133 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/threaded-inbound.d.ts +19 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +25 -0
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +14 -10
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/fast-help.ts +30 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/notify-cli.ts +152 -5
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/commands/team.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +55 -7
- package/src/config/settings-schema.ts +30 -1
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/defaults/skc-defaults.ts +2 -27
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/hooks/skill-state.ts +57 -0
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +56 -11
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/event-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +53 -11
- package/src/modes/interactive-mode.ts +50 -3
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config.ts +23 -2
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +417 -45
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +984 -242
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/threaded-inbound.ts +60 -4
- package/src/notifications/threaded-render.ts +20 -2
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +248 -125
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +85 -22
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +25 -8
- package/src/skc-runtime/ultragoal-runtime.ts +75 -15
- package/src/skc-runtime/workflow-manifest.generated.json +29 -0
- package/src/skc-runtime/workflow-manifest.ts +7 -2
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/fetch.ts +94 -1
- package/src/tools/index.ts +6 -1
- package/src/tools/telegram-send.ts +137 -0
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +159 -0
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { safeStderrWrite } from "@sayknow-cli/utils";
|
|
4
3
|
import { VERSION } from "@sayknow-cli/utils/dirs";
|
|
4
|
+
import { safeStderrWrite } from "@sayknow-cli/utils/safe-stderr";
|
|
5
5
|
import type { Args } from "../cli/args";
|
|
6
6
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
7
7
|
import { SKC_COORDINATOR_SESSION_ID_ENV, SKC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildSkcTmuxProfileCommands,
|
|
10
10
|
buildSkcTmuxSessionName,
|
|
11
11
|
buildSkcTmuxSessionSlug,
|
|
12
|
+
resolveSkcTmuxBinary,
|
|
12
13
|
resolveSkcTmuxCommand,
|
|
13
14
|
SKC_DEFAULT_TMUX_SESSION,
|
|
14
15
|
SKC_TMUX_COMMAND_ENV,
|
|
@@ -31,6 +32,7 @@ export {
|
|
|
31
32
|
export const SKC_TMUX_LAUNCHED_ENV = "SKC_TMUX_LAUNCHED";
|
|
32
33
|
export const SKC_LAUNCH_POLICY_ENV = "SKC_LAUNCH_POLICY";
|
|
33
34
|
export const SKC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
|
|
35
|
+
export const SKC_PSMUX_PROFILE_FORCE_ENV = "SKC_PSMUX_PROFILE_FORCE";
|
|
34
36
|
|
|
35
37
|
type LaunchPolicy = "direct" | "tmux";
|
|
36
38
|
|
|
@@ -93,6 +95,10 @@ function hasCurrentSkcVersion(session: SkcTmuxSessionStatus | undefined): boolea
|
|
|
93
95
|
return session?.version === VERSION;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
|
|
99
|
+
return Boolean(parsed.continue || parsed.resume || explicitTmuxSessionName(env));
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
function findExistingSessionForLaunch(context: {
|
|
97
103
|
env: NodeJS.ProcessEnv;
|
|
98
104
|
project: string;
|
|
@@ -162,6 +168,18 @@ function formatTmuxLaunchDiagnostic(stage: string, stderr?: string): string {
|
|
|
162
168
|
return `skc --tmux failed after creating tmux session: ${stage}.${suffix}\n`;
|
|
163
169
|
}
|
|
164
170
|
|
|
171
|
+
function formatTmuxUnavailableDiagnostic(platform: NodeJS.Platform, tmuxCommand: string): string {
|
|
172
|
+
if (platform === "win32") {
|
|
173
|
+
return (
|
|
174
|
+
`skc --tmux requested but no tmux executable was found; starting without a tmux-backed session. ` +
|
|
175
|
+
`SKC searched for psmux, pmux, and tmux on PATH (got \`${tmuxCommand}\`). ` +
|
|
176
|
+
"Install psmux (https://github.com/psmux/psmux) for native Windows tmux support, or use WSL with real tmux. " +
|
|
177
|
+
"You can also point SKC at a specific binary via SKC_TMUX_COMMAND.\n"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return `skc --tmux requested but no ${tmuxCommand} executable was found; starting without a tmux-backed session.\n`;
|
|
181
|
+
}
|
|
182
|
+
|
|
165
183
|
function shellQuote(value: string): string {
|
|
166
184
|
if (value.length === 0) return "''";
|
|
167
185
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -195,14 +213,26 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
|
|
|
195
213
|
export function applySkcTmuxProfile(context: SkcTmuxProfileContext): SkcTmuxProfileResult {
|
|
196
214
|
const env = context.env ?? process.env;
|
|
197
215
|
const branchSlug = context.branch ? buildSkcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
// The psmux UX filter (mouse / set-clipboard / mode-style /
|
|
217
|
+
// set-window-option) now lives in buildSkcTmuxProfileCommands so every
|
|
218
|
+
// caller — skc --tmux planning, skc session create, skc team bootstrap —
|
|
219
|
+
// applies the same drop set when the active multiplexer is psmux. We pass
|
|
220
|
+
// the resolved tmuxCommand through the new opts seam so the filter
|
|
221
|
+
// engages for this exact command, not whatever the resolver returns at
|
|
222
|
+
// profile-build time.
|
|
223
|
+
const commands = buildSkcTmuxProfileCommands(
|
|
224
|
+
context.target,
|
|
225
|
+
env,
|
|
226
|
+
{
|
|
227
|
+
branch: context.branch ?? null,
|
|
228
|
+
branchSlug,
|
|
229
|
+
project: context.project ?? null,
|
|
230
|
+
sessionId: context.sessionId ?? env[SKC_COORDINATOR_SESSION_ID_ENV] ?? null,
|
|
231
|
+
sessionStateFile: context.sessionStateFile ?? env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
|
|
232
|
+
version: context.version ?? null,
|
|
233
|
+
},
|
|
234
|
+
{ tmuxCommand: context.tmuxCommand },
|
|
235
|
+
);
|
|
206
236
|
if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
|
|
207
237
|
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
208
238
|
const cwd = context.cwd ?? process.cwd();
|
|
@@ -276,20 +306,33 @@ function truncateVisibleTail(value: string, maxWidth: number): string {
|
|
|
276
306
|
return `…${result}`;
|
|
277
307
|
}
|
|
278
308
|
|
|
309
|
+
const SKC_TMUX_WINDOW_BRANCH_SEPARATOR = "-";
|
|
310
|
+
|
|
311
|
+
function sanitizeTmuxWindowTitleSegment(value: string): string {
|
|
312
|
+
return value.replace(/:+/g, "-");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function sanitizeTmuxWindowProjectName(project: string): string {
|
|
316
|
+
const trimmed = project.trim();
|
|
317
|
+
if (!trimmed || /^\.+$/.test(trimmed)) return "skc";
|
|
318
|
+
if (trimmed.startsWith(".")) return sanitizeTmuxWindowTitleSegment(`dot-${trimmed.replace(/^\.+/, "")}`);
|
|
319
|
+
return sanitizeTmuxWindowTitleSegment(trimmed);
|
|
320
|
+
}
|
|
321
|
+
|
|
279
322
|
export function buildSkcTmuxWindowTitle(cwd: string, branch: string | null | undefined): string {
|
|
280
|
-
const project = path.basename(path.resolve(cwd)) || "skc";
|
|
281
|
-
const trimmedBranch = branch?.trim();
|
|
323
|
+
const project = sanitizeTmuxWindowProjectName(path.basename(path.resolve(cwd)) || "skc");
|
|
324
|
+
const trimmedBranch = sanitizeTmuxWindowTitleSegment(branch?.trim() ?? "");
|
|
282
325
|
if (!trimmedBranch) return truncateVisible(project, SKC_TMUX_WINDOW_LABEL_MAX_WIDTH);
|
|
283
326
|
|
|
284
|
-
const separatorWidth = visibleWidth(
|
|
327
|
+
const separatorWidth = visibleWidth(SKC_TMUX_WINDOW_BRANCH_SEPARATOR);
|
|
285
328
|
const projectWidth = visibleWidth(project);
|
|
286
|
-
const fullTitle = `${project}
|
|
329
|
+
const fullTitle = `${project}${SKC_TMUX_WINDOW_BRANCH_SEPARATOR}${trimmedBranch}`;
|
|
287
330
|
if (visibleWidth(fullTitle) <= SKC_TMUX_WINDOW_LABEL_MAX_WIDTH) return fullTitle;
|
|
288
331
|
|
|
289
332
|
const remainingBranchWidth = SKC_TMUX_WINDOW_LABEL_MAX_WIDTH - projectWidth - separatorWidth;
|
|
290
333
|
if (remainingBranchWidth <= 0) return truncateVisible(project, SKC_TMUX_WINDOW_LABEL_MAX_WIDTH);
|
|
291
334
|
|
|
292
|
-
return `${project}
|
|
335
|
+
return `${project}${SKC_TMUX_WINDOW_BRANCH_SEPARATOR}${truncateVisibleTail(trimmedBranch, remainingBranchWidth)}`;
|
|
293
336
|
}
|
|
294
337
|
|
|
295
338
|
function buildTmuxRenameWindowArgs(title: string, target?: string): string[] {
|
|
@@ -311,8 +354,10 @@ function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
|
|
|
311
354
|
if (!env.TMUX || env[SKC_TMUX_LAUNCHED_ENV] === "1") return;
|
|
312
355
|
if (parseLaunchPolicy(env) === "direct") return;
|
|
313
356
|
|
|
314
|
-
|
|
315
|
-
|
|
357
|
+
// Note: Windows is intentionally allowed here. Psmux supports
|
|
358
|
+
// `rename-window` and we want the leader window to inherit the
|
|
359
|
+
// sanitized project-branch title even on native Windows, where
|
|
360
|
+
// skc --tmux runs through PowerShell to a psmux backend.
|
|
316
361
|
|
|
317
362
|
const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
|
|
318
363
|
if (!isInteractiveRootLaunch(context.parsed, tty)) return;
|
|
@@ -352,6 +397,11 @@ function readCurrentBranch(cwd: string): string | null {
|
|
|
352
397
|
function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
|
|
353
398
|
spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
|
|
354
399
|
}
|
|
400
|
+
function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
401
|
+
if (result.signalCode === "SIGHUP") return true;
|
|
402
|
+
const stderr = result.stderr?.toLowerCase() ?? "";
|
|
403
|
+
return stderr.includes("eio") || stderr.includes("input/output error");
|
|
404
|
+
}
|
|
355
405
|
|
|
356
406
|
export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
|
|
357
407
|
const env = context.env ?? process.env;
|
|
@@ -366,7 +416,12 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
366
416
|
const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
|
|
367
417
|
const project = context.project ?? cwd;
|
|
368
418
|
const sessionName = buildSkcTmuxSessionName(env, { branch });
|
|
369
|
-
|
|
419
|
+
// Pick the most appropriate tmux binary for this platform. On native Windows
|
|
420
|
+
// the resolver walks psmux / pmux / tmux and uses the first one present on
|
|
421
|
+
// PATH, so the default `skc --tmux` flow lands on a real multiplexer even
|
|
422
|
+
// without an explicit SKC_TMUX_COMMAND override.
|
|
423
|
+
const resolvedBinary = resolveSkcTmuxBinary({ platform, env });
|
|
424
|
+
const tmuxCommand = resolvedBinary.command;
|
|
370
425
|
const sessionId = env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
|
|
371
426
|
// The session ROOT is keyed by the active SKC session (SKC_SESSION_ID), NOT the
|
|
372
427
|
// coordinator/tmux identity. Fall back to the coordinator id only for standalone
|
|
@@ -376,15 +431,19 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
376
431
|
env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
|
|
377
432
|
tmuxRuntimeSessionPath(cwd, skcSessionId, buildSkcTmuxSessionSlug(sessionName));
|
|
378
433
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
379
|
-
if (!tmuxAvailable)
|
|
380
|
-
|
|
381
|
-
|
|
434
|
+
if (!tmuxAvailable) {
|
|
435
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxUnavailableDiagnostic(platform, tmuxCommand));
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
|
|
439
|
+
? "existingBranchSessionName" in context
|
|
382
440
|
? (context.existingBranchSessionName ?? undefined)
|
|
383
441
|
: findExistingSessionForLaunch({
|
|
384
442
|
env,
|
|
385
443
|
project,
|
|
386
444
|
branch,
|
|
387
|
-
})
|
|
445
|
+
})
|
|
446
|
+
: undefined;
|
|
388
447
|
const innerCommand = buildInnerCommand(
|
|
389
448
|
{
|
|
390
449
|
cwd,
|
|
@@ -441,7 +500,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
441
500
|
|
|
442
501
|
if (plan.attachSessionName) {
|
|
443
502
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
|
|
444
|
-
|
|
503
|
+
if (attached.exitCode === 0) return true;
|
|
445
504
|
}
|
|
446
505
|
|
|
447
506
|
const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
|
|
@@ -478,6 +537,10 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
478
537
|
if (created.exitCode !== 0) return false;
|
|
479
538
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
|
|
480
539
|
if (attached.exitCode === 0) return true;
|
|
540
|
+
if (isTmuxAttachDisconnectError(attached)) {
|
|
541
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
481
544
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
482
545
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
483
546
|
return true;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows psmux detection and tmux-binary resolution.
|
|
3
|
+
*
|
|
4
|
+
* Recent psmux releases (see docs/compatibility.md in the psmux repo) close
|
|
5
|
+
* the round-trip gap for set-option / show-options user options and the
|
|
6
|
+
* set-window-option profile values skc emits, which is what unblocks the
|
|
7
|
+
* native Windows skc --tmux path. This module detects that capability so skc
|
|
8
|
+
* can pick psmux when tmux is missing on Windows, and so callers can decide
|
|
9
|
+
* whether to treat a given tmux binary as psmux (affecting e.g. the untagged
|
|
10
|
+
* diagnostic wording and namespace handling).
|
|
11
|
+
*
|
|
12
|
+
* The probe is intentionally lightweight: it runs a single tmux -V (or
|
|
13
|
+
* --version) once per process and caches the verdict. Cache invalidation
|
|
14
|
+
* knobs:
|
|
15
|
+
* - force: true re-probes on every call (used by tests).
|
|
16
|
+
* - SKC_PSMUX_FORCE_DETECT=1 re-probes each call.
|
|
17
|
+
* - SKC_PSMUX_DETECTION=off skips probing entirely.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const SKC_PSMUX_COMMAND_ENV = "SKC_PSMUX_COMMAND";
|
|
21
|
+
export const SKC_PSMUX_DETECTION_ENV = "SKC_PSMUX_DETECTION";
|
|
22
|
+
export const SKC_PSMUX_FORCE_DETECT_ENV = "SKC_PSMUX_FORCE_DETECT";
|
|
23
|
+
|
|
24
|
+
/** Names that psmux installs as the canonical executable / alias. */
|
|
25
|
+
export const PSMUX_BINARY_NAMES = ["psmux", "pmux", "tmux"] as const;
|
|
26
|
+
|
|
27
|
+
/** Substrings that uniquely identify psmux in version/help output. */
|
|
28
|
+
const PSMUX_VERSION_MARKERS = ["psmux", "pmux"] as const;
|
|
29
|
+
|
|
30
|
+
export type PsmuxSpawnRunner = (
|
|
31
|
+
command: string,
|
|
32
|
+
args: string[],
|
|
33
|
+
) => { exitCode: number | null; stdout?: string; stderr?: string };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves a tmux-class binary name (e.g. "psmux", "tmux") to an absolute
|
|
37
|
+
* filesystem path or returns null when the binary cannot be located. The
|
|
38
|
+
* default implementation uses `Bun.which`; production callers leave it
|
|
39
|
+
* alone and unit tests inject a stub via `__setBinaryResolverForTests`
|
|
40
|
+
* so the version-banner probe can be exercised hermetically.
|
|
41
|
+
*/
|
|
42
|
+
export type BinaryResolver = (candidate: string) => string | null;
|
|
43
|
+
|
|
44
|
+
const DEFAULT_BINARY_RESOLVER: BinaryResolver = candidate => {
|
|
45
|
+
if (!candidate) return null;
|
|
46
|
+
const stripped = candidate.trim().replace(/^["']|["']$/g, "");
|
|
47
|
+
if (!stripped) return null;
|
|
48
|
+
if (Bun.which(stripped)) return stripped;
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let activeBinaryResolver: BinaryResolver = DEFAULT_BINARY_RESOLVER;
|
|
53
|
+
|
|
54
|
+
/** @internal Test-only seam; production code never calls this. */
|
|
55
|
+
export function __setBinaryResolverForTests(resolver: BinaryResolver | null): void {
|
|
56
|
+
activeBinaryResolver = resolver ?? DEFAULT_BINARY_RESOLVER;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface CacheEntry {
|
|
60
|
+
command: string;
|
|
61
|
+
isPsmux: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const detectionCache = new Map<string, CacheEntry>();
|
|
65
|
+
|
|
66
|
+
export function envDisabled(value: string | undefined): boolean {
|
|
67
|
+
const normalized = value?.trim().toLowerCase();
|
|
68
|
+
return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* SKC_PSMUX_FORCE_DETECT opt-in re-probe switch. Any non-empty value other
|
|
73
|
+
* than a "disabled" sentinel forces a fresh probe on every call. The unset
|
|
74
|
+
* (undefined) case must NOT force probing, otherwise the in-process cache
|
|
75
|
+
* never engages.
|
|
76
|
+
*/
|
|
77
|
+
function envForcesProbe(value: string | undefined): boolean {
|
|
78
|
+
if (value === undefined) return false;
|
|
79
|
+
if (envDisabled(value)) return false;
|
|
80
|
+
return value.trim().length > 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readSpawnRunner(): PsmuxSpawnRunner {
|
|
84
|
+
return (command, args) => {
|
|
85
|
+
try {
|
|
86
|
+
const result = Bun.spawnSync({
|
|
87
|
+
cmd: [command, ...args],
|
|
88
|
+
stdout: "pipe",
|
|
89
|
+
stderr: "pipe",
|
|
90
|
+
env: process.env,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
exitCode: result.exitCode,
|
|
94
|
+
stdout: result.stdout.toString(),
|
|
95
|
+
stderr: result.stderr.toString(),
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return { exitCode: -1, stdout: "", stderr: "" };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function probeVersionOutput(command: string, runner: PsmuxSpawnRunner): string {
|
|
104
|
+
const flags = ["-V", "--version"];
|
|
105
|
+
for (const flag of flags) {
|
|
106
|
+
const result = runner(command, [flag]);
|
|
107
|
+
const text = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
|
|
108
|
+
if (result.exitCode === 0 && text.trim().length > 0) return text;
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function outputMentionsPsmux(output: string): boolean {
|
|
114
|
+
if (!output) return false;
|
|
115
|
+
return PSMUX_VERSION_MARKERS.some(marker => output.includes(marker));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveBinaryPath(candidate: string): string | null {
|
|
119
|
+
return activeBinaryResolver(candidate);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function detectPsmuxForCommand(command: string, runner: PsmuxSpawnRunner): boolean {
|
|
123
|
+
const resolved = resolveBinaryPath(command);
|
|
124
|
+
if (!resolved) return false;
|
|
125
|
+
const output = probeVersionOutput(resolved, runner);
|
|
126
|
+
return outputMentionsPsmux(output);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Decide whether command resolves to a psmux binary by probing its version
|
|
131
|
+
* output. The result is cached per process unless force is set or
|
|
132
|
+
* SKC_PSMUX_FORCE_DETECT=1.
|
|
133
|
+
*/
|
|
134
|
+
export function detectPsmux(
|
|
135
|
+
command: string,
|
|
136
|
+
options: { force?: boolean; env?: NodeJS.ProcessEnv; runner?: PsmuxSpawnRunner } = {},
|
|
137
|
+
): boolean {
|
|
138
|
+
const env = options.env ?? process.env;
|
|
139
|
+
const explicit = env[SKC_PSMUX_COMMAND_ENV]?.trim();
|
|
140
|
+
if (explicit) {
|
|
141
|
+
// The override is authoritative on its own — we trust the user's
|
|
142
|
+
// SKC_PSMUX_COMMAND value when they name a psmux-class binary, even
|
|
143
|
+
// when the binary cannot be located on PATH in the current process.
|
|
144
|
+
// This keeps the override usable from CI runners and from test
|
|
145
|
+
// environments where Bun.which would otherwise return null.
|
|
146
|
+
const normalized = explicit.toLowerCase();
|
|
147
|
+
if (
|
|
148
|
+
normalized === "psmux" ||
|
|
149
|
+
normalized === "pmux" ||
|
|
150
|
+
normalized.endsWith("/psmux") ||
|
|
151
|
+
normalized.endsWith("/pmux") ||
|
|
152
|
+
normalized.endsWith("\\psmux") ||
|
|
153
|
+
normalized.endsWith("\\pmux")
|
|
154
|
+
) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const explicitPath = resolveBinaryPath(explicit);
|
|
158
|
+
if (explicitPath && explicitPath === resolveBinaryPath(command)) return true;
|
|
159
|
+
}
|
|
160
|
+
if (envDisabled(env[SKC_PSMUX_DETECTION_ENV])) return false;
|
|
161
|
+
const force = options.force === true || envForcesProbe(env[SKC_PSMUX_FORCE_DETECT_ENV]);
|
|
162
|
+
const useCache = !force && !options.force;
|
|
163
|
+
if (useCache) {
|
|
164
|
+
const cached = detectionCache.get(command);
|
|
165
|
+
if (cached) return cached.isPsmux;
|
|
166
|
+
}
|
|
167
|
+
const runner = options.runner ?? readSpawnRunner();
|
|
168
|
+
const isPsmux = detectPsmuxForCommand(command, runner);
|
|
169
|
+
if (useCache) detectionCache.set(command, { command, isPsmux });
|
|
170
|
+
return isPsmux;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ResolveSkcTmuxBinaryOptions {
|
|
174
|
+
platform?: NodeJS.Platform;
|
|
175
|
+
env?: NodeJS.ProcessEnv;
|
|
176
|
+
runner?: PsmuxSpawnRunner;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface ResolvedTmuxBinary {
|
|
180
|
+
command: string;
|
|
181
|
+
isPsmux: boolean;
|
|
182
|
+
viaExplicitOverride: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the tmux command SKC should invoke. Honors the existing
|
|
187
|
+
* SKC_TMUX_COMMAND / SKC_TEAM_TMUX_COMMAND overrides; on Windows when no
|
|
188
|
+
* override is set, psmux (installed as psmux, pmux, or tmux) is picked
|
|
189
|
+
* automatically so the default skc --tmux flow lands on a real multiplexer.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveSkcTmuxBinary(options: ResolveSkcTmuxBinaryOptions = {}): ResolvedTmuxBinary {
|
|
192
|
+
const env = options.env ?? process.env;
|
|
193
|
+
const platform = options.platform ?? process.platform;
|
|
194
|
+
const runner = options.runner ?? readSpawnRunner();
|
|
195
|
+
const explicit = env.SKC_TMUX_COMMAND?.trim() || env.SKC_TEAM_TMUX_COMMAND?.trim();
|
|
196
|
+
if (explicit) {
|
|
197
|
+
const isPsmux = detectPsmux(explicit, { env, runner });
|
|
198
|
+
return { command: explicit, isPsmux, viaExplicitOverride: true };
|
|
199
|
+
}
|
|
200
|
+
if (platform === "win32") {
|
|
201
|
+
for (const candidate of PSMUX_BINARY_NAMES) {
|
|
202
|
+
if (resolveBinaryPath(candidate)) {
|
|
203
|
+
const isPsmux = detectPsmux(candidate, { env, runner });
|
|
204
|
+
return { command: candidate, isPsmux, viaExplicitOverride: false };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const tmuxPath = resolveBinaryPath("tmux");
|
|
209
|
+
if (tmuxPath) {
|
|
210
|
+
const isPsmux = detectPsmux("tmux", { env, runner });
|
|
211
|
+
return { command: "tmux", isPsmux, viaExplicitOverride: false };
|
|
212
|
+
}
|
|
213
|
+
return { command: "tmux", isPsmux: false, viaExplicitOverride: false };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Test-only helper: drop the in-process detection cache. */
|
|
217
|
+
export function clearPsmuxDetectionCache(): void {
|
|
218
|
+
detectionCache.clear();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface PsmuxProbe {
|
|
222
|
+
command: string;
|
|
223
|
+
versionOutput: string;
|
|
224
|
+
isPsmux: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function probePsmux(
|
|
228
|
+
command: string,
|
|
229
|
+
options: { env?: NodeJS.ProcessEnv; runner?: PsmuxSpawnRunner; force?: boolean } = {},
|
|
230
|
+
): PsmuxProbe {
|
|
231
|
+
const env = options.env ?? process.env;
|
|
232
|
+
const runner = options.runner ?? readSpawnRunner();
|
|
233
|
+
const resolved = resolveBinaryPath(command);
|
|
234
|
+
if (!resolved) return { command, versionOutput: "", isPsmux: false };
|
|
235
|
+
if (options.force) clearPsmuxDetectionCache();
|
|
236
|
+
const output = probeVersionOutput(resolved, runner);
|
|
237
|
+
const isPsmux = outputMentionsPsmux(output) || env[SKC_PSMUX_COMMAND_ENV]?.trim() === resolved;
|
|
238
|
+
return { command: resolved, versionOutput: output, isPsmux };
|
|
239
|
+
}
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
*
|
|
38
38
|
* 2. **Artifact write**: `skc ralplan --write --stage <type> --stage_n <N> --artifact
|
|
39
39
|
* <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
40
|
-
* / Critic / revision / ADR / final markdown under `.skc/plans/ralplan/<run-id>/`, maintains
|
|
40
|
+
* / Critic / revision / post-interview / ADR / final markdown under `.skc/plans/ralplan/<run-id>/`, maintains
|
|
41
41
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
42
42
|
* the HUD chip to reflect the latest persisted stage.
|
|
43
43
|
*/
|
|
@@ -48,7 +48,7 @@ export interface RalplanCommandResult {
|
|
|
48
48
|
stderr?: string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "adr", "final"] as const;
|
|
51
|
+
const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"] as const;
|
|
52
52
|
type RalplanStage = (typeof KNOWN_STAGES)[number];
|
|
53
53
|
|
|
54
54
|
const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
|
|
@@ -5,7 +5,7 @@ import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
|
5
5
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
7
|
import type { GcPidProbe, GcRecord } from "./gc-runtime";
|
|
8
|
-
import { applySkcTmuxProfile
|
|
8
|
+
import { applySkcTmuxProfile } from "./launch-tmux";
|
|
9
9
|
import { modeStatePath, sessionIdFromDirName, sessionReportsDir, teamStateRoot } from "./session-layout";
|
|
10
10
|
import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
11
11
|
import {
|
|
@@ -595,6 +595,16 @@ function teamDir(stateRoot: string, teamName: string): string {
|
|
|
595
595
|
function shellQuote(value: string): string {
|
|
596
596
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
597
597
|
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* PowerShell-safe single-quote escape: doubles single quotes inside a
|
|
601
|
+
* single-quoted PowerShell literal ('it''s ok') and uses the same
|
|
602
|
+
* surrounding quotes. Used to build worker command strings that psmux
|
|
603
|
+
* will hand to a Windows ConPTY pane running PowerShell.
|
|
604
|
+
*/
|
|
605
|
+
function powershellQuote(value: string): string {
|
|
606
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
607
|
+
}
|
|
598
608
|
function safePathSegment(kind: string, value: string): string {
|
|
599
609
|
assertSafeId(kind, value);
|
|
600
610
|
return value;
|
|
@@ -1812,7 +1822,7 @@ async function ensureWorkerWorktree(
|
|
|
1812
1822
|
|
|
1813
1823
|
function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
|
|
1814
1824
|
const suffix = detail?.trim() ? `:${detail.trim()}` : "";
|
|
1815
|
-
return `skc_team_requires_tmux_leader: run \`skc --tmux
|
|
1825
|
+
return `skc_team_requires_tmux_leader: start a tmux session first (run \`skc --tmux\`, or launch tmux yourself), then run \`skc team ...\` inside it, or use \`skc team --dry-run\` for state-only smoke tests${suffix}`;
|
|
1816
1826
|
}
|
|
1817
1827
|
function readSkcTmuxProfileValue(tmuxCommand: string, sessionName: string): string {
|
|
1818
1828
|
const result = Bun.spawnSync(
|
|
@@ -1826,7 +1836,7 @@ function readSkcTmuxProfileValue(tmuxCommand: string, sessionName: string): stri
|
|
|
1826
1836
|
return result.stdout.toString().trim();
|
|
1827
1837
|
}
|
|
1828
1838
|
|
|
1829
|
-
function
|
|
1839
|
+
function tagTmuxSessionAsSkcLeader(tmuxCommand: string, sessionName: string): boolean {
|
|
1830
1840
|
const result = Bun.spawnSync(
|
|
1831
1841
|
[
|
|
1832
1842
|
tmuxCommand,
|
|
@@ -1845,25 +1855,39 @@ function retagSkcLaunchedTmuxSession(tmuxCommand: string, sessionName: string):
|
|
|
1845
1855
|
}
|
|
1846
1856
|
|
|
1847
1857
|
function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): SkcTmuxLeaderContext {
|
|
1858
|
+
if (Bun.which(tmuxCommand) === null)
|
|
1859
|
+
throw new Error(buildTeamTmuxLeaderRequirementMessage(`tmux_not_installed:${tmuxCommand}`));
|
|
1848
1860
|
const paneTarget = env.TMUX_PANE?.trim();
|
|
1849
1861
|
const args = paneTarget
|
|
1850
1862
|
? ["display-message", "-p", "-t", paneTarget, "#S:#I #{pane_id}"]
|
|
1851
1863
|
: ["display-message", "-p", "#S:#I #{pane_id}"];
|
|
1852
1864
|
const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
1853
|
-
if (result.exitCode !== 0)
|
|
1865
|
+
if (result.exitCode !== 0) {
|
|
1866
|
+
// Distinguish "you are not inside any tmux session" from a genuine tmux
|
|
1867
|
+
// query failure so the caller gets actionable guidance instead of raw
|
|
1868
|
+
// tmux stderr. `skc team` needs a tmux leader; outside tmux there is none.
|
|
1869
|
+
const insideTmux = Boolean(env.TMUX?.trim() || env.TMUX_PANE?.trim());
|
|
1870
|
+
const stderr = result.stderr.toString().trim();
|
|
1871
|
+
throw new Error(
|
|
1872
|
+
buildTeamTmuxLeaderRequirementMessage(
|
|
1873
|
+
insideTmux ? `tmux_query_failed${stderr ? `:${stderr}` : ""}` : "not_inside_tmux",
|
|
1874
|
+
),
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1854
1877
|
const [sessionAndWindow = "", leaderPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
|
|
1855
1878
|
const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
|
|
1856
1879
|
if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
|
|
1857
1880
|
throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
|
|
1858
1881
|
if (readSkcTmuxProfileValue(tmuxCommand, sessionName) !== SKC_TMUX_PROFILE_VALUE) {
|
|
1859
|
-
//
|
|
1860
|
-
//
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
1863
|
-
//
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1882
|
+
// Adopt any real tmux leader as a SKC team leader — including a session
|
|
1883
|
+
// the user created outside `skc --tmux` — by writing SKC's @skc-profile
|
|
1884
|
+
// ownership tag and reading it back. A provider that round-trips tmux
|
|
1885
|
+
// user options (real tmux) keeps the tag and is adopted; one that does
|
|
1886
|
+
// not (e.g. psmux on Windows) drops it, so the readback still fails and
|
|
1887
|
+
// the leader is rejected as unmanaged. This also self-heals a genuine
|
|
1888
|
+
// `skc --tmux` pane that lost its @skc-profile tag mid-startup.
|
|
1889
|
+
const tagged = tagTmuxSessionAsSkcLeader(tmuxCommand, sessionName);
|
|
1890
|
+
if (!tagged || readSkcTmuxProfileValue(tmuxCommand, sessionName) !== SKC_TMUX_PROFILE_VALUE)
|
|
1867
1891
|
throw new Error(
|
|
1868
1892
|
buildTeamTmuxLeaderRequirementMessage(
|
|
1869
1893
|
`unmanaged_tmux_session:${sessionName} — ${buildSkcTmuxUntaggedSessionHint(tmuxCommand)}`,
|
|
@@ -1881,7 +1905,15 @@ export function resolveSkcWorkerCommand(cwd = process.cwd(), env: NodeJS.Process
|
|
|
1881
1905
|
if (entrypoint && path.basename(entrypoint).startsWith("skc")) return shellQuote(path.resolve(cwd, entrypoint));
|
|
1882
1906
|
return "skc";
|
|
1883
1907
|
}
|
|
1884
|
-
|
|
1908
|
+
/** @internal Exported for unit tests. */
|
|
1909
|
+
export function buildWorkerCommand(
|
|
1910
|
+
config: SkcTeamConfig,
|
|
1911
|
+
worker: SkcTeamWorker,
|
|
1912
|
+
platform: NodeJS.Platform = process.platform,
|
|
1913
|
+
): string {
|
|
1914
|
+
const quote = platform === "win32" ? powershellQuote : shellQuote;
|
|
1915
|
+
const envAssignment = (key: string, value: string): string =>
|
|
1916
|
+
platform === "win32" ? `$env:${key} = ${quote(value)};` : `${key}=${quote(value)}`;
|
|
1885
1917
|
const workspace = worker.worktree_path
|
|
1886
1918
|
? `Worker worktree: ${worker.worktree_path}.`
|
|
1887
1919
|
: `Worker cwd: ${config.leader.cwd}.`;
|
|
@@ -1894,17 +1926,18 @@ function buildWorkerCommand(config: SkcTeamConfig, worker: SkcTeamWorker): strin
|
|
|
1894
1926
|
`Before claiming work, send startup ACK: skc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
|
|
1895
1927
|
`Use skc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
1896
1928
|
].join("\n");
|
|
1897
|
-
const
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
...(worker.worktree_path ? [
|
|
1929
|
+
const envLines = [
|
|
1930
|
+
envAssignment("SKC_TEAM_WORKER", `${config.team_name}/${worker.id}`),
|
|
1931
|
+
envAssignment("SKC_TEAM_INTERNAL_WORKER", `${config.team_name}/${worker.id}`),
|
|
1932
|
+
envAssignment("SKC_TEAM_NAME", config.team_name),
|
|
1933
|
+
envAssignment("SKC_TEAM_WORKER_ID", worker.id),
|
|
1934
|
+
envAssignment("SKC_TEAM_STATE_ROOT", config.state_root),
|
|
1935
|
+
envAssignment("SKC_TEAM_LEADER_CWD", config.leader.cwd),
|
|
1936
|
+
envAssignment("SKC_TEAM_DISPLAY_NAME", config.display_name),
|
|
1937
|
+
...(worker.worktree_path ? [envAssignment("SKC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
|
|
1906
1938
|
];
|
|
1907
|
-
|
|
1939
|
+
const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
|
|
1940
|
+
return `${joined} ${config.worker_command} ${quote(prompt)}`;
|
|
1908
1941
|
}
|
|
1909
1942
|
interface SkcTeamInitialLane {
|
|
1910
1943
|
label: string;
|