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 +1 -1
- package/src/cli/main.mjs +1 -1
- package/src/cli/maintenance-commands.mjs +18 -1
- package/src/core/fsx.mjs +1 -1
- package/src/core/pipeline.mjs +6 -1
- package/src/core/tmux-ui.mjs +73 -16
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.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('
|
|
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
|
|
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.
|
|
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
|
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -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 = [];
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -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
|
|
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
|
-
['
|
|
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
|
|
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 ?
|
|
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)
|
|
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
|
|
614
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)),
|