sneakoscope 0.7.74 → 0.7.75

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.74",
4
+ "version": "0.7.75",
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",
package/src/cli/main.mjs CHANGED
@@ -3305,7 +3305,7 @@ async function selftest() {
3305
3305
  await ensureDir(fakeTmuxDir);
3306
3306
  const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
3307
3307
  const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
3308
- await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('node:fs'),e=process.env,c=process.argv[2];if(e.SKS_FAKE_TMUX_LOG)a(e.SKS_FAKE_TMUX_LOG,process.argv.slice(2).join(' ')+'\\n');if(['has-session','kill-session','kill-pane','set-option','select-pane','select-layout','resize-window','set-window-option','set-hook'].includes(c))process.exit(0);if(c==='new-session'){console.log('%1');process.exit(0)}if(c==='split-window'){console.log(e.SKS_FAKE_TMUX_SPLIT_ID||'%2');process.exit(0)}if(c==='list-windows'){console.log('@1');process.exit(0)}if(c==='display-message'){console.log(e.SKS_FAKE_TMUX_DISPLAY||'sks-existing-selftest\\t@1\\t%1');process.exit(0)}if(c==='list-panes'){console.log(e.SKS_FAKE_TMUX_LIST||'');process.exit(0)}process.exit(0);\n`);
3308
+ await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('fs'),e=process.env,r=process.argv.slice(2),c=r[0];if(e.SKS_FAKE_TMUX_LOG)a(e.SKS_FAKE_TMUX_LOG,r.join(' ')+'\\n');if(c==='new-session')console.log('%1');else if(c==='split-window')console.log(e.SKS_FAKE_TMUX_SPLIT_ID||'%2');else if(c==='list-windows')console.log('@1');else if(c==='display-message')console.log(e.SKS_FAKE_TMUX_DISPLAY||'sks-existing-selftest\\t@1\\t%1');else if(c==='list-panes'){let t=r[r.indexOf('-t')+1]||'';console.log(t[0]=='%'&&r.join(' ').includes('pane_dead')?'0\\t'+t:e.SKS_FAKE_TMUX_LIST||'')}\n`);
3309
3309
  await fsp.chmod(fakeTmuxBin, 0o755);
3310
3310
  const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
3311
3311
  const previousPath = process.env.PATH;
@@ -761,15 +761,32 @@ export async function validateArtifactsCommand(args = []) {
761
761
  const root = await sksRoot();
762
762
  const missionArg = args[0] && !String(args[0]).startsWith('--') ? args[0] : 'latest';
763
763
  const id = await resolveMissionId(root, missionArg);
764
- const targetDir = id ? (await loadMission(root, id)).dir : root;
764
+ const loaded = id ? await loadMission(root, id) : null;
765
+ const targetDir = loaded ? loaded.dir : root;
765
766
  const requiredRaw = readFlagValue(args, '--required', '');
766
767
  const required = requiredRaw === 'all'
767
768
  ? Object.keys(ARTIFACT_FILES)
768
769
  : String(requiredRaw || '').split(',').map((x) => x.trim()).filter(Boolean);
769
770
  const report = await writeValidationReport(targetDir, { required });
771
+ const missionMode = String(loaded?.mission?.mode || '').toLowerCase();
772
+ if (missionMode === 'research' || await exists(path.join(targetDir, 'research-gate.json'))) {
773
+ const researchGate = await evaluateResearchGate(targetDir);
774
+ report.route_gate = {
775
+ route: 'Research',
776
+ ok: researchGate.passed === true,
777
+ gate_file: 'research-gate.evaluated.json',
778
+ reasons: researchGate.reasons || []
779
+ };
780
+ if (!report.route_gate.ok) {
781
+ report.ok = false;
782
+ report.errors = [...(report.errors || []), ...report.route_gate.reasons.map((reason) => `research-gate:${reason}`)];
783
+ }
784
+ await writeJsonAtomic(path.join(targetDir, 'artifact-validation.json'), report);
785
+ }
770
786
  if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
771
787
  console.log(`Artifact validation: ${report.ok ? 'pass' : 'fail'}`);
772
788
  console.log(`Target: ${path.relative(root, targetDir) || '.'}`);
789
+ if (report.route_gate) console.log(`Route gate: ${report.route_gate.route} ${report.route_gate.ok ? 'pass' : `fail (${report.route_gate.reasons.join(', ')})`}`);
773
790
  if (report.missing.length) console.log(`Missing: ${report.missing.join(', ')}`);
774
791
  for (const [schema, result] of Object.entries(report.results)) console.log(`${schema}: ${result.ok ? 'pass' : `fail (${result.errors.join(', ')})`}`);
775
792
  if (!report.ok) process.exitCode = 2;
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.74';
8
+ export const PACKAGE_VERSION = '0.7.75';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -12,7 +12,7 @@ import { writeMemorySweepReport } from './memory-governor.mjs';
12
12
  import { writeMistakeMemoryReport } from './mistake-memory.mjs';
13
13
  import { MISTAKE_RECALL_ARTIFACT, mistakeRecallGateStatus } from './mistake-recall.mjs';
14
14
  import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } from './skill-forge.mjs';
15
- import { writeResearchPlan } from './research.mjs';
15
+ import { evaluateResearchGate, writeResearchPlan } from './research.mjs';
16
16
  import { PPT_REQUIRED_GATE_FIELDS, writePptRouteArtifacts } from './ppt.mjs';
17
17
  import { writeQaLoopArtifacts } from './qa-loop.mjs';
18
18
  import { IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS, writeImageUxReviewRouteArtifacts } from './image-ux-review.mjs';
@@ -1456,6 +1456,11 @@ function missingRequiredGateFields(file, state, gate = {}) {
1456
1456
 
1457
1457
  async function missingRequiredGateArtifacts(root, file, state, gate = {}) {
1458
1458
  const mode = String(state?.mode || '').toUpperCase();
1459
+ if (file === 'research-gate.json' || mode === 'RESEARCH') {
1460
+ const evaluated = await evaluateResearchGate(missionDir(root, state.mission_id));
1461
+ if (evaluated.passed === true) return [];
1462
+ return (evaluated.reasons || ['research_gate_blocked']).map((reason) => `research-gate:${reason}`);
1463
+ }
1459
1464
  if (file === IMAGE_UX_REVIEW_GATE_ARTIFACT || mode === 'IMAGE_UX_REVIEW') return missingImageUxReviewArtifacts(root, state, gate);
1460
1465
  if (file !== 'team-gate.json' && mode !== 'TEAM') return [];
1461
1466
  const missing = [];
@@ -122,6 +122,8 @@ const TERMINAL_TEAM_AGENT_STATUSES = new Set([
122
122
  const LEGACY_TEAM_PANE_TITLE_RE = /^(?:overview: mission_overview|scout: analysis_scout|plan: (?:debate|consensus|planner|user)|exec: (?:executor|implementation|worker)|review: (?:reviewer|qa|validation)|safety:)/;
123
123
  const GENERIC_TEAM_AGENT_IDS = new Set(['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer']);
124
124
  const DYNAMIC_TEAM_TMUX_LAYOUT = 'main-vertical';
125
+ const TEAM_TMUX_MAIN_PANE_MIN_WIDTH = 48;
126
+ const TEAM_TMUX_MAIN_PANE_WIDTH_RATIO = 0.5;
125
127
 
126
128
  export function isTmuxShellSession(env = process.env) {
127
129
  return Boolean(String(env.TMUX || '').trim());
@@ -483,6 +485,16 @@ async function listTmuxWindowPanes(bin, windowId) {
483
485
  return { ok: true, panes: parseTmuxPaneLines(run.stdout) };
484
486
  }
485
487
 
488
+ async function tmuxPaneExists(bin, paneId) {
489
+ if (!paneId || !String(paneId).startsWith('%')) return false;
490
+ const run = await tmuxRun(bin, ['list-panes', '-t', paneId, '-F', '#{pane_dead}\t#{pane_id}'], { timeoutMs: 5000, maxOutputBytes: 4096 });
491
+ if (run.code !== 0) return false;
492
+ return String(run.stdout || '').split(/\r?\n/).some((line) => {
493
+ const [dead = '', id = ''] = line.trim().split('\t');
494
+ return id === paneId && dead !== '1';
495
+ });
496
+ }
497
+
486
498
  async function setTmuxPaneUserOptions(bin, paneId, options = {}) {
487
499
  const applied = [];
488
500
  const failed = [];
@@ -517,11 +529,52 @@ function tmuxLayoutName(value = 'tiled') {
517
529
  : 'tiled';
518
530
  }
519
531
 
532
+ function teamMainPaneWidthFromWindow(width) {
533
+ const n = Number(width);
534
+ if (!Number.isFinite(n) || n <= 0) return TEAM_TMUX_MAIN_PANE_MIN_WIDTH;
535
+ return Math.max(TEAM_TMUX_MAIN_PANE_MIN_WIDTH, Math.floor(n * TEAM_TMUX_MAIN_PANE_WIDTH_RATIO));
536
+ }
537
+
538
+ async function applyStableTeamLayout(tmuxBin, target, mainPaneId = null, opts = {}) {
539
+ const layout = tmuxLayoutName(opts.layout || DYNAMIC_TEAM_TMUX_LAYOUT);
540
+ const windowTarget = target || mainPaneId;
541
+ const applied = [];
542
+ const failed = [];
543
+ const runAndRecord = async (args) => {
544
+ const run = await tmuxRun(tmuxBin, args, { timeoutMs: 5000 });
545
+ const command = [path.basename(tmuxBin), ...args].join(' ');
546
+ if (run.code === 0) applied.push(command);
547
+ else failed.push({ command, stderr: run.stderr || run.stdout || 'tmux command failed' });
548
+ return run;
549
+ };
550
+ if (mainPaneId) await runAndRecord(['select-pane', '-t', mainPaneId]);
551
+ const width = await tmuxRun(tmuxBin, ['display-message', '-p', '-t', windowTarget, '#{window_width}'], { timeoutMs: 5000, maxOutputBytes: 1024 });
552
+ if (width.code === 0) {
553
+ const mainWidth = teamMainPaneWidthFromWindow(String(width.stdout || '').trim());
554
+ await runAndRecord(['set-window-option', '-t', windowTarget, 'main-pane-width', String(mainWidth)]);
555
+ }
556
+ await runAndRecord(['select-layout', '-t', windowTarget, layout]);
557
+ return { ok: failed.length === 0, layout_name: layout, applied, failed };
558
+ }
559
+
520
560
  async function enableTmuxDynamicResize(tmuxBin, session, opts = {}) {
521
561
  const layout = tmuxLayoutName(opts.layout || 'tiled');
522
562
  const safeSession = sanitizeTmuxSessionName(session);
523
563
  const target = await tmuxWindowTarget(tmuxBin, safeSession);
524
- const relayout = `resize-window -t ${target} -A; set-window-option -t ${target} window-size latest; select-layout -t ${target} ${layout}; select-layout -t ${target} -E; set-window-option -t ${target} window-size latest`;
564
+ const stableMainVertical = layout === DYNAMIC_TEAM_TMUX_LAYOUT && opts.stableTeamLayout;
565
+ const tmuxShell = shellEscape(tmuxBin || 'tmux');
566
+ const targetShell = shellEscape(target);
567
+ const stableRelayoutShell = [
568
+ `${tmuxShell} resize-window -t ${targetShell} -A >/dev/null 2>&1 || true`,
569
+ `${tmuxShell} set-window-option -t ${targetShell} window-size latest >/dev/null 2>&1 || true`,
570
+ `w=$(${tmuxShell} display-message -p -t ${targetShell} '#{window_width}' 2>/dev/null || printf 120)`,
571
+ `if [ "$w" -gt 0 ] 2>/dev/null; then ${tmuxShell} set-window-option -t ${targetShell} main-pane-width $((w / 2)) >/dev/null 2>&1 || true; fi`,
572
+ `${tmuxShell} select-layout -t ${targetShell} ${layout} >/dev/null 2>&1 || true`,
573
+ `${tmuxShell} set-window-option -t ${targetShell} window-size latest >/dev/null 2>&1 || true`
574
+ ].join('; ');
575
+ const relayout = stableMainVertical
576
+ ? `run-shell -b ${shellEscape(stableRelayoutShell)}`
577
+ : `resize-window -t ${target} -A; set-window-option -t ${target} window-size latest; select-layout -t ${target} ${layout}; select-layout -t ${target} -E; set-window-option -t ${target} window-size latest`;
525
578
  const commands = [
526
579
  ['set-window-option', '-t', target, 'window-size', 'latest'],
527
580
  ['set-window-option', '-t', target, 'aggressive-resize', 'on'],
@@ -529,8 +582,8 @@ async function enableTmuxDynamicResize(tmuxBin, session, opts = {}) {
529
582
  ['set-hook', '-t', safeSession, 'client-resized', relayout],
530
583
  ['resize-window', '-t', target, '-A'],
531
584
  ['set-window-option', '-t', target, 'window-size', 'latest'],
532
- ['select-layout', '-t', target, layout],
533
- ['select-layout', '-t', target, '-E'],
585
+ ...(stableMainVertical ? [] : [['select-layout', '-t', target, layout], ['select-layout', '-t', target, '-E']]),
586
+ ...(stableMainVertical ? [['display-message', '-p', '-t', target, '#{window_width}'], ['select-layout', '-t', target, layout]] : []),
534
587
  ['set-window-option', '-t', target, 'window-size', 'latest']
535
588
  ];
536
589
  const applied = [];
@@ -599,19 +652,21 @@ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
599
652
  const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-x', dimensions.width, '-y', dimensions.height, '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
600
653
  if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
601
654
  const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
602
- let rightStackTarget = created[0].pane_id || session;
655
+ let rightStackRootPaneId = null;
603
656
  for (const pane of normalizedPanes.slice(1)) {
604
657
  const direction = rightSidePanes ? (created.length === 1 ? '-h' : '-v') : (pane.vertical ? '-v' : '-h');
605
- const splitTarget = rightSidePanes ? rightStackTarget : session;
658
+ const splitTarget = rightSidePanes ? (rightStackRootPaneId || created[0].pane_id || session) : session;
606
659
  const split = await tmuxRun(tmuxBin, ['split-window', '-t', splitTarget, direction, '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
607
660
  if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
608
661
  const newPaneId = paneId(split.stdout);
662
+ if (newPaneId && !(await tmuxPaneExists(tmuxBin, newPaneId))) return { ok: false, session, panes: created, stderr: `tmux split-window returned pane ${newPaneId}, but the pane was not present after creation` };
609
663
  created.push({ pane_id: newPaneId, role: pane.role || 'lane', title: pane.title || null });
610
- if (rightSidePanes && newPaneId) rightStackTarget = newPaneId;
611
- await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
664
+ if (rightSidePanes && !rightStackRootPaneId && newPaneId) rightStackRootPaneId = newPaneId;
665
+ if (!rightSidePanes) await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
612
666
  }
613
- const dynamic_resize = await enableTmuxDynamicResize(tmuxBin, session, { layout });
614
- return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}`, layout, initial_size: dimensions, dynamic_resize };
667
+ const stable_layout = rightSidePanes ? await applyStableTeamLayout(tmuxBin, session, created[0].pane_id, { layout }) : null;
668
+ const dynamic_resize = await enableTmuxDynamicResize(tmuxBin, session, { layout, stableTeamLayout: rightSidePanes });
669
+ return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}`, layout, initial_size: dimensions, stable_layout, dynamic_resize };
615
670
  }
616
671
 
617
672
  export async function launchTmuxUi(args = [], opts = {}) {
@@ -770,18 +825,23 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
770
825
  }
771
826
  }
772
827
  const remainingManaged = managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id));
773
- let rightStackTarget = remainingManaged.at(-1)?.pane_id || mainPaneId || target.window_id;
828
+ let rightStackRootPaneId = remainingManaged[0]?.pane_id || null;
774
829
  for (const lane of lanes) {
775
830
  if (byAgent.has(lane.agent)) continue;
776
831
  const firstRightPane = remainingManaged.length === 0 && opened.length === 0;
777
832
  const direction = firstRightPane ? '-h' : '-v';
778
- const split = await tmuxRun(tmuxBin, ['split-window', direction, '-t', rightStackTarget, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
833
+ const splitTarget = firstRightPane ? (mainPaneId || target.window_id) : (rightStackRootPaneId || mainPaneId || target.window_id);
834
+ const split = await tmuxRun(tmuxBin, ['split-window', direction, '-t', splitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
779
835
  const pane_id = paneId(split.stdout);
780
836
  if (split.code !== 0 || !pane_id) {
781
837
  failed.push({ action: 'split-window', agent: lane.agent, role: lane.role, stderr: split.stderr || split.stdout || 'tmux split-window failed' });
782
838
  continue;
783
839
  }
784
- rightStackTarget = pane_id;
840
+ if (!(await tmuxPaneExists(tmuxBin, pane_id))) {
841
+ failed.push({ action: 'verify-pane', pane_id, agent: lane.agent, role: lane.role, stderr: 'tmux split-window returned a pane id, but the pane was not present after creation' });
842
+ continue;
843
+ }
844
+ if (!rightStackRootPaneId) rightStackRootPaneId = pane_id;
785
845
  const optionResult = await setTmuxPaneUserOptions(tmuxBin, pane_id, {
786
846
  '@sks_team_managed': '1',
787
847
  '@sks_mission_id': id,
@@ -793,10 +853,7 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
793
853
  }
794
854
  let relayout = null;
795
855
  if (opened.length || closed.length) {
796
- const selectedMain = mainPaneId ? await tmuxRun(tmuxBin, ['select-pane', '-t', mainPaneId], { timeoutMs: 5000 }) : { code: 0 };
797
- const tiled = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 });
798
- const even = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, '-E'], { timeoutMs: 5000 });
799
- relayout = { ok: selectedMain.code === 0 && tiled.code === 0 && even.code === 0, selected_main: selectedMain.code, layout: tiled.code, even: even.code, layout_name: DYNAMIC_TEAM_TMUX_LAYOUT };
856
+ relayout = await applyStableTeamLayout(tmuxBin, target.window_id, mainPaneId, { layout: DYNAMIC_TEAM_TMUX_LAYOUT });
800
857
  }
801
858
  const nextPanes = [
802
859
  ...managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id)),