sneakoscope 0.7.62 → 0.7.64
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/README.md +2 -2
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +10 -5
- package/src/cli/main.mjs +85 -11
- package/src/cli/maintenance-commands.mjs +55 -9
- package/src/core/codex-app.mjs +23 -8
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +1 -0
- package/src/core/init.mjs +7 -3
- package/src/core/mission.mjs +2 -2
- package/src/core/pipeline.mjs +2 -2
- package/src/core/routes.mjs +3 -3
- package/src/core/team-live.mjs +24 -3
- package/src/core/tmux-ui.mjs +290 -6
package/README.md
CHANGED
|
@@ -239,9 +239,9 @@ sks team log latest
|
|
|
239
239
|
|
|
240
240
|
By default, Team missions keep at least five QA/reviewer lanes active. Use explicit role counts only when you need to raise or otherwise pin the lane mix for a specific mission.
|
|
241
241
|
|
|
242
|
-
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and
|
|
242
|
+
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and reconciles split live lanes inside the current SKS-owned tmux session when available. Outside an SKS tmux session, `sks team open-tmux --separate-session` keeps the named `sks-team-*` fallback view. Use `--no-open-tmux` for artifact-only mission creation. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
243
243
|
|
|
244
|
-
The tmux Team launch is a live orchestration screen in one tmux window: the
|
|
244
|
+
The tmux Team launch is a live orchestration screen in one tmux window: the main Codex pane stays alive, a managed overview pane follows `sks team watch <mission-id> --follow`, and neighboring managed split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. Pane headers show only mission, lane, phase, follow command, and cleanup command. SKS tags Team panes with tmux user options, closes only those managed panes when agent lanes complete or cleanup is requested, and recalculates the tiled layout after split/close operations. The separate `sks-team-*` session remains available as a fallback. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while detailed evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
|
|
245
245
|
|
|
246
246
|
Team roster and runtime artifacts now include per-agent Fast reasoning metadata. Simple bounded Team lanes can use low reasoning, tool-heavy runtime/CLI/tmux work uses medium, and knowledge, current-docs, safety, DB, release, commit, or research-heavy lanes use high or xhigh as appropriate instead of opening every scout at high.
|
|
247
247
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.64",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -307,12 +307,17 @@ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
|
|
|
307
307
|
export function normalizeCodexFastModeUiConfig(text = '') {
|
|
308
308
|
let next = removeLegacyTopLevelCodexModeLocks(text);
|
|
309
309
|
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
310
|
-
next = removeTomlTableKey(next, 'features', '
|
|
310
|
+
next = removeTomlTableKey(next, 'features', 'codex_hooks');
|
|
311
311
|
next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
|
|
312
312
|
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
313
|
-
next = upsertTomlTableKey(next, 'features', '
|
|
313
|
+
next = upsertTomlTableKey(next, 'features', 'hooks = true');
|
|
314
|
+
next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
|
|
314
315
|
next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
|
|
315
316
|
next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
|
|
317
|
+
next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
|
|
318
|
+
next = upsertTomlTableKey(next, 'features', 'computer_use = true');
|
|
319
|
+
next = upsertTomlTableKey(next, 'features', 'apps = true');
|
|
320
|
+
next = upsertTomlTableKey(next, 'features', 'plugins = true');
|
|
316
321
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
|
|
317
322
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
|
|
318
323
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
|
|
@@ -993,7 +998,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
993
998
|
if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest failed: postinstall should stay quiet when codex-lb is not configured');
|
|
994
999
|
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
995
1000
|
if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
|
|
996
|
-
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('
|
|
1001
|
+
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('codex_git_commit = true') || !codexLbConfig.includes('computer_use = true') || !codexLbConfig.includes('apps = true') || !codexLbConfig.includes('plugins = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App feature flags, Fast mode defaults, Codex Git commit generation, force GPT-5.5, or migrate the hooks feature flag');
|
|
997
1002
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
998
1003
|
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
999
1004
|
if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
|
|
@@ -1007,7 +1012,7 @@ function hasTopLevelCodexModeLock(text = '') {
|
|
|
1007
1012
|
return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(text) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(text);
|
|
1008
1013
|
}
|
|
1009
1014
|
|
|
1010
|
-
function
|
|
1015
|
+
function hasDeprecatedCodexHooksFeatureFlag(text = '') {
|
|
1011
1016
|
const lines = String(text || '').split('\n');
|
|
1012
1017
|
const start = lines.findIndex((line) => line.trim() === '[features]');
|
|
1013
1018
|
if (start === -1) return false;
|
|
@@ -1018,5 +1023,5 @@ function hasLegacyHooksFeatureFlag(text = '') {
|
|
|
1018
1023
|
break;
|
|
1019
1024
|
}
|
|
1020
1025
|
}
|
|
1021
|
-
return lines.slice(start + 1, end).some((line) => /^\s*
|
|
1026
|
+
return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
|
|
1022
1027
|
}
|
package/src/cli/main.mjs
CHANGED
|
@@ -75,7 +75,7 @@ import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
|
|
|
75
75
|
import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
|
|
76
76
|
import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
|
|
77
77
|
import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
|
|
78
|
-
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
78
|
+
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
79
79
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
80
80
|
import { context7Command } from './context7-command.mjs';
|
|
81
81
|
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
@@ -204,9 +204,11 @@ Usage:
|
|
|
204
204
|
sks goal pause|resume|clear <mission-id|latest>
|
|
205
205
|
sks goal status <mission-id|latest>
|
|
206
206
|
sks team "task" [executor:5 reviewer:6 user:1] [--json]
|
|
207
|
-
sks team log|tail|watch|lane|status|dashboard [mission-id|latest]
|
|
207
|
+
sks team log|tail|watch|lane|status|dashboard|open-tmux|attach-tmux [mission-id|latest]
|
|
208
208
|
sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."
|
|
209
209
|
sks team message [mission-id|latest] --from <agent> --to <agent|all> --message "..."
|
|
210
|
+
sks team open-tmux [mission-id|latest] [--no-attach|--separate-session]
|
|
211
|
+
sks team attach-tmux [mission-id|latest]
|
|
210
212
|
sks team cleanup-tmux [mission-id|latest]
|
|
211
213
|
sks research prepare "topic" [--depth frontier]
|
|
212
214
|
sks research run <mission-id|latest> [--mock] [--max-cycles N]
|
|
@@ -1452,7 +1454,7 @@ function usage(args = []) {
|
|
|
1452
1454
|
deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
|
|
1453
1455
|
tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session in fast-high mode: --model gpt-5.5 -c model_reasoning_effort="high". SKS always forces gpt-5.5; SKS_CODEX_MODEL and SKS_CODEX_FAST_HIGH=0 cannot downgrade or remove that model pin. Use SKS_CODEX_REASONING only for reasoning effort. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
|
|
1454
1456
|
openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
|
|
1455
|
-
team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
|
|
1457
|
+
team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team open-tmux latest', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
|
|
1456
1458
|
'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
|
|
1457
1459
|
ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT infers delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; Codex App $imagegen/gpt-image-2 and Context7 are conditional only when the sealed PPT contract needs raster assets, slide visual critique, or current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
|
|
1458
1460
|
'image-ux-review': ['Image UX Review', '', ' $Image-UX-Review localhost 화면을 이미지 생성 리뷰 루프로 검수해줘', ' $UX-Review 이 스크린샷을 gpt-image-2 콜아웃 리뷰로 분석하고 고쳐줘', ' sks image-ux-review status latest --json', '', '$Image-UX-Review captures or receives source UI screenshots, runs Codex App $imagegen/gpt-image-2 to create generated annotated review images with numbered callouts, then extracts those generated images into image-ux-issue-ledger.json. Text-only screenshot critique cannot pass image-ux-review-gate.json; missing generated review images remain an explicit blocker.'],
|
|
@@ -1922,7 +1924,7 @@ function hasTopLevelCodexModeLock(text = '') {
|
|
|
1922
1924
|
return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
|
|
1923
1925
|
}
|
|
1924
1926
|
|
|
1925
|
-
function
|
|
1927
|
+
function hasDeprecatedCodexHooksFeatureFlag(text = '') {
|
|
1926
1928
|
const lines = String(text || '').split('\n');
|
|
1927
1929
|
const start = lines.findIndex((line) => line.trim() === '[features]');
|
|
1928
1930
|
if (start === -1) return false;
|
|
@@ -1933,7 +1935,13 @@ function hasLegacyHooksFeatureFlag(text = '') {
|
|
|
1933
1935
|
break;
|
|
1934
1936
|
}
|
|
1935
1937
|
}
|
|
1936
|
-
return lines.slice(start + 1, end).some((line) => /^\s*
|
|
1938
|
+
return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = ['hooks', 'multi_agent', 'fast_mode', 'fast_mode_ui', 'codex_git_commit', 'computer_use', 'apps', 'plugins'];
|
|
1942
|
+
|
|
1943
|
+
function missingGeneratedCodexAppFeatureFlags(text = '') {
|
|
1944
|
+
return REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => !String(text || '').includes(`${name} = true`));
|
|
1937
1945
|
}
|
|
1938
1946
|
|
|
1939
1947
|
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
@@ -2150,6 +2158,8 @@ async function selftest() {
|
|
|
2150
2158
|
if (await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest failed: postinstall installed deprecated agent-team fallback skill');
|
|
2151
2159
|
if (!String(postinstallSetup.stdout || '').includes('SKS bootstrap: auto-running sks setup --bootstrap --install-scope global --force') || !String(postinstallSetup.stdout || '').includes('SKS Ready')) throw new Error('selftest failed: postinstall did not auto-run global forced bootstrap');
|
|
2152
2160
|
if (!(await exists(path.join(postinstallSetupTmp, '.codex', 'hooks.json')))) throw new Error('selftest failed: postinstall did not create project hooks during automatic bootstrap');
|
|
2161
|
+
const postinstallSetupConfig = await safeReadText(path.join(postinstallSetupTmp, '.codex', 'config.toml'));
|
|
2162
|
+
if (missingGeneratedCodexAppFeatureFlags(postinstallSetupConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallSetupConfig)) throw new Error('selftest failed: postinstall automatic bootstrap did not preserve required Codex App feature flags or migrate deprecated codex_hooks');
|
|
2153
2163
|
if (!String(postinstallSetup.stdout || '').includes('Codex App global $ skills: installed')) throw new Error('selftest failed: postinstall did not report automatic global Codex App skills');
|
|
2154
2164
|
if (!String(postinstallSetup.stdout || '').includes('Removed stale generated skill shadow(s):')) throw new Error('selftest failed: postinstall did not report stale first-party plugin shadow cleanup');
|
|
2155
2165
|
const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
|
|
@@ -2184,6 +2194,8 @@ async function selftest() {
|
|
|
2184
2194
|
if (postinstallNoMarker.code !== 0) throw new Error(`selftest failed: no-marker postinstall bootstrap exited ${postinstallNoMarker.code}: ${postinstallNoMarker.stderr}`);
|
|
2185
2195
|
if (!String(postinstallNoMarker.stdout || '').includes('no project marker found; auto-running global SKS runtime bootstrap')) throw new Error('selftest failed: no-marker postinstall did not report global runtime bootstrap');
|
|
2186
2196
|
if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest failed: no-marker postinstall did not bootstrap global runtime root');
|
|
2197
|
+
const postinstallNoMarkerConfig = await safeReadText(path.join(postinstallNoMarkerGlobalRoot, '.codex', 'config.toml'));
|
|
2198
|
+
if (missingGeneratedCodexAppFeatureFlags(postinstallNoMarkerConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallNoMarkerConfig)) throw new Error('selftest failed: no-marker postinstall bootstrap did not preserve required Codex App feature flags or migrate deprecated codex_hooks');
|
|
2187
2199
|
if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest failed: no-marker postinstall polluted install cwd');
|
|
2188
2200
|
if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest failed: global runtime bootstrap without project git wrote shared .gitignore');
|
|
2189
2201
|
const bootstrapJsonTmp = tmpdir();
|
|
@@ -2938,8 +2950,8 @@ async function selftest() {
|
|
|
2938
2950
|
if (wikiContext.includes('MANDATORY ambiguity-removal gate activated') || wikiContext.includes('Mission:')) throw new Error('selftest failed: Wiki route created ambiguity mission state');
|
|
2939
2951
|
if (!wikiJson.systemMessage?.includes('wiki refresh')) throw new Error('selftest failed: Wiki route missing system message');
|
|
2940
2952
|
const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
|
|
2941
|
-
|
|
2942
|
-
if (
|
|
2953
|
+
const missingCodexConfigFlags = missingGeneratedCodexAppFeatureFlags(codexConfigText);
|
|
2954
|
+
if (missingCodexConfigFlags.length || hasDeprecatedCodexHooksFeatureFlag(codexConfigText)) throw new Error(`selftest failed: generated Codex App feature flags missing or deprecated: ${missingCodexConfigFlags.join(', ')}`);
|
|
2943
2955
|
if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest failed: Context7 MCP not configured');
|
|
2944
2956
|
if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest failed: GPT-5.5 reasoning profiles not configured');
|
|
2945
2957
|
if (!/\[profiles\.sks-mad-high\][\s\S]*?approval_policy = "never"[\s\S]*?sandbox_mode = "danger-full-access"/.test(codexConfigText)) throw new Error('selftest failed: generated sks-mad-high profile is not full access');
|
|
@@ -2947,13 +2959,29 @@ async function selftest() {
|
|
|
2947
2959
|
if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
|
|
2948
2960
|
const preservedConfigTmp = tmpdir();
|
|
2949
2961
|
await ensureDir(path.join(preservedConfigTmp, '.codex'));
|
|
2950
|
-
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\
|
|
2962
|
+
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\ncodex_hooks = true\nfast_mode_ui = false\ncodex_git_commit = false\ncomputer_use = false\napps = false\nplugins = false\ncustom_preview = true\n\n[user.fast_mode]\nvisible = true\n');
|
|
2951
2963
|
await initProject(preservedConfigTmp, {});
|
|
2952
2964
|
const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
|
|
2953
2965
|
if (!/^model = "gpt-5\.5"/m.test(preservedConfig) || !preservedConfig.includes('service_tier = "fast"') || !preservedConfig.includes('fast_mode = true') || !preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true') || !preservedConfig.includes('enabled = true') || !preservedConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(preservedConfig)) throw new Error('selftest failed: Codex config merge dropped or failed to enable Fast mode defaults and GPT-5.5');
|
|
2954
2966
|
if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest failed: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
|
|
2955
|
-
|
|
2967
|
+
const missingPreservedFlags = missingGeneratedCodexAppFeatureFlags(preservedConfig);
|
|
2968
|
+
if (missingPreservedFlags.length || hasDeprecatedCodexHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('custom_preview = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error(`selftest failed: Codex config merge did not add required app feature flags, preserve existing feature flags, or remove deprecated codex_hooks: ${missingPreservedFlags.join(', ')}`);
|
|
2956
2969
|
if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
|
|
2970
|
+
const appFeatureTmp = tmpdir();
|
|
2971
|
+
const fakeCodexApp = path.join(appFeatureTmp, 'Codex.app');
|
|
2972
|
+
const fakeCodexBinDir = path.join(appFeatureTmp, 'bin');
|
|
2973
|
+
await ensureDir(fakeCodexApp);
|
|
2974
|
+
await ensureDir(fakeCodexBinDir);
|
|
2975
|
+
const fakeCodex = path.join(fakeCodexBinDir, 'codex');
|
|
2976
|
+
await writeTextAtomic(fakeCodex, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nhooks stable true\nimage_generation stable true\nplugins stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2977
|
+
await fsp.chmod(fakeCodex, 0o755);
|
|
2978
|
+
const codexAppFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
2979
|
+
if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit) throw new Error('selftest failed: codex-app check did not accept required app feature flags including under-development codex_git_commit');
|
|
2980
|
+
const fakeCodexMissing = path.join(fakeCodexBinDir, 'codex-missing-git-commit');
|
|
2981
|
+
await writeTextAtomic(fakeCodexMissing, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nhooks stable true\nimage_generation stable true\nplugins stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2982
|
+
await fsp.chmod(fakeCodexMissing, 0o755);
|
|
2983
|
+
const codexAppMissingFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissing, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
2984
|
+
if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit) throw new Error('selftest failed: codex-app check did not block disabled codex_git_commit feature flag');
|
|
2957
2985
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
2958
2986
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
2959
2987
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
|
@@ -3169,7 +3197,9 @@ async function selftest() {
|
|
|
3169
3197
|
await writeJsonAtomic(path.join(teamDir, 'team-plan.json'), teamPlan);
|
|
3170
3198
|
if (teamPlan.agent_session_count !== 5) throw new Error('selftest failed: team default sessions not 5');
|
|
3171
3199
|
if (teamPlan.role_counts.executor !== 3 || teamPlan.role_counts.user !== 1 || teamPlan.role_counts.reviewer !== 5) throw new Error('selftest failed: team default role counts invalid');
|
|
3172
|
-
|
|
3200
|
+
const teamPlanFeatureFlags = teamPlan.codex_config_required?.features || {};
|
|
3201
|
+
const missingTeamPlanFeatureFlags = REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => teamPlanFeatureFlags[name] !== true);
|
|
3202
|
+
if (missingTeamPlanFeatureFlags.length || teamPlanFeatureFlags.codex_hooks === true) throw new Error(`selftest failed: team plan Codex config missing required app flags or still uses deprecated codex_hooks: ${missingTeamPlanFeatureFlags.join(', ')}`);
|
|
3173
3203
|
if (!teamPlan.review_gate?.passed || teamPlan.review_gate.required_reviewer_lanes !== 5) throw new Error('selftest failed: team review policy gate did not pass default plan');
|
|
3174
3204
|
if (teamPlan.codex_config_required?.service_tier !== 'fast' || teamPlan.reasoning?.service_tier !== 'fast') throw new Error('selftest failed: team plan did not require Fast service tier');
|
|
3175
3205
|
if (!teamPlan.goal_continuation?.enabled || teamPlan.goal_continuation?.mode !== 'ambient_codex_native_goal_overlay') throw new Error('selftest failed: Team plan did not include ambient Goal continuation');
|
|
@@ -3208,6 +3238,7 @@ async function selftest() {
|
|
|
3208
3238
|
if (!fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT)) || !fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT)) || !fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT))) throw new Error('selftest failed: From-Chat-IMG team plan missing checklist/temp TriWiki/QA invariants');
|
|
3209
3239
|
const teamWorkflow = teamWorkflowMarkdown(teamPlan);
|
|
3210
3240
|
if (!teamWorkflow.includes('SSOT: triwiki') || !teamWorkflow.includes('Analysis Scouts') || !teamWorkflow.includes('sks wiki validate')) throw new Error('selftest failed: team workflow missing scout-first TriWiki context tracking');
|
|
3241
|
+
if (!teamWorkflow.includes('sks team open-tmux')) throw new Error('selftest failed: team workflow missing existing-mission tmux open command');
|
|
3211
3242
|
if (!teamWorkflow.includes(TEAM_GRAPH_ARTIFACT) || !teamWorkflow.includes(TEAM_INBOX_DIR)) throw new Error('selftest failed: team workflow missing runtime graph/inbox guidance');
|
|
3212
3243
|
if (!teamWorkflow.includes('before every stage') || !teamWorkflow.includes('after findings/artifact changes')) throw new Error('selftest failed: team workflow missing per-stage TriWiki policy');
|
|
3213
3244
|
const customTeamPlan = buildTeamPlan(teamId, '병렬 구현 팀 테스트', { agentSessions: 5 });
|
|
@@ -3241,7 +3272,7 @@ async function selftest() {
|
|
|
3241
3272
|
await ensureDir(fakeTmuxDir);
|
|
3242
3273
|
const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
|
|
3243
3274
|
const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
|
|
3244
|
-
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst fs = require('node:fs');\nconst log = process.env.SKS_FAKE_TMUX_LOG;\nif (log) fs.appendFileSync(log, process.argv.slice(2).join(' ') + '\\n');\nconst cmd = process.argv[2];\nif (cmd === 'has-session') process.exit(0);\nif (cmd === 'kill-session') process.exit(0);\nif (cmd === 'new-session') { console.log('%1'); process.exit(0); }\nif (cmd === 'split-window') { console.log('%2'); process.exit(0); }\nif (cmd === 'list-windows') { console.log('@1'); process.exit(0); }\nprocess.exit(0);\n`);
|
|
3275
|
+
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst fs = require('node:fs');\nconst log = process.env.SKS_FAKE_TMUX_LOG;\nif (log) fs.appendFileSync(log, process.argv.slice(2).join(' ') + '\\n');\nconst cmd = process.argv[2];\nif (cmd === 'has-session') process.exit(0);\nif (cmd === 'kill-session') process.exit(0);\nif (cmd === 'kill-pane') process.exit(0);\nif (cmd === 'new-session') { console.log('%1'); process.exit(0); }\nif (cmd === 'split-window') { console.log(process.env.SKS_FAKE_TMUX_SPLIT_ID || '%2'); process.exit(0); }\nif (cmd === 'list-windows') { console.log('@1'); process.exit(0); }\nif (cmd === 'display-message') { console.log(process.env.SKS_FAKE_TMUX_DISPLAY || 'sks-existing-selftest\\t@1\\t%1'); process.exit(0); }\nif (cmd === 'list-panes') { console.log(process.env.SKS_FAKE_TMUX_LIST || ''); process.exit(0); }\nif (cmd === 'set-option' || cmd === 'select-layout' || cmd === 'resize-window' || cmd === 'set-window-option' || cmd === 'set-hook') process.exit(0);\nprocess.exit(0);\n`);
|
|
3245
3276
|
await fsp.chmod(fakeTmuxBin, 0o755);
|
|
3246
3277
|
const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
|
|
3247
3278
|
process.env.SKS_FAKE_TMUX_LOG = fakeTmuxLog;
|
|
@@ -3253,6 +3284,48 @@ async function selftest() {
|
|
|
3253
3284
|
if (!recreatedTmux.ok || !fakeTmuxLogText.includes('kill-session -t sks-existing-selftest') || !fakeTmuxLogText.includes('new-session') || !fakeTmuxLogText.includes('split-window')) throw new Error('selftest failed: tmux recreate did not replace stale existing session with split panes');
|
|
3254
3285
|
if (!recreatedTmux.dynamic_resize?.enabled || !fakeTmuxLogText.includes('list-windows -t sks-existing-selftest -F #{window_id}') || !fakeTmuxLogText.includes('set-window-option -t @1 window-size latest') || !fakeTmuxLogText.includes('set-hook -t sks-existing-selftest client-resized') || !fakeTmuxLogText.includes('resize-window -t @1 -A')) throw new Error('selftest failed: tmux dynamic resize hooks were not installed for split panes');
|
|
3255
3286
|
if (recreatedTmux.layout !== 'tiled' || Number(recreatedTmux.initial_size?.width || 0) < 120 || Number(recreatedTmux.initial_size?.height || 0) < 36) throw new Error('selftest failed: tmux dynamic resize metadata missing normalized initial size/layout');
|
|
3287
|
+
await ensureDir(path.join(tmp, '.sneakoscope', 'state'));
|
|
3288
|
+
await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'state', 'tmux-sessions.json'), {
|
|
3289
|
+
schema_version: 1,
|
|
3290
|
+
sessions: {
|
|
3291
|
+
'sks-existing-selftest': {
|
|
3292
|
+
session: 'sks-existing-selftest',
|
|
3293
|
+
root: tmp,
|
|
3294
|
+
panes: [{ pane_id: '%1', role: 'codex', title: 'Codex CLI' }]
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
});
|
|
3298
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3299
|
+
process.env.SKS_FAKE_TMUX_DISPLAY = 'sks-existing-selftest\t@1\t%1';
|
|
3300
|
+
process.env.SKS_FAKE_TMUX_LIST = '';
|
|
3301
|
+
process.env.SKS_FAKE_TMUX_SPLIT_ID = '%80';
|
|
3302
|
+
const cockpitOpen = await reconcileTmuxTeamCockpit({
|
|
3303
|
+
root: tmp,
|
|
3304
|
+
missionId: teamId,
|
|
3305
|
+
plan: roleTeamPlan,
|
|
3306
|
+
dashboard: { agents: { analysis_scout_1: { status: 'assigned' } } },
|
|
3307
|
+
control: { status: 'running' },
|
|
3308
|
+
tmux: { bin: fakeTmuxBin },
|
|
3309
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3310
|
+
});
|
|
3311
|
+
const cockpitOpenLog = await safeReadText(fakeTmuxLog);
|
|
3312
|
+
if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -t @1') || !cockpitOpenLog.includes('set-option -pt %80 @sks_team_managed 1') || !cockpitOpenLog.includes('select-layout -t @1 tiled')) throw new Error('selftest failed: dynamic Team cockpit did not split managed panes in the current SKS tmux session');
|
|
3313
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3314
|
+
process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_2\tnode\t1\t${teamId}\tanalysis_scout_2\tscout\n%83\tuser pane\tzsh\t\t\t\t`;
|
|
3315
|
+
const cockpitClose = await reconcileTmuxTeamCockpit({
|
|
3316
|
+
root: tmp,
|
|
3317
|
+
missionId: teamId,
|
|
3318
|
+
plan: roleTeamPlan,
|
|
3319
|
+
dashboard: { agents: { analysis_scout_1: { status: 'completed' }, analysis_scout_2: { status: 'assigned' } } },
|
|
3320
|
+
control: { status: 'running' },
|
|
3321
|
+
tmux: { bin: fakeTmuxBin },
|
|
3322
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3323
|
+
});
|
|
3324
|
+
const cockpitCloseLog = await safeReadText(fakeTmuxLog);
|
|
3325
|
+
if (!cockpitClose.ok || cockpitClose.closed_lane_count !== 1 || !cockpitCloseLog.includes('kill-pane -t %81') || cockpitCloseLog.includes('kill-pane -t %83')) throw new Error('selftest failed: dynamic Team cockpit did not close only stale managed panes');
|
|
3326
|
+
delete process.env.SKS_FAKE_TMUX_DISPLAY;
|
|
3327
|
+
delete process.env.SKS_FAKE_TMUX_LIST;
|
|
3328
|
+
delete process.env.SKS_FAKE_TMUX_SPLIT_ID;
|
|
3256
3329
|
await writeTextAtomic(fakeTmuxLog, '');
|
|
3257
3330
|
const madCockpit = await launchMadTmuxUi(['--workspace', 'sks-mad-selftest-ui', '--no-attach'], { root: tmp, tmux: { ok: true, bin: fakeTmuxBin, version: '3.4' }, codex: { bin: process.execPath }, app: { ok: true, guidance: [] }, missionId: 'M-MAD-SELFTEST' });
|
|
3258
3331
|
const madTmuxLogText = await safeReadText(fakeTmuxLog);
|
|
@@ -3344,6 +3417,7 @@ async function selftest() {
|
|
|
3344
3417
|
if (!teamDashboard?.latest_messages?.some((entry) => entry.agent === 'team_consensus')) throw new Error('selftest failed: team live dashboard missing agent event');
|
|
3345
3418
|
const teamLive = await readTeamLive(teamDir);
|
|
3346
3419
|
if (!teamLive.includes('Analysis scouts') || !teamLive.includes('selftest mapped repo slice')) throw new Error('selftest failed: team live transcript missing analysis scout section/event');
|
|
3420
|
+
if (!teamLive.includes('sks team open-tmux')) throw new Error('selftest failed: team live transcript missing existing-mission tmux open command');
|
|
3347
3421
|
if (!teamLive.includes('selftest mapped options')) throw new Error('selftest failed: team live transcript missing event');
|
|
3348
3422
|
if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest failed: team live transcript missing TriWiki context tracking');
|
|
3349
3423
|
if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest failed: team transcript tail missing event');
|
|
@@ -30,7 +30,7 @@ import { PIPELINE_PLAN_ARTIFACT, validatePipelinePlan, writePipelinePlan } from
|
|
|
30
30
|
import { GOAL_BRIDGE_ARTIFACT, GOAL_WORKFLOW_ARTIFACT, updateGoalWorkflow, writeGoalWorkflow } from '../core/goal-workflow.mjs';
|
|
31
31
|
import { scanCodeStructure, writeCodeStructureReport } from '../core/code-structure.mjs';
|
|
32
32
|
import { writeMemorySweepReport } from '../core/memory-governor.mjs';
|
|
33
|
-
import { cleanupTmuxTeamView, defaultTmuxSessionName, launchMadTmuxUi, launchTmuxTeamView, sanitizeTmuxSessionName } from '../core/tmux-ui.mjs';
|
|
33
|
+
import { cleanupTmuxTeamView, defaultTmuxSessionName, launchMadTmuxUi, launchTmuxTeamView, reconcileTmuxTeamCockpit, sanitizeTmuxSessionName } from '../core/tmux-ui.mjs';
|
|
34
34
|
import { loadSkillDreamState, recordSkillDreamEvent, runSkillDream, writeSkillForgeReport } from '../core/skill-forge.mjs';
|
|
35
35
|
import { writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
|
|
36
36
|
import { checkDbOperation, checkSqlFile, classifyCommand, classifySql, loadDbSafetyPolicy, safeSupabaseMcpConfig, scanDbSafety } from '../core/db-safety.mjs';
|
|
@@ -516,7 +516,7 @@ async function goalCreate(args) {
|
|
|
516
516
|
if (!prompt) throw new Error('Missing goal task prompt.');
|
|
517
517
|
const { id, dir, mission } = await createMission(root, { mode: 'goal', prompt });
|
|
518
518
|
const workflow = await writeGoalWorkflow(dir, mission, { action: 'create', prompt });
|
|
519
|
-
await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: 'GOAL_READY', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'none' });
|
|
519
|
+
await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: 'GOAL_READY', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'none' }, { replace: true });
|
|
520
520
|
console.log(`Goal mission created: ${id}`);
|
|
521
521
|
console.log(`Artifact: ${path.relative(root, path.join(dir, GOAL_WORKFLOW_ARTIFACT))}`);
|
|
522
522
|
console.log(`Bridge: ${path.relative(root, path.join(dir, GOAL_BRIDGE_ARTIFACT))}`);
|
|
@@ -529,7 +529,7 @@ async function goalControl(action, args) {
|
|
|
529
529
|
if (!id) throw new Error(`Usage: sks goal ${action} <mission-id|latest>`);
|
|
530
530
|
const { dir } = await loadMission(root, id);
|
|
531
531
|
const workflow = await updateGoalWorkflow(dir, action);
|
|
532
|
-
await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: `GOAL_${String(action).toUpperCase()}`, native_goal: workflow.native_goal, questions_allowed: true, implementation_allowed: action !== 'pause' && action !== 'clear', stop_gate: 'none' });
|
|
532
|
+
await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: `GOAL_${String(action).toUpperCase()}`, native_goal: workflow.native_goal, questions_allowed: true, implementation_allowed: action !== 'pause' && action !== 'clear', stop_gate: 'none' }, { replace: true });
|
|
533
533
|
console.log(`Goal ${action}: ${id}`);
|
|
534
534
|
console.log(`Native Codex control: ${workflow.native_goal.slash_command}`);
|
|
535
535
|
}
|
|
@@ -1626,7 +1626,7 @@ export async function gxCommand(sub, args) {
|
|
|
1626
1626
|
}
|
|
1627
1627
|
|
|
1628
1628
|
export async function team(args) {
|
|
1629
|
-
const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'dashboard', 'event', 'message', 'cleanup-tmux']);
|
|
1629
|
+
const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'dashboard', 'event', 'message', 'open-tmux', 'attach-tmux', 'cleanup-tmux']);
|
|
1630
1630
|
if (teamSubcommands.has(args[0])) return teamCommand(args[0], args.slice(1));
|
|
1631
1631
|
const jsonOutput = flag(args, '--json');
|
|
1632
1632
|
const openTmux = !jsonOutput && !flag(args, '--no-open-tmux') && !flag(args, '--no-tmux');
|
|
@@ -1635,7 +1635,7 @@ export async function team(args) {
|
|
|
1635
1635
|
const { prompt, agentSessions, roleCounts, roster } = opts;
|
|
1636
1636
|
if (!prompt) {
|
|
1637
1637
|
console.error('Usage: sks team "task" [executor:5 reviewer:6 user:1] [--agents N] [--no-open-tmux] [--json]');
|
|
1638
|
-
console.error(' sks team log|tail|watch|lane|status|message|cleanup-tmux [mission-id|latest]');
|
|
1638
|
+
console.error(' sks team log|tail|watch|lane|status|message|open-tmux|attach-tmux|cleanup-tmux [mission-id|latest]');
|
|
1639
1639
|
console.error(' sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."');
|
|
1640
1640
|
console.error(' sks team message [mission-id|latest] --from <agent> --to <agent|all> --message "..."');
|
|
1641
1641
|
process.exitCode = 1;
|
|
@@ -1778,7 +1778,7 @@ export function buildTeamPlan(id, prompt, opts = {}) {
|
|
|
1778
1778
|
reasoning: teamReasoningPolicy(prompt, roster),
|
|
1779
1779
|
codex_config_required: {
|
|
1780
1780
|
service_tier: 'fast',
|
|
1781
|
-
features: { multi_agent: true,
|
|
1781
|
+
features: { multi_agent: true, hooks: true, fast_mode: true, fast_mode_ui: true, codex_git_commit: true, computer_use: true, apps: true, plugins: true },
|
|
1782
1782
|
agents: { max_threads: 6, max_depth: 1 },
|
|
1783
1783
|
custom_agents_dir: '.codex/agents'
|
|
1784
1784
|
},
|
|
@@ -1887,6 +1887,7 @@ export function buildTeamPlan(id, prompt, opts = {}) {
|
|
|
1887
1887
|
'sks team status <mission-id>',
|
|
1888
1888
|
'sks team log <mission-id>',
|
|
1889
1889
|
'sks team tail <mission-id>',
|
|
1890
|
+
'sks team open-tmux <mission-id>',
|
|
1890
1891
|
'sks team watch <mission-id>',
|
|
1891
1892
|
'sks team lane <mission-id> --agent <name> --follow',
|
|
1892
1893
|
'sks team event <mission-id> --agent <name> --phase <phase> --message "..."',
|
|
@@ -1967,7 +1968,7 @@ ${plan.roster.validation_team.map((agent) => `- ${agent.id}: ${agent.persona} [r
|
|
|
1967
1968
|
- Keep team-live.md readable for the user inside Codex App.
|
|
1968
1969
|
- Mirror every useful subagent status, debate result, handoff, review finding, and integration decision to team-transcript.jsonl.
|
|
1969
1970
|
- Use \`sks team event ${plan.mission_id} --agent <name> --phase <phase> --message "..."\` when recording a live event from the parent thread.
|
|
1970
|
-
- The user can inspect the flow with \`sks team log ${plan.mission_id}\`, \`sks team tail ${plan.mission_id}\`, \`sks team watch ${plan.mission_id}\`, or \`sks team lane ${plan.mission_id} --agent analysis_scout_1 --follow\`.
|
|
1971
|
+
- The user can inspect the flow with \`sks team open-tmux ${plan.mission_id}\`, \`sks team log ${plan.mission_id}\`, \`sks team tail ${plan.mission_id}\`, \`sks team watch ${plan.mission_id}\`, or \`sks team lane ${plan.mission_id} --agent analysis_scout_1 --follow\`.
|
|
1971
1972
|
|
|
1972
1973
|
## Phases
|
|
1973
1974
|
|
|
@@ -1989,6 +1990,37 @@ async function teamCommand(sub, args) {
|
|
|
1989
1990
|
return;
|
|
1990
1991
|
}
|
|
1991
1992
|
const { dir } = await loadMission(root, id);
|
|
1993
|
+
if (sub === 'open-tmux' || sub === 'attach-tmux') {
|
|
1994
|
+
const plan = await readJson(path.join(dir, 'team-plan.json'), null);
|
|
1995
|
+
if (!plan) {
|
|
1996
|
+
console.error(`Team plan missing for ${id}; cannot open tmux Team view.`);
|
|
1997
|
+
process.exitCode = 2;
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
const tmux = await launchTmuxTeamView({
|
|
2001
|
+
root,
|
|
2002
|
+
missionId: id,
|
|
2003
|
+
plan,
|
|
2004
|
+
promptFile: path.join(dir, 'team-workflow.md'),
|
|
2005
|
+
json: flag(args, '--json'),
|
|
2006
|
+
attach: sub === 'attach-tmux' || !flag(args, '--no-attach'),
|
|
2007
|
+
args
|
|
2008
|
+
});
|
|
2009
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(tmux, null, 2));
|
|
2010
|
+
if (!tmux.ready) {
|
|
2011
|
+
const reasons = [tmux.opened?.stderr, ...(tmux.blockers || [])].filter(Boolean);
|
|
2012
|
+
console.error(`tmux Team view blocked for ${id}: ${reasons.join('; ') || 'tmux creation failed'}`);
|
|
2013
|
+
if (tmux.attach_command) console.error(`Attach after repair: ${tmux.attach_command}`);
|
|
2014
|
+
process.exitCode = 2;
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
console.log(`tmux: opened ${tmux.opened_lane_count || tmux.lanes?.length || 0} Team lane(s) in ${tmux.session}`);
|
|
2018
|
+
if (tmux.split_ui?.mode) console.log(`tmux UI: ${tmux.split_ui.mode} (${tmux.split_ui.layout})`);
|
|
2019
|
+
if (tmux.split_ui?.current_session) console.log('tmux cockpit: reconciled inside the current SKS tmux window');
|
|
2020
|
+
console.log(`Attach: ${tmux.attach_command}`);
|
|
2021
|
+
console.log(`Watch: sks team watch ${id}`);
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
1992
2024
|
if (sub === 'event') {
|
|
1993
2025
|
const message = readFlagValue(args, '--message', '');
|
|
1994
2026
|
if (!message) {
|
|
@@ -1997,6 +2029,7 @@ async function teamCommand(sub, args) {
|
|
|
1997
2029
|
return;
|
|
1998
2030
|
}
|
|
1999
2031
|
const phase = readFlagValue(args, '--phase', 'general');
|
|
2032
|
+
const plan = await readJson(path.join(dir, 'team-plan.json'), null).catch(() => null);
|
|
2000
2033
|
const record = await appendTeamEvent(dir, {
|
|
2001
2034
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
|
2002
2035
|
phase,
|
|
@@ -2004,16 +2037,29 @@ async function teamCommand(sub, args) {
|
|
|
2004
2037
|
artifact: readFlagValue(args, '--artifact', ''),
|
|
2005
2038
|
message
|
|
2006
2039
|
});
|
|
2040
|
+
const cockpit = plan
|
|
2041
|
+
? await reconcileTmuxTeamCockpit({
|
|
2042
|
+
root,
|
|
2043
|
+
missionId: id,
|
|
2044
|
+
plan,
|
|
2045
|
+
promptFile: path.join(dir, 'team-workflow.md'),
|
|
2046
|
+
close: /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(String(phase || '')),
|
|
2047
|
+
plannedFallback: false
|
|
2048
|
+
}).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'tmux cockpit reconcile failed' }))
|
|
2049
|
+
: null;
|
|
2007
2050
|
const tmuxCleanup = /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(String(phase || ''))
|
|
2008
2051
|
? await requestTeamSessionCleanup(dir, {
|
|
2009
2052
|
missionId: id,
|
|
2010
2053
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
|
2011
2054
|
reason: message,
|
|
2012
|
-
finalMessage: 'Team cleanup event received.
|
|
2055
|
+
finalMessage: 'Team cleanup event received. Managed tmux Team panes may close; follow loops may stop.'
|
|
2013
2056
|
}).then(() => cleanupTmuxTeamView({ root, missionId: id, closeSession: flag(args, '--close-session') || flag(args, '--close') })).catch((err) => ({ ok: false, reason: err.message || 'tmux cleanup failed' }))
|
|
2014
2057
|
: null;
|
|
2015
2058
|
if (flag(args, '--json')) return console.log(JSON.stringify(record, null, 2));
|
|
2016
2059
|
console.log(`${record.ts} [${record.phase}] ${record.agent}: ${record.message}`);
|
|
2060
|
+
if (cockpit?.ok && (cockpit.opened_lane_count || cockpit.closed_lane_count)) {
|
|
2061
|
+
console.log(`tmux cockpit: +${cockpit.opened_lane_count || 0} -${cockpit.closed_lane_count || 0} managed pane(s) in ${cockpit.session}`);
|
|
2062
|
+
}
|
|
2017
2063
|
if (tmuxCleanup) {
|
|
2018
2064
|
if (tmuxCleanup.ok) console.log(`tmux cleanup: marked complete (${tmuxCleanup.reason || 'record updated'})`);
|
|
2019
2065
|
else console.log(`tmux cleanup: skipped (${tmuxCleanup.reason || 'not available'})`);
|
|
@@ -2045,7 +2091,7 @@ async function teamCommand(sub, args) {
|
|
|
2045
2091
|
missionId: id,
|
|
2046
2092
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
|
2047
2093
|
reason: readFlagValue(args, '--reason', 'Team session ended; clean up live follow panes.'),
|
|
2048
|
-
finalMessage: 'Team session ended. Lane/watch follow loops will stop after showing this cleanup summary; tmux panes
|
|
2094
|
+
finalMessage: 'Team session ended. Lane/watch follow loops will stop after showing this cleanup summary; managed tmux Team panes may close.'
|
|
2049
2095
|
});
|
|
2050
2096
|
await appendTeamEvent(dir, {
|
|
2051
2097
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { getCodexInfo } from './codex-adapter.mjs';
|
|
|
7
7
|
export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
|
|
8
8
|
export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
|
|
9
9
|
export const CODEX_REMOTE_CONTROL_MIN_VERSION = '0.130.0';
|
|
10
|
+
const REQUIRED_CODEX_APP_FEATURE_FLAGS = ['codex_git_commit', 'hooks', 'fast_mode', 'computer_use', 'apps', 'plugins'];
|
|
10
11
|
|
|
11
12
|
export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
|
|
12
13
|
const candidates = [];
|
|
@@ -106,10 +107,12 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
106
107
|
const computerUseMcpListed = /computer[-_ ]?use/i.test(mcpText);
|
|
107
108
|
const browserUseMcpListed = /browser[-_ ]?use/i.test(mcpText);
|
|
108
109
|
const imageGenerationReady = codexFeatureEnabled(featureText, 'image_generation');
|
|
110
|
+
const requiredFeatureFlags = Object.fromEntries(REQUIRED_CODEX_APP_FEATURE_FLAGS.map((name) => [name, codexFeatureEnabled(featureText, name)]));
|
|
111
|
+
const requiredFeatureFlagsOk = Object.values(requiredFeatureFlags).every(Boolean);
|
|
109
112
|
const computerUseReady = computerUseMcpListed || Boolean(computerUsePath);
|
|
110
113
|
const browserUseReady = browserUseMcpListed || Boolean(browserUsePath);
|
|
111
114
|
const appInstalled = Boolean(appPath);
|
|
112
|
-
const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && imageGenerationReady && computerUseReady && browserUseReady;
|
|
115
|
+
const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && imageGenerationReady && computerUseReady && browserUseReady;
|
|
113
116
|
return {
|
|
114
117
|
ok: ready,
|
|
115
118
|
app: {
|
|
@@ -136,6 +139,9 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
136
139
|
features: {
|
|
137
140
|
checked: featureList.checked,
|
|
138
141
|
ok: featureList.ok,
|
|
142
|
+
...requiredFeatureFlags,
|
|
143
|
+
required_flags: requiredFeatureFlags,
|
|
144
|
+
required_flags_ok: requiredFeatureFlagsOk,
|
|
139
145
|
image_generation: imageGenerationReady,
|
|
140
146
|
image_generation_source: imageGenerationReady ? 'codex_features_list' : 'missing',
|
|
141
147
|
stdout: featureList.stdout,
|
|
@@ -145,7 +151,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
145
151
|
computer_use_cache: computerUsePath,
|
|
146
152
|
browser_use_cache: browserUsePath
|
|
147
153
|
},
|
|
148
|
-
guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
|
|
154
|
+
guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
|
|
149
155
|
};
|
|
150
156
|
}
|
|
151
157
|
|
|
@@ -200,7 +206,7 @@ export function formatCodexRemoteControlStatus(status) {
|
|
|
200
206
|
return lines.filter(Boolean).join('\n');
|
|
201
207
|
}
|
|
202
208
|
|
|
203
|
-
export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
|
|
209
|
+
export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
|
|
204
210
|
const lines = [];
|
|
205
211
|
if (!appInstalled) {
|
|
206
212
|
lines.push('Install and open Codex App for first-party MCP/plugin tools. SKS tmux launch can still run with Codex CLI alone, but Codex Computer Use and imagegen/gpt-image-2 evidence will be unavailable until Codex App is ready.');
|
|
@@ -221,6 +227,11 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, im
|
|
|
221
227
|
lines.push(`Codex feature check failed: ${summarizeCodexMcpError(featureList.stderr || featureList.stdout)}`);
|
|
222
228
|
lines.push('Verify with: codex features list');
|
|
223
229
|
}
|
|
230
|
+
if (featureList?.checked && featureList.ok && !requiredFeatureFlagsOk) {
|
|
231
|
+
const missing = missingRequiredFeatureFlags(requiredFeatureFlags);
|
|
232
|
+
lines.push(`Codex App feature flag(s) disabled or missing: ${missing.join(', ')}. Commit message generation and app-only tool paths can fail even when CLI chat works.`);
|
|
233
|
+
lines.push('Verify with: codex features list | rg "codex_git_commit|hooks|fast_mode|computer_use|apps|plugins"');
|
|
234
|
+
}
|
|
224
235
|
if (appInstalled && (!computerUseReady || !browserUseReady)) {
|
|
225
236
|
lines.push('Open Codex App settings and enable recommended MCP/plugin tools. Codex CLI 0.130.0+ remote-control/app-server sessions can pick up config changes live; restart older CLI/TUI sessions.');
|
|
226
237
|
lines.push('Required for SKS QA-LOOP UI/browser evidence: Codex Computer Use only. Browser Use can support non-UI browser context, but it does not satisfy UI-level E2E verification.');
|
|
@@ -232,7 +243,7 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, im
|
|
|
232
243
|
lines.push('Codex image_generation was not visible from `codex features list`. Required imagegen/gpt-image-2 evidence must stay blocked or unverified until $imagegen is available in Codex App.');
|
|
233
244
|
}
|
|
234
245
|
if (computerUseReady && !computerUseMcpListed) {
|
|
235
|
-
lines.push('Computer Use plugin files are installed, but this check cannot prove the current thread exposes the live Computer Use tools. Start a new Codex App thread and invoke @Computer or @AppName
|
|
246
|
+
lines.push('Computer Use plugin files are installed, but this check cannot prove the current thread exposes the live Computer Use tools. Start a new Codex App thread and invoke @Computer or @AppName for the actual target app or screen; Codex App readiness itself should stay on `codex features list`, `codex mcp list`, and `sks codex-app check`.');
|
|
236
247
|
}
|
|
237
248
|
if (browserUseReady && !browserUseMcpListed) {
|
|
238
249
|
lines.push('Browser Use plugin files are installed, but `codex mcp list` does not list a browser-use MCP server. Treat Browser Use as plugin-scoped, not as SKS UI verification evidence.');
|
|
@@ -248,6 +259,7 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
|
|
|
248
259
|
`Codex App: ${status.app.installed ? 'ok' : 'missing'}${status.app.path ? ` ${status.app.path}` : ''}`,
|
|
249
260
|
`Codex CLI: ${status.codex_cli.ok ? 'ok' : 'missing'}${status.codex_cli.version ? ` ${status.codex_cli.version}` : ''}`,
|
|
250
261
|
`Remote Ctrl: ${status.remote_control?.ok ? 'ok' : 'missing'}${status.remote_control?.codex_cli?.version_number ? ` min ${status.remote_control.min_version}` : ''}`,
|
|
262
|
+
`App Flags: ${status.features?.required_flags_ok ? 'ok' : `missing ${missingRequiredFeatureFlags(status.features?.required_flags).join(', ') || 'required flags'}`}`,
|
|
251
263
|
`Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
|
|
252
264
|
`Browser Use: ${status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
|
|
253
265
|
`Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
|
|
@@ -273,12 +285,15 @@ function summarizeCodexMcpError(text) {
|
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
function codexFeatureEnabled(text, featureName) {
|
|
276
|
-
const
|
|
277
|
-
return
|
|
288
|
+
const expected = String(featureName || '').toLowerCase();
|
|
289
|
+
return String(text || '').split(/\r?\n/).some((line) => {
|
|
290
|
+
const parts = line.trim().split(/\s+/).filter(Boolean);
|
|
291
|
+
return parts[0]?.toLowerCase() === expected && parts[parts.length - 1]?.toLowerCase() === 'true';
|
|
292
|
+
});
|
|
278
293
|
}
|
|
279
294
|
|
|
280
|
-
function
|
|
281
|
-
return
|
|
295
|
+
function missingRequiredFeatureFlags(flags = {}) {
|
|
296
|
+
return REQUIRED_CODEX_APP_FEATURE_FLAGS.filter((name) => flags?.[name] !== true);
|
|
282
297
|
}
|
|
283
298
|
|
|
284
299
|
function remoteControlGuidance(status = {}) {
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.7.
|
|
8
|
+
export const PACKAGE_VERSION = '0.7.64';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -744,6 +744,7 @@ async function teamLiveDigest(root, state = {}) {
|
|
|
744
744
|
const lines = events.map(formatTeamDigestEvent);
|
|
745
745
|
const context = boundText([
|
|
746
746
|
`SKS Team live digest: mission ${id}, phase ${phase}, source ${source}.`,
|
|
747
|
+
`Open tmux multi-view with: sks team open-tmux ${id}`,
|
|
747
748
|
`Open live view with: sks team watch ${id}`,
|
|
748
749
|
'Recent events:',
|
|
749
750
|
...lines.map((line) => `- ${line}`)
|
package/src/core/init.mjs
CHANGED
|
@@ -440,13 +440,17 @@ function installPolicy(scope, commandPrefix) {
|
|
|
440
440
|
function mergeManagedCodexConfigToml(existingContent = '') {
|
|
441
441
|
let next = removeLegacyTopLevelCodexModeLocks(String(existingContent || '').trimEnd());
|
|
442
442
|
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
443
|
-
next = removeTomlTableKey(next, 'features', '
|
|
443
|
+
next = removeTomlTableKey(next, 'features', 'codex_hooks');
|
|
444
444
|
next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
|
|
445
445
|
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
446
|
-
next = upsertTomlTableKey(next, 'features', '
|
|
446
|
+
next = upsertTomlTableKey(next, 'features', 'hooks = true');
|
|
447
447
|
next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
|
|
448
448
|
next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
|
|
449
449
|
next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
|
|
450
|
+
next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
|
|
451
|
+
next = upsertTomlTableKey(next, 'features', 'computer_use = true');
|
|
452
|
+
next = upsertTomlTableKey(next, 'features', 'apps = true');
|
|
453
|
+
next = upsertTomlTableKey(next, 'features', 'plugins = true');
|
|
450
454
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
|
|
451
455
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
|
|
452
456
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
|
|
@@ -733,7 +737,7 @@ function codexAppQuickReference(scope, commandPrefix) {
|
|
|
733
737
|
`Context Tracking: TriWiki SSOT. Before each route phase read only the latest coordinate+voxel overlay pack at .sneakoscope/wiki/context-pack.json; coordinate-only legacy packs are invalid. Use attention.use_first for compact high-trust recall and hydrate attention.hydrate_first from source before risky/lower-trust decisions. During every stage hydrate low-trust claims from source/hash/RGBA anchors; after changes run ${commandPrefix} wiki refresh or pack; before handoff/final run ${commandPrefix} wiki validate .sneakoscope/wiki/context-pack.json.`,
|
|
734
738
|
stackCurrentDocsPolicyText(commandPrefix),
|
|
735
739
|
`Team review: ${MIN_TEAM_REVIEW_POLICY_TEXT}`,
|
|
736
|
-
`Team tmux view: ${commandPrefix} team "task" prepares live watch/lane commands and
|
|
740
|
+
`Team tmux view: ${commandPrefix} team "task" prepares live watch/lane commands and reconciles managed Team panes inside the current SKS-owned tmux session when available; add --no-open-tmux for artifact-only creation or --separate-session to force the named sks-team-* fallback. Existing hook-created Team missions can be opened later with ${commandPrefix} team open-tmux latest. The view keeps the main Codex pane alive, adds an overview watch pane plus color-coded split per-agent lanes, and closes only SKS-managed Team panes as agent lanes finish or cleanup is requested; ${commandPrefix} team lane latest --agent analysis_scout_1 --follow shows one agent's status, assigned runtime tasks, recent agent events, direct messages, and fallback global tail; ${commandPrefix} team message latest --from analysis_scout_1 --to executor_1 --message "handoff note" mirrors bounded agent communication into transcript/lane panes; ${commandPrefix} team cleanup-tmux latest marks the SKS session record complete and asks managed panes/follow loops to close or show a cleanup summary.`,
|
|
737
741
|
`Runtime: open Codex App once, then run ${commandPrefix} bootstrap and ${commandPrefix} deps check. Bare ${commandPrefix} opens or reuses the default tmux/Codex CLI session; before launch it checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. ${commandPrefix} codex-app remote-control wraps the Codex CLI 0.130.0+ headless remote-control entrypoint. ${commandPrefix} tmux open is the explicit form for session/workspace flags.`,
|
|
738
742
|
`Guard: generated harness files are immutable outside the engine source repo; check ${commandPrefix} guard check; conflicts use ${commandPrefix} conflicts prompt with human approval.`
|
|
739
743
|
].join('\n') + '\n';
|
package/src/core/mission.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export async function findLatestMission(root) {
|
|
|
62
62
|
return candidates.at(-1)?.id || null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export async function setCurrent(root, patch) {
|
|
66
|
-
const current = await readJson(stateFile(root), {});
|
|
65
|
+
export async function setCurrent(root, patch, opts = {}) {
|
|
66
|
+
const current = opts.replace ? {} : await readJson(stateFile(root), {});
|
|
67
67
|
await writeJsonAtomic(stateFile(root), { ...current, ...patch, updated_at: nowIso() });
|
|
68
68
|
}
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -803,7 +803,7 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
|
|
|
803
803
|
transcript: 'team-transcript.jsonl',
|
|
804
804
|
dashboard: 'team-dashboard.json',
|
|
805
805
|
tmux: 'CLI Team entrypoints open tmux live lanes for the visible Team agent budget when tmux is available.',
|
|
806
|
-
commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow']
|
|
806
|
+
commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team open-tmux latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow']
|
|
807
807
|
},
|
|
808
808
|
required_artifacts: ['team-roster.json', 'team-analysis.md', ...(fromChatImgRequired ? [FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT] : []), 'team-consensus.md', ...teamRuntimeRequiredArtifacts(), 'team-review.md', 'team-gate.json', TEAM_SESSION_CLEANUP_ARTIFACT, 'reflection.md', 'reflection-gate.json', 'team-live.md', 'team-transcript.jsonl', 'team-dashboard.json', '.sneakoscope/wiki/context-pack.json', 'context7-evidence.jsonl']
|
|
809
809
|
};
|
|
@@ -894,7 +894,7 @@ async function prepareTeam(root, route, task, required, opts = {}) {
|
|
|
894
894
|
transcript: 'team-transcript.jsonl',
|
|
895
895
|
dashboard: 'team-dashboard.json',
|
|
896
896
|
tmux: 'CLI Team entrypoints open tmux live lanes for the visible Team agent budget when tmux is available.',
|
|
897
|
-
commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow', 'sks team event latest --agent <name> --phase <phase> --message "..."']
|
|
897
|
+
commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team open-tmux latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow', 'sks team event latest --agent <name> --phase <phase> --message "..."']
|
|
898
898
|
},
|
|
899
899
|
required_artifacts: ['team-roster.json', 'team-analysis.md', ...(fromChatImgRequired ? [FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT] : []), 'team-consensus.md', ...teamRuntimeRequiredArtifacts(), 'team-review.md', 'team-gate.json', TEAM_SESSION_CLEANUP_ARTIFACT, 'reflection.md', 'reflection-gate.json', 'team-live.md', 'team-transcript.jsonl', 'team-dashboard.json', '.sneakoscope/wiki/context-pack.json', 'context7-evidence.jsonl']
|
|
900
900
|
};
|
package/src/core/routes.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export const CODEX_COMPUTER_USE_EVIDENCE_SOURCE = 'codex_computer_use';
|
|
|
33
33
|
export const CODEX_IMAGEGEN_EVIDENCE_SOURCE = 'codex_app_imagegen_gpt_image_2';
|
|
34
34
|
export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/codex/app/features#image-generation';
|
|
35
35
|
export const OPENAI_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/api/docs/guides/image-generation';
|
|
36
|
-
export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use or install Playwright packages, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable, mark the UI/browser evidence unverified instead of substituting another tool. In Codex App prompts, invoke @Computer or @AppName in a new thread when live Computer Use tools are needed; SKS hooks and skills can require the policy but cannot attach missing host tools to an already-started turn.';
|
|
36
|
+
export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use or install Playwright packages, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable for the target UI, mark the UI/browser evidence unverified instead of substituting another tool. Codex App readiness/config verification is not target-UI evidence: use the Codex-provided control surfaces `codex features list`, `codex mcp list`, `sks codex-app check`, remote-control status, and plugin/tool exposure, not direct OS Accessibility control of the Codex App bundle. In Codex App prompts, invoke @Computer or @AppName in a new thread when live Computer Use tools are needed for the actual target app or screen; SKS hooks and skills can require the policy but cannot attach missing host tools to an already-started turn.';
|
|
37
37
|
export const CODEX_IMAGEGEN_REQUIRED_POLICY = 'Pipeline image generation, raster asset creation/editing, and generated image-review evidence must use real Codex App imagegen/$imagegen with gpt-image-2 when that evidence is required. Do not substitute placeholder SVG/HTML/CSS, prose-only critique, stock-like stand-ins, manually fabricated files, or missing-output ledgers for requested/generated raster assets or required generated review images. If imagegen/gpt-image-2 is unavailable, record the blocker and mark the image asset or review evidence unverified instead of passing the gate. In Codex App prompts, invoke $imagegen when live image generation is needed; SKS hooks and skills can require the policy but cannot attach missing host image-generation tools to an already-started turn.';
|
|
38
38
|
export const RESERVED_CODEX_PLUGIN_SKILL_NAMES = Object.freeze(['computer-use', 'browser', 'browser-use']);
|
|
39
39
|
export const FORBIDDEN_BROWSER_AUTOMATION_RE = /\b(playwright|chrome\s+mcp|browser\s+use|selenium|puppeteer)\b/i;
|
|
@@ -308,7 +308,7 @@ export const ROUTES = [
|
|
|
308
308
|
context7Policy: 'optional',
|
|
309
309
|
reasoningPolicy: 'high',
|
|
310
310
|
stopGate: 'team-gate.json',
|
|
311
|
-
cliEntrypoint: 'sks team "task" [executor:5 reviewer:6 user:1] | sks team log|tail|watch|lane|status|event|message|cleanup-tmux',
|
|
311
|
+
cliEntrypoint: 'sks team "task" [executor:5 reviewer:6 user:1] | sks team log|tail|watch|lane|status|event|message|open-tmux|attach-tmux|cleanup-tmux',
|
|
312
312
|
examples: ['$Team executor:5 agree on the best plan and implement it', '$From-Chat-IMG 채팅+첨부 이미지 작업 지시서']
|
|
313
313
|
},
|
|
314
314
|
{
|
|
@@ -548,7 +548,7 @@ export const COMMAND_CATALOG = [
|
|
|
548
548
|
{ name: 'validate-artifacts', usage: 'sks validate-artifacts [mission-id|latest] [--json]', description: 'Validate schema-backed mission artifacts for work orders, effort decisions, visual maps, dogfood reports, skills, mistake memory, Team dashboard state, and Honest Mode.' },
|
|
549
549
|
{ name: 'wiki', usage: 'sks wiki coords|pack|refresh|prune|validate ...', description: 'Build, refresh, prune, and validate RGBA/trig LLM Wiki context packs with attention.use_first and attention.hydrate_first for compact recall plus source hydration.' },
|
|
550
550
|
{ name: 'hproof', usage: 'sks hproof check [mission-id|latest]', description: 'Evaluate the H-Proof done gate for a mission.' },
|
|
551
|
-
{ name: 'team', usage: 'sks team "task" [executor:5 reviewer:6 user:1]|log|tail|watch|lane|status|dashboard|event|message|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with at least five reviewer/QA validation lanes,
|
|
551
|
+
{ name: 'team', usage: 'sks team "task" [executor:5 reviewer:6 user:1]|log|tail|watch|lane|status|dashboard|event|message|open-tmux|attach-tmux|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with at least five reviewer/QA validation lanes, current-session managed tmux lanes when available, transcript messages, and cleanup-aware follow panes.' },
|
|
552
552
|
{ name: 'reasoning', usage: 'sks reasoning ["prompt"] [--json]', description: 'Show SKS temporary reasoning-effort routing: medium for simple tasks, high for logic, xhigh for research.' },
|
|
553
553
|
{ name: 'gx', usage: 'sks gx init|render|validate|drift|snapshot [name]', description: 'Create and verify deterministic SVG/HTML visual context cartridges.' },
|
|
554
554
|
{ name: 'profile', usage: 'sks profile show|set <model>', description: 'Inspect or set the current SKS model profile metadata.' },
|
package/src/core/team-live.mjs
CHANGED
|
@@ -151,6 +151,7 @@ export function defaultTeamDashboard(id, prompt, opts = {}) {
|
|
|
151
151
|
status: `sks team status ${id}`,
|
|
152
152
|
log: `sks team log ${id}`,
|
|
153
153
|
tail: `sks team tail ${id}`,
|
|
154
|
+
open_tmux: `sks team open-tmux ${id}`,
|
|
154
155
|
watch: `sks team watch ${id}`,
|
|
155
156
|
lane: `sks team lane ${id} --agent <agent> --follow`,
|
|
156
157
|
event: `sks team event ${id} --agent <agent> --phase <phase> --message "..."`,
|
|
@@ -204,6 +205,7 @@ ${prompt}
|
|
|
204
205
|
sks team status ${id}
|
|
205
206
|
sks team log ${id}
|
|
206
207
|
sks team tail ${id}
|
|
208
|
+
sks team open-tmux ${id}
|
|
207
209
|
sks team watch ${id}
|
|
208
210
|
sks team lane ${id} --agent analysis_scout_1 --follow
|
|
209
211
|
sks team event ${id} --agent analysis_scout_1 --phase parallel_analysis_scouting --message "mapped repo slice"
|
|
@@ -313,7 +315,7 @@ export function parseTeamSpecArgs(args = []) {
|
|
|
313
315
|
i++;
|
|
314
316
|
continue;
|
|
315
317
|
}
|
|
316
|
-
if (arg === '--json' || arg === '--open-tmux' || arg === '--tmux-open' || arg === '--no-open-tmux' || arg === '--no-tmux' || arg === '--no-attach') continue;
|
|
318
|
+
if (arg === '--json' || arg === '--open-tmux' || arg === '--tmux-open' || arg === '--no-open-tmux' || arg === '--no-tmux' || arg === '--no-attach' || arg === '--separate-session' || arg === '--new-session' || arg === '--legacy-team-session' || arg === '--no-dynamic-team-tmux') continue;
|
|
317
319
|
cleanArgs.push(args[i]);
|
|
318
320
|
}
|
|
319
321
|
return { cleanArgs, ...normalizeTeamSpec({ roleCounts, agentSessions: explicitSession }) };
|
|
@@ -444,6 +446,7 @@ export async function appendTeamEvent(dir, event) {
|
|
|
444
446
|
dashboard.agents[agent].last_seen = record.ts;
|
|
445
447
|
await writeJsonAtomic(files.dashboard, dashboard);
|
|
446
448
|
}
|
|
449
|
+
await reconcileTeamTmuxFromEvent(dir, record).catch(() => null);
|
|
447
450
|
const current = await readText(files.live, teamLiveMarkdown('unknown', 'unknown'));
|
|
448
451
|
const target = record.to ? ` -> ${record.to}` : '';
|
|
449
452
|
const line = `\n- ${record.ts} [${record.phase || 'general'}] ${record.agent || 'unknown'}${target}: ${record.message || ''}${record.artifact ? ` (${record.artifact})` : ''}\n`;
|
|
@@ -451,6 +454,22 @@ export async function appendTeamEvent(dir, event) {
|
|
|
451
454
|
return record;
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
async function reconcileTeamTmuxFromEvent(dir, record = {}) {
|
|
458
|
+
if (!process.env.TMUX || String(process.env.SKS_TMUX_EVENT_RECONCILE || '1') === '0') return null;
|
|
459
|
+
if (record.type === 'tmux_lane_opened') return null;
|
|
460
|
+
const missionId = path.basename(dir);
|
|
461
|
+
const root = path.resolve(dir, '..', '..', '..');
|
|
462
|
+
const cockpitState = await readJson(path.join(root, '.sneakoscope', 'state', 'tmux-cockpit.json'), {}).catch(() => ({}));
|
|
463
|
+
if (!cockpitState?.missions?.[missionId]) return null;
|
|
464
|
+
const plan = await readJson(path.join(dir, 'team-plan.json'), null).catch(() => null);
|
|
465
|
+
if (!plan) return null;
|
|
466
|
+
const { reconcileTmuxTeamCockpit } = await import('./tmux-ui.mjs');
|
|
467
|
+
const phase = String(record.phase || '');
|
|
468
|
+
const type = String(record.type || '');
|
|
469
|
+
const close = /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(phase) || /^cleanup$/i.test(type);
|
|
470
|
+
return reconcileTmuxTeamCockpit({ root, missionId, plan, close, plannedFallback: false });
|
|
471
|
+
}
|
|
472
|
+
|
|
454
473
|
export async function readTeamControl(dir) {
|
|
455
474
|
return readJson(teamLogPaths(dir).control, defaultTeamControl(path.basename(dir)));
|
|
456
475
|
}
|
|
@@ -466,7 +485,7 @@ export async function requestTeamSessionCleanup(dir, opts = {}) {
|
|
|
466
485
|
cleanup_requested_at: opts.ts || nowIso(),
|
|
467
486
|
cleanup_requested_by: opts.agent || 'parent_orchestrator',
|
|
468
487
|
cleanup_reason: opts.reason || 'Team session cleanup requested.',
|
|
469
|
-
final_message: opts.finalMessage || 'Team session ended. Lane follow loops may stop; tmux panes
|
|
488
|
+
final_message: opts.finalMessage || 'Team session ended. Lane follow loops may stop; managed tmux Team panes may close.'
|
|
470
489
|
};
|
|
471
490
|
await writeJsonAtomic(files.control, next);
|
|
472
491
|
return next;
|
|
@@ -486,7 +505,7 @@ export function renderTeamCleanupSummary(control = {}) {
|
|
|
486
505
|
`Requested by: ${control.cleanup_requested_by || 'unknown'}`,
|
|
487
506
|
`Reason: ${control.cleanup_reason || 'Team session cleanup requested.'}`,
|
|
488
507
|
'',
|
|
489
|
-
control.final_message || 'Team session ended. tmux panes
|
|
508
|
+
control.final_message || 'Team session ended. managed tmux Team panes may close.'
|
|
490
509
|
].join('\n');
|
|
491
510
|
}
|
|
492
511
|
|
|
@@ -569,6 +588,8 @@ export async function renderTeamWatch(dir, opts = {}) {
|
|
|
569
588
|
'',
|
|
570
589
|
'## Split-Screen Map',
|
|
571
590
|
'- This overview pane follows the whole mission transcript.',
|
|
591
|
+
'- Run `sks team open-tmux ...` to materialize or reopen the split-pane Team tmux view for an existing mission.',
|
|
592
|
+
'- Inside an SKS-owned tmux session, Team panes are reconciled in the current window and managed panes may close as agent lanes finish.',
|
|
572
593
|
'- Neighbor tmux panes follow individual `sks team lane ... --agent <name>` views.',
|
|
573
594
|
'- Use `sks team event ...` to mirror scout, debate, executor, review, and verification status into the live panes.',
|
|
574
595
|
'- Use `sks team message ... --from <agent> --to <agent|all>` for bounded inter-agent communication in transcript/lane views.',
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { getCodexInfo } from './codex-adapter.mjs';
|
|
|
7
7
|
import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
|
|
8
8
|
import { REQUIRED_CODEX_MODEL, forceGpt55CodexArgs } from './codex-model-guard.mjs';
|
|
9
9
|
import { MIN_TEAM_REVIEWER_LANES } from './team-review-policy.mjs';
|
|
10
|
-
import { appendTeamEvent } from './team-live.mjs';
|
|
10
|
+
import { appendTeamEvent, readTeamControl, readTeamDashboard, teamCleanupRequested } from './team-live.mjs';
|
|
11
11
|
|
|
12
12
|
const SKS_FIGLET_FONT = 'Standard';
|
|
13
13
|
|
|
@@ -97,6 +97,28 @@ export function tmuxTeamStatePath(root = process.cwd()) {
|
|
|
97
97
|
return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-team-sessions.json');
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function tmuxCockpitStatePath(root = process.cwd()) {
|
|
101
|
+
return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-cockpit.json');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const TERMINAL_TEAM_AGENT_STATUSES = new Set([
|
|
105
|
+
'agent_closed',
|
|
106
|
+
'agent_done',
|
|
107
|
+
'cancelled',
|
|
108
|
+
'canceled',
|
|
109
|
+
'cleanup',
|
|
110
|
+
'cleanup_requested',
|
|
111
|
+
'closed',
|
|
112
|
+
'complete',
|
|
113
|
+
'completed',
|
|
114
|
+
'done',
|
|
115
|
+
'ended',
|
|
116
|
+
'failed',
|
|
117
|
+
'stopped',
|
|
118
|
+
'terminal',
|
|
119
|
+
'tmux_lane_closed'
|
|
120
|
+
]);
|
|
121
|
+
|
|
100
122
|
export function isTmuxShellSession(env = process.env) {
|
|
101
123
|
return Boolean(String(env.TMUX || '').trim());
|
|
102
124
|
}
|
|
@@ -298,7 +320,8 @@ export function formatTmuxBanner(status = null) {
|
|
|
298
320
|
' sks open or attach the default tmux Codex CLI session',
|
|
299
321
|
' sks tmux open open or attach a tmux Codex CLI session with explicit flags',
|
|
300
322
|
' sks --mad open one-shot MAD full-access auto-review tmux session',
|
|
301
|
-
' sks team "task" prepare Team mission and
|
|
323
|
+
' sks team "task" prepare Team mission and reconcile Team panes in the current SKS tmux session when available',
|
|
324
|
+
' sks team open-tmux latest reopen current-session panes, or use --separate-session for the legacy view',
|
|
302
325
|
'',
|
|
303
326
|
'Useful terminal commands:',
|
|
304
327
|
' sks commands',
|
|
@@ -342,6 +365,116 @@ async function tmuxWindowTarget(bin, session) {
|
|
|
342
365
|
return windowId || fallback;
|
|
343
366
|
}
|
|
344
367
|
|
|
368
|
+
async function currentTmuxTarget(bin, env = process.env) {
|
|
369
|
+
if (!isTmuxShellSession(env)) return { ok: false, reason: 'not running inside tmux' };
|
|
370
|
+
const run = await tmuxRun(bin, ['display-message', '-p', '#{session_name}\t#{window_id}\t#{pane_id}'], { timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
371
|
+
if (run.code !== 0) return { ok: false, reason: run.stderr || run.stdout || 'tmux display-message failed' };
|
|
372
|
+
const [session, windowId, paneId] = String(run.stdout || '').trim().split('\t');
|
|
373
|
+
if (!session || !windowId) return { ok: false, reason: 'tmux did not report current session/window' };
|
|
374
|
+
return { ok: true, session: sanitizeTmuxSessionName(session), window_id: windowId, pane_id: paneId || null };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function isRecordedSksTmuxSession(root, session) {
|
|
378
|
+
const state = await readJson(tmuxStatePath(root), {}).catch(() => ({}));
|
|
379
|
+
const sessions = state.sessions && typeof state.sessions === 'object' ? state.sessions : {};
|
|
380
|
+
const record = sessions[sanitizeTmuxSessionName(session)] || null;
|
|
381
|
+
if (!record) return { ok: false, reason: 'current tmux session is not recorded as an SKS session' };
|
|
382
|
+
const recordRoot = path.resolve(record.root || root || process.cwd());
|
|
383
|
+
const resolvedRoot = path.resolve(root || process.cwd());
|
|
384
|
+
if (recordRoot !== resolvedRoot) return { ok: false, reason: `recorded SKS session belongs to ${recordRoot}` };
|
|
385
|
+
return { ok: true, record };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isTerminalTeamAgentStatus(status = '') {
|
|
389
|
+
const normalized = String(status || '').trim().toLowerCase();
|
|
390
|
+
return TERMINAL_TEAM_AGENT_STATUSES.has(normalized) || /(?:^|_)(?:done|complete|completed|closed|cleanup|cancelled|canceled|failed|ended|stopped)(?:_|$)/.test(normalized);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function teamCockpitAgentIds(plan = {}, dashboard = null, control = null, opts = {}) {
|
|
394
|
+
if (teamCleanupRequested(control)) return [];
|
|
395
|
+
const visible = teamViewAgentIds(plan).filter((id) => id && id !== 'mission_overview');
|
|
396
|
+
const agents = dashboard?.agents && typeof dashboard.agents === 'object' ? dashboard.agents : null;
|
|
397
|
+
if (!agents) return opts.plannedFallback ? visible : [];
|
|
398
|
+
const active = [];
|
|
399
|
+
for (const id of visible) {
|
|
400
|
+
const entry = agents[id] || {};
|
|
401
|
+
const status = String(entry.status || '').trim().toLowerCase();
|
|
402
|
+
if (!status || status === 'pending') continue;
|
|
403
|
+
if (isTerminalTeamAgentStatus(status)) continue;
|
|
404
|
+
active.push(id);
|
|
405
|
+
}
|
|
406
|
+
if (!active.length && opts.plannedFallback) return visible;
|
|
407
|
+
return uniqueAgentIds(active);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function teamCockpitLanes(plan = {}, dashboard = null, control = null, opts = {}) {
|
|
411
|
+
const agents = teamCockpitAgentIds(plan, dashboard, control, opts);
|
|
412
|
+
if (!agents.length) return [];
|
|
413
|
+
const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(opts.root, opts.missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
|
|
414
|
+
return [
|
|
415
|
+
overview,
|
|
416
|
+
...agents.map((agent) => {
|
|
417
|
+
const style = teamLaneStyle(agent);
|
|
418
|
+
return {
|
|
419
|
+
agent,
|
|
420
|
+
role: style.role,
|
|
421
|
+
command: teamAgentCommand(opts.root, opts.missionId, agent, teamLanePhase(agent)),
|
|
422
|
+
style,
|
|
423
|
+
title: teamLaneTitle(agent)
|
|
424
|
+
};
|
|
425
|
+
})
|
|
426
|
+
];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseTmuxPaneLines(stdout = '') {
|
|
430
|
+
return String(stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
|
|
431
|
+
const [pane_id, title, command, managed, mission_id, agent, role] = line.split('\t');
|
|
432
|
+
return {
|
|
433
|
+
pane_id,
|
|
434
|
+
title: title || '',
|
|
435
|
+
command: command || '',
|
|
436
|
+
managed: managed === '1' || managed === 'true',
|
|
437
|
+
mission_id: mission_id || '',
|
|
438
|
+
agent: agent || '',
|
|
439
|
+
role: role || ''
|
|
440
|
+
};
|
|
441
|
+
}).filter((pane) => /^%\d+$/.test(pane.pane_id || ''));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function listTmuxWindowPanes(bin, windowId) {
|
|
445
|
+
const format = ['#{pane_id}', '#{pane_title}', '#{pane_current_command}', '#{@sks_team_managed}', '#{@sks_mission_id}', '#{@sks_agent_id}', '#{@sks_lane_role}'].join('\t');
|
|
446
|
+
const run = await tmuxRun(bin, ['list-panes', '-t', windowId, '-F', format], { timeoutMs: 5000, maxOutputBytes: 32 * 1024 });
|
|
447
|
+
if (run.code !== 0) return { ok: false, panes: [], stderr: run.stderr || run.stdout || 'tmux list-panes failed' };
|
|
448
|
+
return { ok: true, panes: parseTmuxPaneLines(run.stdout) };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function setTmuxPaneUserOptions(bin, paneId, options = {}) {
|
|
452
|
+
const applied = [];
|
|
453
|
+
const failed = [];
|
|
454
|
+
for (const [key, value] of Object.entries(options)) {
|
|
455
|
+
const run = await tmuxRun(bin, ['set-option', '-pt', paneId, key, String(value)], { timeoutMs: 5000 });
|
|
456
|
+
const command = [path.basename(bin), 'set-option', '-pt', paneId, key, String(value)].join(' ');
|
|
457
|
+
if (run.code === 0) applied.push(command);
|
|
458
|
+
else failed.push({ command, stderr: run.stderr || run.stdout || 'tmux set-option failed' });
|
|
459
|
+
}
|
|
460
|
+
return { applied, failed };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function writeTmuxCockpitRecord(root, record = {}) {
|
|
464
|
+
if (!record.mission_id || !record.session) return null;
|
|
465
|
+
const statePath = tmuxCockpitStatePath(root);
|
|
466
|
+
const state = await readJson(statePath, {}).catch(() => ({}));
|
|
467
|
+
const now = nowIso();
|
|
468
|
+
const nextRecord = { ...record, schema_version: 1, root: path.resolve(root || process.cwd()), updated_at: now };
|
|
469
|
+
const missions = state.missions && typeof state.missions === 'object' ? state.missions : {};
|
|
470
|
+
await writeJsonAtomic(statePath, {
|
|
471
|
+
schema_version: 1,
|
|
472
|
+
updated_at: now,
|
|
473
|
+
missions: { ...missions, [record.mission_id]: nextRecord }
|
|
474
|
+
});
|
|
475
|
+
return nextRecord;
|
|
476
|
+
}
|
|
477
|
+
|
|
345
478
|
function tmuxLayoutName(value = 'tiled') {
|
|
346
479
|
const layout = String(value || 'tiled').trim();
|
|
347
480
|
return /^(tiled|even-horizontal|even-vertical|main-horizontal|main-horizontal-mirrored|main-vertical|main-vertical-mirrored)$/.test(layout)
|
|
@@ -558,6 +691,97 @@ function teamLanePhase(agentId = '') {
|
|
|
558
691
|
return 'team';
|
|
559
692
|
}
|
|
560
693
|
|
|
694
|
+
export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, promptFile = null, dashboard = undefined, control = undefined, close = false, plannedFallback = false, env = process.env, tmux = null } = {}) {
|
|
695
|
+
const resolvedRoot = path.resolve(root || await sksRoot());
|
|
696
|
+
const id = missionId || 'latest';
|
|
697
|
+
if (String(env.SKS_TMUX_DYNAMIC_TEAM || '1') === '0') return { ok: false, skipped: true, reason: 'SKS_TMUX_DYNAMIC_TEAM=0' };
|
|
698
|
+
const tmuxBin = tmux?.bin || await findTmuxBin() || 'tmux';
|
|
699
|
+
const target = await currentTmuxTarget(tmuxBin, env);
|
|
700
|
+
if (!target.ok) return { ok: false, skipped: true, reason: target.reason };
|
|
701
|
+
const ownership = await isRecordedSksTmuxSession(resolvedRoot, target.session);
|
|
702
|
+
if (!ownership.ok) return { ok: false, skipped: true, session: target.session, reason: ownership.reason };
|
|
703
|
+
const missionDir = path.join(resolvedRoot, '.sneakoscope', 'missions', id);
|
|
704
|
+
const loadedDashboard = dashboard === undefined ? await readTeamDashboard(missionDir).catch(() => null) : dashboard;
|
|
705
|
+
const loadedControl = control === undefined ? await readTeamControl(missionDir).catch(() => null) : control;
|
|
706
|
+
const lanes = close
|
|
707
|
+
? []
|
|
708
|
+
: teamCockpitLanes(plan, loadedDashboard, loadedControl, { root: resolvedRoot, missionId: id, plannedFallback });
|
|
709
|
+
const desiredAgents = new Set(lanes.map((lane) => lane.agent));
|
|
710
|
+
const paneList = await listTmuxWindowPanes(tmuxBin, target.window_id);
|
|
711
|
+
if (!paneList.ok) return { ok: false, skipped: false, session: target.session, window_id: target.window_id, reason: paneList.stderr };
|
|
712
|
+
const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
|
|
713
|
+
const byAgent = new Map();
|
|
714
|
+
for (const pane of managed) {
|
|
715
|
+
if (pane.agent && !byAgent.has(pane.agent)) byAgent.set(pane.agent, pane);
|
|
716
|
+
}
|
|
717
|
+
const opened = [];
|
|
718
|
+
const closed = [];
|
|
719
|
+
const failed = [];
|
|
720
|
+
for (const pane of managed) {
|
|
721
|
+
if (!desiredAgents.has(pane.agent)) {
|
|
722
|
+
const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
|
|
723
|
+
if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role });
|
|
724
|
+
else failed.push({ action: 'kill-pane', pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const lane of lanes) {
|
|
728
|
+
if (byAgent.has(lane.agent)) continue;
|
|
729
|
+
const split = await tmuxRun(tmuxBin, ['split-window', '-t', target.window_id, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
730
|
+
const pane_id = paneId(split.stdout);
|
|
731
|
+
if (split.code !== 0 || !pane_id) {
|
|
732
|
+
failed.push({ action: 'split-window', agent: lane.agent, role: lane.role, stderr: split.stderr || split.stdout || 'tmux split-window failed' });
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const optionResult = await setTmuxPaneUserOptions(tmuxBin, pane_id, {
|
|
736
|
+
'@sks_team_managed': '1',
|
|
737
|
+
'@sks_mission_id': id,
|
|
738
|
+
'@sks_agent_id': lane.agent,
|
|
739
|
+
'@sks_lane_role': lane.role || teamLaneStyle(lane.agent).role
|
|
740
|
+
});
|
|
741
|
+
failed.push(...optionResult.failed.map((entry) => ({ action: 'set-option', agent: lane.agent, role: lane.role, ...entry })));
|
|
742
|
+
opened.push({ pane_id, agent: lane.agent, role: lane.role, title: lane.title });
|
|
743
|
+
}
|
|
744
|
+
let relayout = null;
|
|
745
|
+
if (opened.length || closed.length) {
|
|
746
|
+
const tiled = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, 'tiled'], { timeoutMs: 5000 });
|
|
747
|
+
const even = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, '-E'], { timeoutMs: 5000 });
|
|
748
|
+
relayout = { ok: tiled.code === 0 && even.code === 0, tiled: tiled.code, even: even.code };
|
|
749
|
+
}
|
|
750
|
+
const nextPanes = [
|
|
751
|
+
...managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id)),
|
|
752
|
+
...opened
|
|
753
|
+
];
|
|
754
|
+
await writeTmuxCockpitRecord(resolvedRoot, {
|
|
755
|
+
mission_id: id,
|
|
756
|
+
session: target.session,
|
|
757
|
+
window_id: target.window_id,
|
|
758
|
+
main_pane_id: target.pane_id,
|
|
759
|
+
mode: 'current_session_dynamic_panes',
|
|
760
|
+
desired_lane_count: lanes.length,
|
|
761
|
+
panes: nextPanes,
|
|
762
|
+
opened,
|
|
763
|
+
closed,
|
|
764
|
+
failed
|
|
765
|
+
}).catch(() => null);
|
|
766
|
+
return {
|
|
767
|
+
ok: failed.length === 0,
|
|
768
|
+
mode: 'current_session_dynamic_panes',
|
|
769
|
+
session: target.session,
|
|
770
|
+
window_id: target.window_id,
|
|
771
|
+
main_pane_id: target.pane_id,
|
|
772
|
+
desired_lane_count: lanes.length,
|
|
773
|
+
opened_lane_count: opened.length,
|
|
774
|
+
closed_lane_count: closed.length,
|
|
775
|
+
managed_lane_count: nextPanes.length,
|
|
776
|
+
opened,
|
|
777
|
+
closed,
|
|
778
|
+
failed,
|
|
779
|
+
relayout,
|
|
780
|
+
attach_command: `tmux attach-session -t ${target.session}`,
|
|
781
|
+
cleanup_policy: 'managed Team panes are reconciled in the current SKS tmux session; main Codex pane is never killed'
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
561
785
|
export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false, attach = false, args = [] } = {}) {
|
|
562
786
|
const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
|
|
563
787
|
const visibleAgents = teamViewAgentIds(plan);
|
|
@@ -594,6 +818,60 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
594
818
|
attach_command: launch.attach_command
|
|
595
819
|
};
|
|
596
820
|
if (json || !launch.ready) return result;
|
|
821
|
+
const wantsSeparateSession = args.includes('--separate-session') || args.includes('--new-session') || args.includes('--legacy-team-session') || args.includes('--no-dynamic-team-tmux');
|
|
822
|
+
if (!wantsSeparateSession) {
|
|
823
|
+
const cockpit = await reconcileTmuxTeamCockpit({ root: launch.root, missionId, plan, promptFile, plannedFallback: true });
|
|
824
|
+
result.dynamic_cockpit = cockpit;
|
|
825
|
+
if (cockpit.ok) {
|
|
826
|
+
result.created = true;
|
|
827
|
+
result.opened = cockpit;
|
|
828
|
+
result.session = cockpit.session;
|
|
829
|
+
result.workspace = cockpit.session;
|
|
830
|
+
result.opened_lane_count = cockpit.managed_lane_count;
|
|
831
|
+
result.all_lanes_opened = cockpit.desired_lane_count === cockpit.managed_lane_count;
|
|
832
|
+
result.ready = true;
|
|
833
|
+
result.attach_command = cockpit.attach_command;
|
|
834
|
+
result.cleanup_policy = cockpit.cleanup_policy;
|
|
835
|
+
result.split_ui = {
|
|
836
|
+
...splitUi,
|
|
837
|
+
mode: cockpit.mode,
|
|
838
|
+
current_session: true,
|
|
839
|
+
window_id: cockpit.window_id,
|
|
840
|
+
user_attach_command: cockpit.attach_command
|
|
841
|
+
};
|
|
842
|
+
await writeTmuxTeamRecord(launch.root, {
|
|
843
|
+
mission_id: missionId,
|
|
844
|
+
session: cockpit.session,
|
|
845
|
+
attach_command: cockpit.attach_command,
|
|
846
|
+
split_ui: result.split_ui,
|
|
847
|
+
cleanup_policy: result.cleanup_policy,
|
|
848
|
+
panes: cockpit.opened || [],
|
|
849
|
+
lanes: lanes.map((entry) => ({
|
|
850
|
+
agent: entry.agent,
|
|
851
|
+
role: entry.style?.role || teamLaneStyle(entry.agent).role,
|
|
852
|
+
style: entry.style || teamLaneStyle(entry.agent),
|
|
853
|
+
title: entry.title || teamLaneTitle(entry.agent)
|
|
854
|
+
})),
|
|
855
|
+
mode: cockpit.mode,
|
|
856
|
+
window_id: cockpit.window_id
|
|
857
|
+
}).catch(() => null);
|
|
858
|
+
if (cockpit.opened?.length) {
|
|
859
|
+
const dir = path.join(launch.root, '.sneakoscope', 'missions', missionId);
|
|
860
|
+
if (await exists(dir)) {
|
|
861
|
+
for (const lane of cockpit.opened) {
|
|
862
|
+
if (!lane.agent || lane.agent === 'mission_overview') continue;
|
|
863
|
+
await appendTeamEvent(dir, {
|
|
864
|
+
agent: lane.agent,
|
|
865
|
+
phase: teamLanePhase(lane.agent),
|
|
866
|
+
type: 'tmux_lane_opened',
|
|
867
|
+
message: `tmux pane opened for ${lane.agent}; following live lane activity in the current SKS tmux session.`
|
|
868
|
+
}).catch(() => null);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
597
875
|
const panes = lanes.map((lane, index) => ({ cwd: launch.root, command: lane.command, focused: index === 0, role: lane.role, title: lane.title, vertical: index > 1 }));
|
|
598
876
|
const created = await createTmuxSession(launch, panes, { layout: 'tiled', recreate: true });
|
|
599
877
|
result.created = Boolean(created.ok);
|
|
@@ -685,8 +963,9 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
|
|
|
685
963
|
const resolvedRoot = path.resolve(root || await sksRoot());
|
|
686
964
|
const record = await readTmuxTeamRecord(resolvedRoot, missionId);
|
|
687
965
|
if (!record?.session) return { ok: false, skipped: true, reason: 'no recorded tmux Team session', mission_id: missionId };
|
|
966
|
+
const dynamicCleanup = await reconcileTmuxTeamCockpit({ root: resolvedRoot, missionId: record.mission_id || missionId, close: true }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'dynamic tmux cleanup failed' }));
|
|
688
967
|
let killed_session = false;
|
|
689
|
-
if (closeSession || closeSession === true) {
|
|
968
|
+
if ((closeSession || closeSession === true) && record.mode !== 'current_session_dynamic_panes') {
|
|
690
969
|
const tmuxBin = await findTmuxBin() || 'tmux';
|
|
691
970
|
const kill = await tmuxRun(tmuxBin, ['kill-session', '-t', record.session], { timeoutMs: 5000 });
|
|
692
971
|
killed_session = kill.code === 0;
|
|
@@ -699,9 +978,14 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
|
|
|
699
978
|
attach_command: record.attach_command,
|
|
700
979
|
close_session: Boolean(closeSession),
|
|
701
980
|
killed_session,
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
981
|
+
dynamic_cleanup: dynamicCleanup,
|
|
982
|
+
requested_close_surfaces: closeSession ? 1 : (dynamicCleanup?.closed_lane_count || 0),
|
|
983
|
+
closed_surfaces: killed_session ? 1 : (dynamicCleanup?.closed_lane_count || 0),
|
|
984
|
+
reason: dynamicCleanup?.ok
|
|
985
|
+
? 'cleanup closed managed Team panes in the current SKS tmux session.'
|
|
986
|
+
: closeSession
|
|
987
|
+
? 'tmux kill-session requested for recorded Team session.'
|
|
988
|
+
: 'cleanup marks the SKS tmux Team record complete; panes remain user-controlled.'
|
|
705
989
|
};
|
|
706
990
|
}
|
|
707
991
|
|