sneakoscope 0.7.78 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { readJson, readText, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, packageRoot, dirSize, formatBytes, PACKAGE_VERSION, sksRoot, readStdin } from '../core/fsx.mjs';
3
+ import { createHash } from 'node:crypto';
4
+ import { readJson, readText, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, packageRoot, dirSize, formatBytes, PACKAGE_VERSION, sksRoot, readStdin, runProcess } from '../core/fsx.mjs';
4
5
  import { initProject } from '../core/init.mjs';
5
6
  import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
6
7
  import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
@@ -42,7 +43,7 @@ const flag = (args, name) => args.includes(name);
42
43
  const promptOf = (args) => args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
43
44
  const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
44
45
  const REPOSITORY_URL = 'https://github.com/mandarange/Sneakoscope-Codex.git';
45
- const RESEARCH_DEFAULT_MAX_CYCLES = 3;
46
+ const RESEARCH_DEFAULT_MAX_CYCLES = 12;
46
47
  const RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES = 120;
47
48
  const RESEARCH_MIN_CYCLE_TIMEOUT_MINUTES = 15;
48
49
  const RESEARCH_MAX_CYCLE_TIMEOUT_MINUTES = 240;
@@ -495,6 +496,7 @@ async function researchPrepare(args) {
495
496
  console.log(`Source skill: ${RESEARCH_SOURCE_SKILL_ARTIFACT}`);
496
497
  console.log('Ledgers: source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json');
497
498
  console.log(`Run: sks research run ${id} --max-cycles ${RESEARCH_DEFAULT_MAX_CYCLES} --cycle-timeout-minutes ${RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES}`);
499
+ console.log('Loop: Research runs until the gate records unanimous scout consensus, or pauses at the explicit safety cap.');
498
500
  }
499
501
 
500
502
  async function researchRun(args) {
@@ -546,13 +548,36 @@ async function researchRun(args) {
546
548
  return;
547
549
  }
548
550
  let last = '';
551
+ const researchCodexArgs = ['-c', 'service_tier="fast"', '-c', 'model_reasoning_effort="xhigh"'];
552
+ const sourceMutationBaseline = await researchCodeMutationSnapshot(root, id);
549
553
  for (let cycle = 1; cycle <= maxCycles; cycle++) {
550
554
  const cycleDir = path.join(dir, 'research', `cycle-${cycle}`);
551
555
  const outputFile = path.join(cycleDir, 'final.md');
552
- await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle, timeoutMinutes: cycleTimeoutMinutes });
556
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle, timeoutMinutes: cycleTimeoutMinutes, profile: 'sks-research', enforced_reasoning_effort: 'xhigh' });
553
557
  const prompt = buildResearchPrompt({ id, mission, plan, cycle, previous: last });
554
- const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-research', logDir: cycleDir, timeoutMs: cycleTimeoutMs });
558
+ const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-research', extraArgs: researchCodexArgs, logDir: cycleDir, timeoutMs: cycleTimeoutMs });
555
559
  await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
560
+ const mutation = await researchCodeMutationDelta(root, sourceMutationBaseline, id);
561
+ if (mutation.blocked) {
562
+ const blocker = {
563
+ schema_version: 1,
564
+ mission_id: id,
565
+ ts: nowIso(),
566
+ phase: 'RESEARCH_BLOCKED_CODE_MUTATION',
567
+ reason: 'Research mode must not modify repository source files. Only route-local mission artifacts are allowed.',
568
+ changed_paths: mutation.changed_paths,
569
+ allowed_prefixes: mutation.allowed_prefixes,
570
+ required_action: 'Review the changed paths, keep or revert them manually as appropriate, then rerun Research after the worktree is clean for source files.',
571
+ implementation_allowed: false
572
+ };
573
+ await writeJsonAtomic(path.join(dir, 'research-code-mutation-blocker.json'), blocker);
574
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: blocker.ts, type: 'research.blocked.code_mutation', changed_paths: mutation.changed_paths });
575
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_BLOCKED_CODE_MUTATION', questions_allowed: true, implementation_allowed: false, blocker: 'research-code-mutation-blocker.json' });
576
+ console.error('Research cannot continue: source-code mutation detected outside the route-local mission artifacts.');
577
+ console.error(JSON.stringify(mutation.changed_paths, null, 2));
578
+ process.exitCode = 2;
579
+ return;
580
+ }
556
581
  last = await safeReadText(outputFile, result.stdout || result.stderr || '');
557
582
  if (containsUserQuestion(last)) {
558
583
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.guard.question_blocked', cycle });
@@ -570,7 +595,7 @@ async function researchRun(args) {
570
595
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.continue', cycle, reasons: gate.reasons });
571
596
  }
572
597
  await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_PAUSED_MAX_CYCLES', questions_allowed: true, implementation_allowed: false });
573
- console.log(`Research paused after max cycles: ${id}`);
598
+ console.log(`Research paused after max cycles without unanimous scout consensus: ${id}`);
574
599
  }
575
600
 
576
601
  async function researchStatus(args) {
@@ -606,6 +631,8 @@ async function researchStatus(args) {
606
631
  eureka_moments: scoutRows.length ? scoutRows.filter((scout) => scout.eureka?.exclamation === 'Eureka!' && String(scout.eureka?.idea || '').trim()).length : null,
607
632
  scout_findings: scoutRows.length ? scoutRows.reduce((sum, scout) => sum + (Array.isArray(scout.findings) ? scout.findings.length : 0), 0) : null,
608
633
  debate_exchanges: debateLedger?.exchanges?.length ?? null,
634
+ consensus_iterations: gate?.metrics?.consensus_iterations ?? gate?.consensus_iterations ?? debateLedger?.consensus_iterations ?? null,
635
+ unanimous_consensus: gate?.metrics?.unanimous_consensus ?? gate?.unanimous_consensus ?? debateLedger?.unanimous_consensus ?? false,
609
636
  research_source_skill_present: Boolean(sourceSkillText.trim()),
610
637
  genius_opinion_summary_present: Boolean(geniusSummaryText.trim()),
611
638
  paper_present: Boolean(paperText.trim()),
@@ -662,6 +689,71 @@ async function safeReadText(file, fallback = '') {
662
689
  try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
663
690
  }
664
691
 
692
+ async function researchCodeMutationSnapshot(root, missionId = null) {
693
+ const tracked = await runProcess('git', ['ls-files'], { cwd: root, timeoutMs: 15000, maxOutputBytes: 2 * 1024 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
694
+ const status = await runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { cwd: root, timeoutMs: 15000, maxOutputBytes: 2 * 1024 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
695
+ if (tracked.code !== 0 || status.code !== 0) return { ok: false, reason: 'git_unavailable', hashes: {}, status_rows: [], error: tracked.stderr || status.stderr };
696
+ const allowedPrefixes = researchAllowedMutationPrefixes(missionId);
697
+ const hashes = {};
698
+ for (const rel of tracked.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)) {
699
+ if (researchMutationAllowedPath(rel, allowedPrefixes)) continue;
700
+ const file = path.join(root, rel);
701
+ try {
702
+ const bytes = await fsp.readFile(file);
703
+ hashes[rel] = createHash('sha256').update(bytes).digest('hex');
704
+ } catch {
705
+ hashes[rel] = null;
706
+ }
707
+ }
708
+ return {
709
+ ok: true,
710
+ hashes,
711
+ status_rows: status.stdout.split(/\r?\n/).filter(Boolean),
712
+ allowed_prefixes: allowedPrefixes
713
+ };
714
+ }
715
+
716
+ async function researchCodeMutationDelta(root, baseline, missionId) {
717
+ if (!baseline?.ok) return { blocked: false, changed_paths: [], reason: baseline?.reason || 'baseline_unavailable' };
718
+ const current = await researchCodeMutationSnapshot(root, missionId);
719
+ if (!current.ok) return { blocked: false, changed_paths: [], reason: current.reason || 'current_snapshot_unavailable' };
720
+ const changed = new Set();
721
+ for (const [rel, hash] of Object.entries(current.hashes)) {
722
+ if (baseline.hashes[rel] !== hash) changed.add(rel);
723
+ }
724
+ for (const rel of Object.keys(baseline.hashes)) {
725
+ if (!(rel in current.hashes)) changed.add(rel);
726
+ }
727
+ const baselineRows = new Set(baseline.status_rows || []);
728
+ for (const row of current.status_rows || []) {
729
+ if (baselineRows.has(row)) continue;
730
+ const rel = porcelainStatusPath(row);
731
+ if (rel && !researchMutationAllowedPath(rel, current.allowed_prefixes)) changed.add(rel);
732
+ }
733
+ const changedPaths = [...changed].sort();
734
+ return {
735
+ blocked: changedPaths.length > 0,
736
+ changed_paths: changedPaths,
737
+ allowed_prefixes: current.allowed_prefixes
738
+ };
739
+ }
740
+
741
+ function researchAllowedMutationPrefixes(missionId = null) {
742
+ return missionId ? [`.sneakoscope/missions/${missionId}/`] : ['.sneakoscope/missions/'];
743
+ }
744
+
745
+ function researchMutationAllowedPath(rel = '', prefixes = []) {
746
+ const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
747
+ return prefixes.some((prefix) => normalized.startsWith(prefix));
748
+ }
749
+
750
+ function porcelainStatusPath(row = '') {
751
+ const payload = String(row || '').slice(3).trim();
752
+ if (!payload) return '';
753
+ const renamed = payload.split(' -> ').pop();
754
+ return String(renamed || '').replace(/^"|"$/g, '');
755
+ }
756
+
665
757
  function readBoundedIntegerFlag(args, name, fallback, min, max) {
666
758
  const i = args.indexOf(name);
667
759
  const raw = i >= 0 && args[i + 1] ? Number(args[i + 1]) : Number(fallback);
@@ -1909,7 +2001,7 @@ export function buildTeamPlan(id, prompt, opts = {}) {
1909
2001
  reasoning: teamReasoningPolicy(prompt, roster),
1910
2002
  codex_config_required: {
1911
2003
  service_tier: 'fast',
1912
- features: { multi_agent: true, hooks: true, fast_mode: true, fast_mode_ui: true, codex_git_commit: true, computer_use: true, apps: true, plugins: true },
2004
+ features: { multi_agent: true, hooks: true, remote_control: true, fast_mode: true, fast_mode_ui: true, codex_git_commit: true, computer_use: true, browser_use: true, browser_use_external: true, image_generation: true, in_app_browser: true, guardian_approval: true, tool_suggest: true, apps: true, plugins: true },
1913
2005
  agents: { max_threads: 6, max_depth: 1 },
1914
2006
  custom_agents_dir: '.codex/agents'
1915
2007
  },
@@ -0,0 +1,157 @@
1
+ import path from 'node:path';
2
+ import { readJson, sksRoot } from '../core/fsx.mjs';
3
+ import { findLatestMission, missionDir, stateFile } from '../core/mission.mjs';
4
+ import {
5
+ EVIDENCE_ENVELOPE_ARTIFACT,
6
+ MISSION_STATUS_LEDGER_ARTIFACT,
7
+ RECALLPULSE_DECISION_ARTIFACT,
8
+ RECALLPULSE_EVAL_ARTIFACT,
9
+ RECALLPULSE_GOVERNANCE_ARTIFACT,
10
+ RECALLPULSE_POLICY,
11
+ RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT,
12
+ RECALLPULSE_TASKS_FILE,
13
+ ROUTE_PROOF_CAPSULE_ARTIFACT,
14
+ buildRecallPulseGovernanceReport,
15
+ buildRecallPulseTaskGoalLedger,
16
+ completeRecallPulseTaskGoal,
17
+ evaluateRecallPulseFixtures,
18
+ readMissionStatusLedger,
19
+ writeRecallPulseArtifacts
20
+ } from '../core/recallpulse.mjs';
21
+
22
+ function flag(args, name) {
23
+ return args.includes(name);
24
+ }
25
+
26
+ export async function recallPulseCommand(sub = 'status', args = []) {
27
+ const root = await sksRoot();
28
+ const action = sub || 'status';
29
+ if (action === 'help' || action === '--help' || action === '-h') return help();
30
+ const missionArg = args.find((arg) => !String(arg).startsWith('--')) || 'latest';
31
+ const id = await resolveMissionId(root, missionArg);
32
+ if (!id) throw new Error('Usage: sks recallpulse run|status|eval|governance|checklist <mission-id|latest> [--json]');
33
+ const state = await readJson(stateFile(root), {});
34
+ if (action === 'run') {
35
+ const result = await writeRecallPulseArtifacts(root, { missionId: id, state, stageId: readOption(args, '--stage', null) });
36
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
37
+ console.log('SKS RecallPulse report-only run\n');
38
+ printSummary(root, id, result.decision);
39
+ return;
40
+ }
41
+ if (action === 'status') {
42
+ const status = await recallPulseStatus(root, id);
43
+ if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
44
+ console.log('SKS RecallPulse status\n');
45
+ printSummary(root, id, status.decision);
46
+ console.log(`Status ledger: ${path.relative(root, path.join(missionDir(root, id), MISSION_STATUS_LEDGER_ARTIFACT))}${status.status_ledger ? '' : ' (missing)'}`);
47
+ console.log(`Proof capsule: ${path.relative(root, path.join(missionDir(root, id), ROUTE_PROOF_CAPSULE_ARTIFACT))}${status.route_proof_capsule ? '' : ' (missing)'}`);
48
+ console.log(`Evidence: ${path.relative(root, path.join(missionDir(root, id), EVIDENCE_ENVELOPE_ARTIFACT))}${status.evidence_envelope ? '' : ' (missing)'}`);
49
+ return;
50
+ }
51
+ if (action === 'eval') {
52
+ const report = await evaluateRecallPulseFixtures(root, { missionId: id, write: true });
53
+ if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
54
+ console.log('SKS RecallPulse fixture eval\n');
55
+ console.log(`Mission: ${id}`);
56
+ console.log(`Passed: ${report.passed ? 'yes' : 'no'}`);
57
+ console.log(`File: ${path.relative(root, path.join(missionDir(root, id), RECALLPULSE_EVAL_ARTIFACT))}`);
58
+ console.log(`Caveat: ${report.caveat}`);
59
+ return;
60
+ }
61
+ if (action === 'governance') {
62
+ const report = await buildRecallPulseGovernanceReport(root, { missionId: id, writeDecisions: !flag(args, '--no-samples') });
63
+ if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
64
+ console.log('SKS RecallPulse governance report\n');
65
+ console.log(`Mission: ${id}`);
66
+ console.log(`Routes inventoried: ${report.route_gate_inventory.length}`);
67
+ console.log(`Recorded samples: ${report.rollout.requested_samples.filter((sample) => sample.report_only_decision_recorded).length}/${report.rollout.requested_samples.length}`);
68
+ console.log(`Enforcement: ${report.shadow_eval.enforcement_decision}`);
69
+ console.log(`File: ${path.relative(root, path.join(missionDir(root, id), RECALLPULSE_GOVERNANCE_ARTIFACT))}`);
70
+ return;
71
+ }
72
+ if (action === 'checklist') {
73
+ const taskId = readOption(args, '--task', null) || readOption(args, '--id', null);
74
+ const apply = flag(args, '--apply');
75
+ if (taskId && apply) {
76
+ const result = await completeRecallPulseTaskGoal(root, id, taskId, {
77
+ allowOutOfOrder: flag(args, '--allow-out-of-order'),
78
+ evidence: readListOption(args, '--evidence'),
79
+ verification: readListOption(args, '--verification'),
80
+ notes: readOption(args, '--notes', '')
81
+ });
82
+ if (flag(args, '--json')) return console.log(JSON.stringify({ ok: true, applied: true, ...result }, null, 2));
83
+ console.log(`Checked ${result.task.task_id} as a child $Goal checkpoint.`);
84
+ console.log(`Ledger: ${path.relative(root, path.join(missionDir(root, id), RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT))}`);
85
+ return;
86
+ }
87
+ const ledger = await buildRecallPulseTaskGoalLedger(root, id);
88
+ const result = { ok: true, applied: false, file: path.join(root, RECALLPULSE_TASKS_FILE), ledger_file: path.join(root, '.sneakoscope', 'missions', id, RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT), next_task: ledger.next_task, counts: ledger.counts };
89
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
90
+ console.log(`RecallPulse sequential task-goal ledger: ${ledger.counts.checked}/${ledger.counts.total} checked.`);
91
+ console.log(`Next: ${ledger.next_task?.id || 'none'} ${ledger.next_task?.title || ''}`.trim());
92
+ console.log(`Run only after evidence: sks recallpulse checklist ${id} --task ${ledger.next_task?.id || 'T001'} --apply --evidence <path>`);
93
+ return;
94
+ }
95
+ throw new Error(`Unknown recallpulse command: ${action}`);
96
+ }
97
+
98
+ async function recallPulseStatus(root, id) {
99
+ const dir = missionDir(root, id);
100
+ return {
101
+ mission_id: id,
102
+ policy: RECALLPULSE_POLICY,
103
+ decision: await readJson(path.join(dir, RECALLPULSE_DECISION_ARTIFACT), null),
104
+ status_ledger: await readMissionStatusLedger(root, id),
105
+ route_proof_capsule: await readJson(path.join(dir, ROUTE_PROOF_CAPSULE_ARTIFACT), null),
106
+ evidence_envelope: await readJson(path.join(dir, EVIDENCE_ENVELOPE_ARTIFACT), null),
107
+ eval_report: await readJson(path.join(dir, RECALLPULSE_EVAL_ARTIFACT), null)
108
+ };
109
+ }
110
+
111
+ function printSummary(root, id, decision) {
112
+ console.log(`Mission: ${id}`);
113
+ if (!decision) {
114
+ console.log(`Decision: missing (${path.relative(root, path.join(missionDir(root, id), RECALLPULSE_DECISION_ARTIFACT))})`);
115
+ console.log(`Run: sks recallpulse run ${id}`);
116
+ return;
117
+ }
118
+ console.log(`Decision: ${decision.recommended_action}`);
119
+ console.log(`Stage: ${decision.stage_id}`);
120
+ console.log(`L1: ${(decision.l1?.selected || []).map((item) => item.id).join(', ') || 'none'}`);
121
+ console.log(`L3: ${(decision.l3?.hydration_requests || []).length} hydration request(s)`);
122
+ console.log(`Status: ${decision.user_visible_status_projection?.message || 'report-only decision written'}`);
123
+ }
124
+
125
+ async function resolveMissionId(root, value = 'latest') {
126
+ if (!value || value === 'latest') return findLatestMission(root);
127
+ return value;
128
+ }
129
+
130
+ function readOption(args = [], name, fallback = null) {
131
+ const index = args.indexOf(name);
132
+ if (index < 0 || index + 1 >= args.length) return fallback;
133
+ return args[index + 1];
134
+ }
135
+
136
+ function readListOption(args = [], name) {
137
+ const values = [];
138
+ for (let i = 0; i < args.length; i += 1) {
139
+ if (args[i] === name && args[i + 1]) values.push(args[i + 1]);
140
+ }
141
+ return values;
142
+ }
143
+
144
+ function help() {
145
+ console.log(`SKS RecallPulse
146
+
147
+ Report-only active recall, durable status, RouteProofCapsule, and EvidenceEnvelope utilities.
148
+
149
+ Commands:
150
+ sks recallpulse run <mission-id|latest> [--json] [--stage before_final]
151
+ sks recallpulse status <mission-id|latest> [--json]
152
+ sks recallpulse eval <mission-id|latest> [--json]
153
+ sks recallpulse governance <mission-id|latest> [--json] [--no-samples]
154
+ sks recallpulse checklist <mission-id|latest> [--json]
155
+ sks recallpulse checklist <mission-id|latest> --task T001 --apply --evidence <path>
156
+ `);
157
+ }
@@ -7,7 +7,30 @@ 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
+ const REQUIRED_CODEX_APP_FEATURE_FLAGS = [
11
+ 'codex_git_commit',
12
+ 'hooks',
13
+ 'remote_control',
14
+ 'fast_mode',
15
+ 'computer_use',
16
+ 'browser_use',
17
+ 'browser_use_external',
18
+ 'image_generation',
19
+ 'in_app_browser',
20
+ 'guardian_approval',
21
+ 'tool_suggest',
22
+ 'apps',
23
+ 'plugins'
24
+ ];
25
+ const DEFAULT_CODEX_APP_PLUGINS = [
26
+ { name: 'browser', marketplace: 'openai-bundled' },
27
+ { name: 'chrome', marketplace: 'openai-bundled' },
28
+ { name: 'computer-use', marketplace: 'openai-bundled' },
29
+ { name: 'latex', marketplace: 'openai-bundled' },
30
+ { name: 'documents', marketplace: 'openai-primary-runtime' },
31
+ { name: 'presentations', marketplace: 'openai-primary-runtime' },
32
+ { name: 'spreadsheets', marketplace: 'openai-primary-runtime' }
33
+ ];
11
34
 
12
35
  export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
13
36
  const candidates = [];
@@ -104,15 +127,20 @@ export async function codexAppIntegrationStatus(opts = {}) {
104
127
  const featureText = `${featureList.stdout}\n${featureList.stderr}`;
105
128
  const browserUsePath = await findPluginCache('browser-use', opts);
106
129
  const computerUsePath = await findPluginCache('computer-use', opts);
130
+ const defaultPlugins = await codexDefaultPluginStatus(opts);
131
+ const fastModeConfig = await codexFastModeConfigStatus(opts);
107
132
  const computerUseMcpListed = /computer[-_ ]?use/i.test(mcpText);
108
133
  const browserUseMcpListed = /browser[-_ ]?use/i.test(mcpText);
109
134
  const imageGenerationReady = codexFeatureEnabled(featureText, 'image_generation');
135
+ const inAppBrowserReady = codexFeatureEnabled(featureText, 'in_app_browser');
136
+ const browserUseFeatureReady = codexFeatureEnabled(featureText, 'browser_use');
110
137
  const requiredFeatureFlags = Object.fromEntries(REQUIRED_CODEX_APP_FEATURE_FLAGS.map((name) => [name, codexFeatureEnabled(featureText, name)]));
111
138
  const requiredFeatureFlagsOk = Object.values(requiredFeatureFlags).every(Boolean);
112
139
  const computerUseReady = computerUseMcpListed || Boolean(computerUsePath);
113
140
  const browserUseReady = browserUseMcpListed || Boolean(browserUsePath);
141
+ const browserToolReady = inAppBrowserReady || browserUseFeatureReady || browserUseReady;
114
142
  const appInstalled = Boolean(appPath);
115
- const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && imageGenerationReady && computerUseReady && browserUseReady;
143
+ const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && defaultPlugins.ok && fastModeConfig.ok && imageGenerationReady && computerUseReady && browserToolReady;
116
144
  return {
117
145
  ok: ready,
118
146
  app: {
@@ -142,16 +170,30 @@ export async function codexAppIntegrationStatus(opts = {}) {
142
170
  ...requiredFeatureFlags,
143
171
  required_flags: requiredFeatureFlags,
144
172
  required_flags_ok: requiredFeatureFlagsOk,
173
+ fast_mode_config: fastModeConfig,
145
174
  image_generation: imageGenerationReady,
146
175
  image_generation_source: imageGenerationReady ? 'codex_features_list' : 'missing',
176
+ in_app_browser: inAppBrowserReady,
177
+ browser_use: browserUseFeatureReady,
178
+ browser_tool_ready: browserToolReady,
179
+ browser_tool_source: inAppBrowserReady
180
+ ? 'codex_features_list:in_app_browser'
181
+ : browserUseFeatureReady
182
+ ? 'codex_features_list:browser_use'
183
+ : browserUseMcpListed
184
+ ? 'mcp_list:browser_use'
185
+ : browserUsePath
186
+ ? 'plugin_cache:browser-use'
187
+ : 'missing',
147
188
  stdout: featureList.stdout,
148
189
  stderr: featureList.stderr
149
190
  },
150
191
  plugins: {
151
192
  computer_use_cache: computerUsePath,
152
- browser_use_cache: browserUsePath
193
+ browser_use_cache: browserUsePath,
194
+ default_plugins: defaultPlugins
153
195
  },
154
- guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
196
+ guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, fastModeConfig, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
155
197
  };
156
198
  }
157
199
 
@@ -206,7 +248,7 @@ export function formatCodexRemoteControlStatus(status) {
206
248
  return lines.filter(Boolean).join('\n');
207
249
  }
208
250
 
209
- export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
251
+ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, fastModeConfig = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
210
252
  const lines = [];
211
253
  if (!appInstalled) {
212
254
  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.');
@@ -229,13 +271,21 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
229
271
  }
230
272
  if (featureList?.checked && featureList.ok && !requiredFeatureFlagsOk) {
231
273
  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"');
274
+ lines.push(`Codex App feature flag(s) disabled or missing: ${missing.join(', ')}. Commit message generation, mobile/remote-control, and app-only tool paths can fail even when CLI chat works.`);
275
+ lines.push('Verify with: codex features list | rg "codex_git_commit|hooks|remote_control|fast_mode|computer_use|browser_use|browser_use_external|image_generation|in_app_browser|guardian_approval|tool_suggest|apps|plugins"');
234
276
  }
235
- if (appInstalled && (!computerUseReady || !browserUseReady)) {
277
+ if (defaultPlugins?.missing_enabled?.length) {
278
+ lines.push(`Codex default plugin(s) installed but not enabled: ${defaultPlugins.missing_enabled.join(', ')}. Composer/tool UI can hide built-in surfaces even while feature flags look green.`);
279
+ lines.push('Run: sks doctor --fix');
280
+ }
281
+ if (fastModeConfig?.blockers?.length) {
282
+ lines.push(`Codex App speed selector can be hidden or locked by config: ${fastModeConfig.blockers.join(', ')}.`);
283
+ lines.push('Run: sks doctor --fix');
284
+ }
285
+ if (appInstalled && (!computerUseReady || !browserToolReady)) {
236
286
  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.');
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.');
238
- lines.push('Verify with: codex mcp list');
287
+ lines.push('Required for SKS QA-LOOP UI/browser evidence: Codex Computer Use only. Browser tools can support browsing context, but they do not satisfy UI-level E2E verification.');
288
+ lines.push('Verify with: codex features list; codex mcp list');
239
289
  }
240
290
  if (imageGenerationReady) {
241
291
  lines.push('Image generation is enabled; required raster assets and generated image-review evidence must invoke $imagegen/gpt-image-2 and record real output.');
@@ -245,6 +295,10 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
245
295
  if (computerUseReady && !computerUseMcpListed) {
246
296
  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`.');
247
297
  }
298
+ if (browserToolReady) {
299
+ const source = inAppBrowserReady ? 'in-app browser feature' : browserUseFeatureReady ? 'browser_use feature' : 'Browser Use plugin';
300
+ lines.push(`Browser tooling is visible via ${source}; prefer the first-party in-app browser for local web apps, and keep Codex Computer Use as the only accepted UI verification evidence source.`);
301
+ }
248
302
  if (browserUseReady && !browserUseMcpListed) {
249
303
  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.');
250
304
  }
@@ -260,8 +314,10 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
260
314
  `Codex CLI: ${status.codex_cli.ok ? 'ok' : 'missing'}${status.codex_cli.version ? ` ${status.codex_cli.version}` : ''}`,
261
315
  `Remote Ctrl: ${status.remote_control?.ok ? 'ok' : 'missing'}${status.remote_control?.codex_cli?.version_number ? ` min ${status.remote_control.min_version}` : ''}`,
262
316
  `App Flags: ${status.features?.required_flags_ok ? 'ok' : `missing ${missingRequiredFeatureFlags(status.features?.required_flags).join(', ') || 'required flags'}`}`,
317
+ `Fast UI: ${status.features?.fast_mode_config?.ok ? 'ok' : `locked ${(status.features?.fast_mode_config?.blockers || []).join(', ') || 'config'}`}`,
318
+ `Default Plugins:${status.plugins?.default_plugins?.ok ? ' ok' : ` missing ${(status.plugins?.default_plugins?.missing_enabled || []).join(', ') || 'enabled plugin config'}`}`,
263
319
  `Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
264
- `Browser Use: ${status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
320
+ `Browser: ${status.features?.browser_tool_ready ? `ok (${status.features.browser_tool_source})` : status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
265
321
  `Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
266
322
  `Ready: ${status.ok ? 'yes' : 'no'}`,
267
323
  '',
@@ -296,6 +352,120 @@ function missingRequiredFeatureFlags(flags = {}) {
296
352
  return REQUIRED_CODEX_APP_FEATURE_FLAGS.filter((name) => flags?.[name] !== true);
297
353
  }
298
354
 
355
+ async function codexDefaultPluginStatus(opts = {}) {
356
+ const home = opts.home || os.homedir();
357
+ const cwd = opts.cwd || process.cwd();
358
+ const globalConfigPath = path.join(home || '', '.codex', 'config.toml');
359
+ const projectConfigPath = path.join(cwd || '', '.codex', 'config.toml');
360
+ const globalConfig = await readTextIfExists(globalConfigPath);
361
+ const projectConfig = path.resolve(projectConfigPath) === path.resolve(globalConfigPath)
362
+ ? ''
363
+ : await readTextIfExists(projectConfigPath);
364
+ const configText = `${globalConfig}\n${projectConfig}`;
365
+ const entries = [];
366
+ for (const plugin of DEFAULT_CODEX_APP_PLUGINS) {
367
+ const source = await findDefaultPluginSource(plugin, { home, configText });
368
+ const enabled = codexPluginEnabled(configText, plugin);
369
+ entries.push({
370
+ id: `${plugin.name}@${plugin.marketplace}`,
371
+ name: plugin.name,
372
+ marketplace: plugin.marketplace,
373
+ installed: Boolean(source),
374
+ source,
375
+ enabled
376
+ });
377
+ }
378
+ const installed = entries.filter((entry) => entry.installed);
379
+ const missingEnabled = installed.filter((entry) => !entry.enabled).map((entry) => entry.id);
380
+ return {
381
+ ok: missingEnabled.length === 0,
382
+ checked: true,
383
+ entries,
384
+ missing_enabled: missingEnabled
385
+ };
386
+ }
387
+
388
+ async function codexFastModeConfigStatus(opts = {}) {
389
+ const home = opts.home || os.homedir();
390
+ const cwd = opts.cwd || process.cwd();
391
+ const globalConfigPath = path.join(home || '', '.codex', 'config.toml');
392
+ const projectConfigPath = path.join(cwd || '', '.codex', 'config.toml');
393
+ const configs = [
394
+ { scope: 'global', path: globalConfigPath, text: await readTextIfExists(globalConfigPath) }
395
+ ];
396
+ if (path.resolve(projectConfigPath) !== path.resolve(globalConfigPath)) {
397
+ configs.push({ scope: 'project', path: projectConfigPath, text: await readTextIfExists(projectConfigPath) });
398
+ }
399
+ const blockers = [];
400
+ for (const config of configs) {
401
+ if (!config.text) continue;
402
+ const topLevel = topLevelToml(config.text);
403
+ if (/(^|\n)\s*model_reasoning_effort\s*=/.test(topLevel)) blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
404
+ if (/(^|\n)\s*fast_default_opt_out\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(tomlTable(config.text, 'notice'))) blockers.push(`${config.scope}:fast_default_opt_out`);
405
+ }
406
+ const merged = configs.map((config) => config.text).join('\n');
407
+ const fastMode = tomlTable(merged, 'user.fast_mode');
408
+ if (!/(^|\n)\s*visible\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(fastMode)) blockers.push('user.fast_mode.visible_missing');
409
+ if (!/(^|\n)\s*enabled\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(fastMode)) blockers.push('user.fast_mode.enabled_missing');
410
+ if (!/(^|\n)\s*default_profile\s*=\s*"sks-fast-high"\s*(?:#.*)?(?=\n|$)/.test(fastMode)) blockers.push('user.fast_mode.default_profile_missing');
411
+ return {
412
+ ok: blockers.length === 0,
413
+ checked: true,
414
+ blockers
415
+ };
416
+ }
417
+
418
+ async function readTextIfExists(file) {
419
+ try {
420
+ return await fsp.readFile(file, 'utf8');
421
+ } catch {
422
+ return '';
423
+ }
424
+ }
425
+
426
+ async function findDefaultPluginSource(plugin, { home, configText }) {
427
+ const cached = await findPluginCache(plugin.name, { home });
428
+ if (cached) return cached;
429
+ for (const source of marketplaceSources(configText, plugin.marketplace)) {
430
+ const candidate = path.join(source, 'plugins', plugin.name, '.codex-plugin', 'plugin.json');
431
+ if (await exists(candidate)) return path.dirname(path.dirname(candidate));
432
+ }
433
+ return null;
434
+ }
435
+
436
+ function marketplaceSources(configText = '', marketplaceName = '') {
437
+ const table = `marketplaces.${marketplaceName}`;
438
+ const re = new RegExp(`(?:^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`, 'g');
439
+ const sources = [];
440
+ for (const match of String(configText || '').matchAll(re)) {
441
+ const source = match[1].match(/(?:^|\n)\s*source\s*=\s*"([^"]+)"/)?.[1];
442
+ if (source) sources.push(source);
443
+ }
444
+ return Array.from(new Set(sources));
445
+ }
446
+
447
+ function codexPluginEnabled(configText = '', plugin = {}) {
448
+ const table = `plugins."${plugin.name}@${plugin.marketplace}"`;
449
+ const re = new RegExp(`(?:^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`);
450
+ const block = String(configText || '').match(re)?.[1] || '';
451
+ return /(?:^|\n)\s*enabled\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
452
+ }
453
+
454
+ function topLevelToml(text = '') {
455
+ const lines = String(text || '').split('\n');
456
+ const firstTable = lines.findIndex((line) => /^\s*\[.+\]\s*$/.test(line));
457
+ return (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
458
+ }
459
+
460
+ function tomlTable(text = '', table = '') {
461
+ const re = new RegExp(`(?:^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`);
462
+ return String(text || '').match(re)?.[1] || '';
463
+ }
464
+
465
+ function escapeRegExp(text = '') {
466
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
467
+ }
468
+
299
469
  function remoteControlGuidance(status = {}) {
300
470
  if (!status.codex_cli?.ok) return 'Codex remote-control requires Codex CLI 0.130.0+. Install with: npm i -g @openai/codex@latest';
301
471
  if (status.reason === 'codex_cli_version_unknown') return 'Codex remote-control requires Codex CLI 0.130.0+, but the installed CLI version could not be parsed. Check: codex --version';
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.78';
8
+ export const PACKAGE_VERSION = '0.8.2';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11