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.
- package/README.md +28 -3
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +38 -9
- package/src/cli/main.mjs +65 -23
- package/src/cli/maintenance-commands.mjs +98 -6
- package/src/cli/recallpulse-command.mjs +157 -0
- package/src/core/codex-app.mjs +181 -11
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +95 -1
- package/src/core/init.mjs +43 -8
- package/src/core/pipeline.mjs +3 -3
- package/src/core/recallpulse.mjs +1215 -0
- package/src/core/research.mjs +119 -60
- package/src/core/routes.mjs +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
|
-
import {
|
|
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 =
|
|
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
|
+
}
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -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 = [
|
|
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 &&
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
|
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
|
|