sneakoscope 5.5.2 → 5.6.0
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 +4 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +7 -1
- package/dist/cli/router.js +28 -0
- package/dist/commands/doctor.js +65 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agent-bridge/agent-manifest.js +64 -0
- package/dist/core/agent-bridge/agent-mode.js +29 -0
- package/dist/core/agent-bridge/mcp-server.js +156 -0
- package/dist/core/agents/agent-orchestrator.js +1 -11
- package/dist/core/commands/agent-bridge-command.js +86 -0
- package/dist/core/commands/image-ux-review-command.js +6 -6
- package/dist/core/commands/loop-command.js +2 -6
- package/dist/core/commands/mad-sks-command.js +3 -3
- package/dist/core/commands/mcp-server-command.js +13 -0
- package/dist/core/commands/naruto-command.js +4 -4
- package/dist/core/commands/ppt-command.js +6 -3
- package/dist/core/commands/qa-loop-command.js +64 -26
- package/dist/core/commands/team-command.js +1 -1
- package/dist/core/commands/wiki-command.js +71 -3
- package/dist/core/config/secret-preservation.js +43 -1
- package/dist/core/doctor/browser-use-repair.js +149 -0
- package/dist/core/doctor/computer-use-repair.js +166 -0
- package/dist/core/doctor/imagegen-repair.js +9 -0
- package/dist/core/doctor/mcp-transport-collision-repair.js +129 -0
- package/dist/core/doctor/supabase-mcp-repair.js +85 -16
- package/dist/core/feature-fixtures.js +8 -0
- package/dist/core/feature-registry.js +21 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/mad-db/mad-db-capability.js +1 -1
- package/dist/core/mission.js +19 -3
- package/dist/core/naruto/naruto-real-worker-child.js +5 -4
- package/dist/core/naruto/naruto-real-worker-runtime.js +5 -4
- package/dist/core/naruto/normalize-worker-prompt-text.js +12 -0
- package/dist/core/routes.js +3 -1
- package/dist/core/triwiki/code-index-scanner.js +313 -0
- package/dist/core/triwiki/code-pack.js +169 -0
- package/dist/core/triwiki/triwiki-cache-key.js +24 -2
- package/dist/core/triwiki-attention.js +34 -5
- package/dist/core/update-check.js +50 -0
- package/dist/core/version.js +1 -1
- package/dist/scripts/doctor-imagegen-repair-check.js +6 -1
- package/package.json +2 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { CODEX_APP_DOCS_URL } from '../codex-app.js';
|
|
3
|
+
import { computerUseStatusReport } from '../computer-use-status.js';
|
|
4
|
+
import { ensureDir, nowIso, runProcess, which, writeJsonAtomic } from '../fsx.js';
|
|
5
|
+
export const DOCTOR_COMPUTER_USE_REPAIR_SCHEMA = 'sks.doctor-computer-use-repair.v1';
|
|
6
|
+
export async function repairComputerUse(input) {
|
|
7
|
+
const root = path.resolve(input.root || process.cwd());
|
|
8
|
+
const apply = input.apply === true;
|
|
9
|
+
const probe = input.probe || computerUseStatusReport;
|
|
10
|
+
const steps = [];
|
|
11
|
+
const before = await probe({ root, codexBin: input.codexBin || undefined, forceMacos: true })
|
|
12
|
+
.catch((err) => ({ ok: false, status: 'unknown', blockers: [messageOf(err)] }));
|
|
13
|
+
let codexBin = input.codexBin || await which('codex').catch(() => null);
|
|
14
|
+
let versionStep;
|
|
15
|
+
if (codexBin) {
|
|
16
|
+
const version = await runProcess(codexBin, ['--version'], { timeoutMs: input.timeoutMs || 5000, maxOutputBytes: 16 * 1024 })
|
|
17
|
+
.catch((err) => ({ code: 1, stdout: '', stderr: messageOf(err) }));
|
|
18
|
+
versionStep = {
|
|
19
|
+
id: 'codex_cli_version',
|
|
20
|
+
ok: version.code === 0,
|
|
21
|
+
attempted: true,
|
|
22
|
+
command: `${codexBin} --version`,
|
|
23
|
+
exit_code: version.code,
|
|
24
|
+
stdout_tail: tail(version.stdout),
|
|
25
|
+
stderr_tail: tail(version.stderr),
|
|
26
|
+
blocker: version.code === 0 ? null : 'codex_cli_version_failed'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
versionStep = {
|
|
31
|
+
id: 'codex_cli_version',
|
|
32
|
+
ok: false,
|
|
33
|
+
attempted: false,
|
|
34
|
+
command: 'codex --version',
|
|
35
|
+
blocker: 'codex_binary_missing'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
steps.push(versionStep);
|
|
39
|
+
const beforeReady = before?.status === 'available';
|
|
40
|
+
const needsFeatureFlag = before?.status === 'codex_app_capability_missing' || before?.status === 'unknown';
|
|
41
|
+
if (codexBin && !beforeReady && needsFeatureFlag && apply) {
|
|
42
|
+
const enable = await runProcess(codexBin, ['features', 'enable', 'computer_use'], {
|
|
43
|
+
timeoutMs: input.timeoutMs || 10000,
|
|
44
|
+
maxOutputBytes: 32 * 1024
|
|
45
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: messageOf(err) }));
|
|
46
|
+
steps.push({
|
|
47
|
+
id: 'computer_use_feature_enable',
|
|
48
|
+
ok: enable.code === 0,
|
|
49
|
+
attempted: true,
|
|
50
|
+
command: `${codexBin} features enable computer_use`,
|
|
51
|
+
exit_code: enable.code,
|
|
52
|
+
stdout_tail: tail(enable.stdout),
|
|
53
|
+
stderr_tail: tail(enable.stderr),
|
|
54
|
+
blocker: enable.code === 0 ? null : 'codex_feature_enable_unsupported_or_failed'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
steps.push({
|
|
59
|
+
id: 'computer_use_feature_enable',
|
|
60
|
+
ok: beforeReady,
|
|
61
|
+
attempted: false,
|
|
62
|
+
command: codexBin ? `${codexBin} features enable computer_use` : 'codex features enable computer_use',
|
|
63
|
+
blocker: beforeReady
|
|
64
|
+
? null
|
|
65
|
+
: !apply
|
|
66
|
+
? 'doctor_fix_not_requested'
|
|
67
|
+
: !codexBin
|
|
68
|
+
? 'codex_cli_missing'
|
|
69
|
+
: 'computer_use_capability_missing_not_feature_flag_shaped'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// codex plugin install subcommand syntax is not documented anywhere in this repo or the
|
|
73
|
+
// Codex CLI docs snapshot; a real `codex plugin --help` lookup was attempted but the
|
|
74
|
+
// binary was unavailable/unverified in this environment, so no install command is guessed.
|
|
75
|
+
const pluginInstallStep = {
|
|
76
|
+
id: 'computer_use_plugin_install_lookup',
|
|
77
|
+
ok: false,
|
|
78
|
+
attempted: false,
|
|
79
|
+
command: codexBin ? `${codexBin} plugin --help` : 'codex plugin --help',
|
|
80
|
+
blocker: 'codex_plugin_install_subcommand_unverified'
|
|
81
|
+
};
|
|
82
|
+
let pluginHelpRaw = null;
|
|
83
|
+
if (codexBin && before?.status === 'codex_app_missing' && apply) {
|
|
84
|
+
const help = await runProcess(codexBin, ['plugin', '--help'], {
|
|
85
|
+
timeoutMs: input.timeoutMs || 8000,
|
|
86
|
+
maxOutputBytes: 32 * 1024
|
|
87
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: messageOf(err) }));
|
|
88
|
+
pluginHelpRaw = tail(help.stdout || help.stderr, 4000);
|
|
89
|
+
pluginInstallStep.attempted = true;
|
|
90
|
+
pluginInstallStep.exit_code = help.code;
|
|
91
|
+
pluginInstallStep.stdout_tail = tail(help.stdout);
|
|
92
|
+
pluginInstallStep.stderr_tail = tail(help.stderr);
|
|
93
|
+
const installSubcommand = extractPluginInstallSubcommand(help.stdout || help.stderr || '');
|
|
94
|
+
if (installSubcommand) {
|
|
95
|
+
pluginInstallStep.ok = false;
|
|
96
|
+
pluginInstallStep.blocker = 'codex_plugin_install_subcommand_found_but_not_auto_run';
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
pluginInstallStep.blocker = 'codex_plugin_help_did_not_reveal_install_subcommand';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
steps.push(pluginInstallStep);
|
|
103
|
+
const after = await probe({ root, codexBin: codexBin || undefined, forceMacos: true })
|
|
104
|
+
.catch((err) => ({ ok: false, status: 'unknown', blockers: [messageOf(err)] }));
|
|
105
|
+
steps.push({
|
|
106
|
+
id: 'computer_use_status_redetect',
|
|
107
|
+
ok: after?.status === 'available',
|
|
108
|
+
attempted: true,
|
|
109
|
+
command: 'codex features list --json',
|
|
110
|
+
blocker: after?.status === 'available' ? null : String(after?.status || 'unknown')
|
|
111
|
+
});
|
|
112
|
+
const recovered = after?.status === 'available';
|
|
113
|
+
const blockers = recovered ? [] : [
|
|
114
|
+
...new Set([
|
|
115
|
+
String(after?.status || 'computer_use_unavailable'),
|
|
116
|
+
...(pluginInstallStep.attempted && !recovered ? [pluginInstallStep.blocker] : [])
|
|
117
|
+
].filter(Boolean).map(String))
|
|
118
|
+
];
|
|
119
|
+
const nextActions = recovered ? [] : [
|
|
120
|
+
'Install/update Codex CLI if missing: npm i -g @openai/codex@latest',
|
|
121
|
+
'Open Codex App settings and enable Computer Use, or run: codex features enable computer_use',
|
|
122
|
+
after?.status === 'codex_app_missing'
|
|
123
|
+
? 'A live `codex plugin --help` (or equivalent) lookup is required to find the real plugin install subcommand before SKS can auto-install a Computer Use plugin; this was not guessed. A human should run `codex plugin --help` (or check Codex App settings) and report the exact install subcommand back so it can be wired into this repair function.'
|
|
124
|
+
: 'Verify with: codex features list --json',
|
|
125
|
+
`Docs: ${CODEX_APP_DOCS_URL}`
|
|
126
|
+
];
|
|
127
|
+
let report = {
|
|
128
|
+
schema: DOCTOR_COMPUTER_USE_REPAIR_SCHEMA,
|
|
129
|
+
generated_at: nowIso(),
|
|
130
|
+
ok: recovered,
|
|
131
|
+
attempted: !beforeReady,
|
|
132
|
+
apply,
|
|
133
|
+
recovered,
|
|
134
|
+
before,
|
|
135
|
+
after,
|
|
136
|
+
steps,
|
|
137
|
+
blockers,
|
|
138
|
+
next_actions: nextActions,
|
|
139
|
+
plugin_help_raw: pluginHelpRaw,
|
|
140
|
+
docs_url: CODEX_APP_DOCS_URL
|
|
141
|
+
};
|
|
142
|
+
if (input.reportPath !== null) {
|
|
143
|
+
const reportPath = input.reportPath || path.join(root, '.sneakoscope', 'reports', 'doctor-computer-use-repair.json');
|
|
144
|
+
try {
|
|
145
|
+
await ensureDir(path.dirname(reportPath));
|
|
146
|
+
await writeJsonAtomic(reportPath, report);
|
|
147
|
+
report = { ...report, report_path: reportPath };
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
report = { ...report, report_write_failed: true, report_write_error: messageOf(err) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return report;
|
|
154
|
+
}
|
|
155
|
+
function extractPluginInstallSubcommand(helpText) {
|
|
156
|
+
const match = String(helpText || '').match(/^\s*install\s+/m);
|
|
157
|
+
return match ? 'install' : null;
|
|
158
|
+
}
|
|
159
|
+
function tail(value, max = 2000) {
|
|
160
|
+
const text = String(value || '');
|
|
161
|
+
return text.length > max ? text.slice(-max) : text;
|
|
162
|
+
}
|
|
163
|
+
function messageOf(err) {
|
|
164
|
+
return err instanceof Error ? err.message : String(err);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=computer-use-repair.js.map
|
|
@@ -111,6 +111,14 @@ export async function repairCodexImagegen(input) {
|
|
|
111
111
|
command: 'codex features list --json',
|
|
112
112
|
blocker: after?.core_ready === true ? null : 'codex_app_builtin_imagegen_capability_missing'
|
|
113
113
|
});
|
|
114
|
+
// No real generation round-trip primitive exists in imagegen-capability.ts today; this only re-confirms the feature flag, not actual output.
|
|
115
|
+
const communicationTest = {
|
|
116
|
+
level: 'flag_level',
|
|
117
|
+
ok: after?.core_ready === true,
|
|
118
|
+
checked: 'codex features list --json (feature-flag/plugin metadata only)',
|
|
119
|
+
real_generation_round_trip_performed: false,
|
|
120
|
+
blocker: after?.core_ready === true ? null : 'codex_app_builtin_imagegen_capability_missing'
|
|
121
|
+
};
|
|
114
122
|
const recovered = after?.core_ready === true;
|
|
115
123
|
const blockers = recovered ? [] : [
|
|
116
124
|
...new Set([
|
|
@@ -129,6 +137,7 @@ export async function repairCodexImagegen(input) {
|
|
|
129
137
|
before,
|
|
130
138
|
after,
|
|
131
139
|
steps,
|
|
140
|
+
communication_test: communicationTest,
|
|
132
141
|
blockers,
|
|
133
142
|
manual_actions: recovered ? [] : [
|
|
134
143
|
`Install/update Codex CLI if missing: npm i -g @openai/codex@latest`,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
5
|
+
import { writeCodexConfigGuarded } from '../codex/codex-config-guard.js';
|
|
6
|
+
import { mcpServerBlock, mcpServerExplicitlyDisabled, readProjectCodexConfig, tomlTableRange } from '../mcp/mcp-config-preservation.js';
|
|
7
|
+
export async function detectAndRepairMcpTransportCollisions(input) {
|
|
8
|
+
const root = path.resolve(input.root);
|
|
9
|
+
const apply = input.apply === true;
|
|
10
|
+
const project = await readProjectCodexConfig(root);
|
|
11
|
+
const global = await readGlobalCodexConfigText();
|
|
12
|
+
const distinctConfigs = path.resolve(global.path) !== path.resolve(project.path);
|
|
13
|
+
const serverNames = listMcpServerNames(project.text);
|
|
14
|
+
let workingText = project.text;
|
|
15
|
+
const servers = [];
|
|
16
|
+
const blockers = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
for (const server of serverNames) {
|
|
19
|
+
const disabled = mcpServerExplicitlyDisabled(workingText, server);
|
|
20
|
+
if (disabled) {
|
|
21
|
+
servers.push({ server, status: 'disabled', project_transport: null, global_transport: null });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const projectBlock = mcpServerBlock(workingText, server);
|
|
25
|
+
const projectTransport = blockTransport(projectBlock);
|
|
26
|
+
const globalTransport = distinctConfigs ? blockTransport(mcpServerBlock(global.text, server)) : null;
|
|
27
|
+
const collision = Boolean(projectTransport) && Boolean(globalTransport) && projectTransport !== globalTransport;
|
|
28
|
+
if (!collision) {
|
|
29
|
+
servers.push({ server, status: 'no_collision', project_transport: projectTransport, global_transport: globalTransport });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!apply) {
|
|
33
|
+
servers.push({ server, status: 'collision_detected', project_transport: projectTransport, global_transport: globalTransport });
|
|
34
|
+
blockers.push(`mcp_transport_collision:${server}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const range = tomlTableRange(workingText, `mcp_servers.${server}`, true);
|
|
38
|
+
if (!range) {
|
|
39
|
+
servers.push({ server, status: 'collision_detected', project_transport: projectTransport, global_transport: globalTransport });
|
|
40
|
+
blockers.push(`mcp_transport_collision:${server}`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const commented = commentOutMcpServerBlock(workingText.slice(range.start, range.end), server, projectTransport, globalTransport);
|
|
44
|
+
const nextText = `${workingText.slice(0, range.start)}${commented}${workingText.slice(range.end)}`;
|
|
45
|
+
const resolved = nextText !== workingText;
|
|
46
|
+
if (!resolved) {
|
|
47
|
+
servers.push({ server, status: 'collision_detected', project_transport: projectTransport, global_transport: globalTransport });
|
|
48
|
+
blockers.push(`mcp_transport_collision:${server}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const before = workingText;
|
|
52
|
+
workingText = nextText;
|
|
53
|
+
await writeCodexConfigGuarded({
|
|
54
|
+
root,
|
|
55
|
+
configPath: project.path,
|
|
56
|
+
before,
|
|
57
|
+
cause: `mcp-transport-collision-repair:${server}`,
|
|
58
|
+
mutate: () => workingText
|
|
59
|
+
});
|
|
60
|
+
servers.push({ server, status: 'collision_resolved', project_transport: projectTransport, global_transport: globalTransport });
|
|
61
|
+
warnings.push(`mcp_transport_collision_resolved:${server}`);
|
|
62
|
+
}
|
|
63
|
+
let report = {
|
|
64
|
+
schema: 'sks.mcp-transport-collision-repair.v1',
|
|
65
|
+
generated_at: nowIso(),
|
|
66
|
+
ok: blockers.length === 0,
|
|
67
|
+
apply,
|
|
68
|
+
project_config_path: project.path,
|
|
69
|
+
global_config_path: global.path,
|
|
70
|
+
servers,
|
|
71
|
+
blockers,
|
|
72
|
+
warnings,
|
|
73
|
+
raw_secret_values_recorded: false
|
|
74
|
+
};
|
|
75
|
+
if (input.reportPath !== null) {
|
|
76
|
+
const reportPath = input.reportPath || path.join(root, '.sneakoscope', 'reports', 'doctor-mcp-transport-collision-repair.json');
|
|
77
|
+
try {
|
|
78
|
+
await writeJsonAtomic(reportPath, report);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
report = { ...report, report_write_failed: true };
|
|
82
|
+
process.stderr.write(`SKS doctor warning: failed to write MCP transport collision repair report ${reportPath}: ${messageOf(err)}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return report;
|
|
86
|
+
}
|
|
87
|
+
/** Scans only the top-level project config text for `[mcp_servers.<name>]` headers (not nested child tables like `.env`). */
|
|
88
|
+
function listMcpServerNames(text) {
|
|
89
|
+
const source = String(text || '');
|
|
90
|
+
const pattern = /^\s*\[mcp_servers\.([^.\]]+)\]\s*(?:#.*)?$/gm;
|
|
91
|
+
const names = [];
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
94
|
+
const name = match[1];
|
|
95
|
+
if (name && !names.includes(name))
|
|
96
|
+
names.push(name);
|
|
97
|
+
}
|
|
98
|
+
return names;
|
|
99
|
+
}
|
|
100
|
+
function blockTransport(block) {
|
|
101
|
+
if (!block)
|
|
102
|
+
return null;
|
|
103
|
+
if (/^\s*command\s*=/m.test(block))
|
|
104
|
+
return 'stdio';
|
|
105
|
+
if (/^\s*url\s*=/m.test(block))
|
|
106
|
+
return 'url';
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
async function readGlobalCodexConfigText() {
|
|
110
|
+
const home = process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex');
|
|
111
|
+
const file = path.join(home, 'config.toml');
|
|
112
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
113
|
+
return { path: file, text };
|
|
114
|
+
}
|
|
115
|
+
/** Comments every line of the colliding project MCP block (header + child tables like .env) so Codex stops merging
|
|
116
|
+
* two transports for the same server name, while keeping the original text — including secrets — recoverable in place. */
|
|
117
|
+
function commentOutMcpServerBlock(block, server, projectTransport, globalTransport) {
|
|
118
|
+
const note = `# [sks doctor] MCP server "${server}" project block disabled: project transport (${projectTransport}) collided with the global config's transport (${globalTransport}) for the same server name (Codex refuses the merged config, e.g. "url is not supported for stdio"). Re-add a block with a matching transport if this project needs its own "${server}" MCP server.\n`;
|
|
119
|
+
const commented = String(block || '')
|
|
120
|
+
.replace(/\s+$/, '')
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.map((line) => (line.length ? `# ${line}` : '#'))
|
|
123
|
+
.join('\n');
|
|
124
|
+
return `${note}${commented}\n`;
|
|
125
|
+
}
|
|
126
|
+
function messageOf(err) {
|
|
127
|
+
return err instanceof Error ? err.message : String(err);
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=mcp-transport-collision-repair.js.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
1
2
|
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
2
4
|
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
5
|
import { writeCodexConfigGuarded } from '../codex/codex-config-guard.js';
|
|
4
|
-
import { mcpServerBlock, mcpServerExplicitlyDisabled, readProjectCodexConfig, replaceOrAppendMcpServerBlock } from '../mcp/mcp-config-preservation.js';
|
|
6
|
+
import { mcpServerBlock, mcpServerExplicitlyDisabled, readProjectCodexConfig, replaceOrAppendMcpServerBlock, tomlTableRange } from '../mcp/mcp-config-preservation.js';
|
|
5
7
|
export async function repairSupabaseMcp(input) {
|
|
6
8
|
const root = path.resolve(input.root);
|
|
7
9
|
const config = await readProjectCodexConfig(root);
|
|
@@ -11,9 +13,37 @@ export async function repairSupabaseMcp(input) {
|
|
|
11
13
|
const tokenEnvPresent = Boolean(process.env.SUPABASE_ACCESS_TOKEN);
|
|
12
14
|
const readOnlyBefore = /read[_-]?only\s*=\s*true|access_mode\s*=\s*"read-only"|--read-only/.test(block);
|
|
13
15
|
const unsafeWriteAccessBefore = configured && !disabled && !readOnlyBefore && /write|service_role|SUPABASE_ACCESS_TOKEN/.test(block);
|
|
16
|
+
// Codex merges the global (~/.codex) and project (.codex) config per key. When
|
|
17
|
+
// the project defines a stdio supabase server (command=...) while the global
|
|
18
|
+
// one uses a streamable-http url, the merged table has both `command` and
|
|
19
|
+
// `url`, and Codex refuses to load with "url is not supported for stdio",
|
|
20
|
+
// blocking every chat/task in that project. Detect and disable the project's
|
|
21
|
+
// stdio block so it inherits the safe global read-only url form.
|
|
22
|
+
const globalConfig = await readGlobalCodexConfigText();
|
|
23
|
+
const distinctConfigs = path.resolve(globalConfig.path) !== path.resolve(config.path);
|
|
24
|
+
const projectTransport = blockTransport(block);
|
|
25
|
+
const globalTransport = distinctConfigs ? blockTransport(mcpServerBlock(globalConfig.text, 'supabase')) : null;
|
|
26
|
+
const stdioUrlTransportCollision = configured && !disabled && projectTransport === 'stdio' && globalTransport === 'url';
|
|
14
27
|
let afterText = config.text;
|
|
15
28
|
let readOnlyMigrated = false;
|
|
16
|
-
|
|
29
|
+
let transportCollisionResolved = false;
|
|
30
|
+
if (stdioUrlTransportCollision && input.apply) {
|
|
31
|
+
const range = tomlTableRange(config.text, 'mcp_servers.supabase', true);
|
|
32
|
+
if (range) {
|
|
33
|
+
const commented = commentOutStdioSupabaseBlock(config.text.slice(range.start, range.end));
|
|
34
|
+
afterText = `${config.text.slice(0, range.start)}${commented}${config.text.slice(range.end)}`;
|
|
35
|
+
transportCollisionResolved = afterText !== config.text;
|
|
36
|
+
if (transportCollisionResolved)
|
|
37
|
+
await writeCodexConfigGuarded({
|
|
38
|
+
root,
|
|
39
|
+
configPath: config.path,
|
|
40
|
+
before: config.text,
|
|
41
|
+
cause: 'supabase-mcp-transport-collision',
|
|
42
|
+
mutate: () => afterText
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (configured && !disabled && !readOnlyBefore && input.apply) {
|
|
17
47
|
afterText = replaceOrAppendMcpServerBlock(config.text, 'supabase', setReadOnly(block));
|
|
18
48
|
readOnlyMigrated = afterText !== config.text;
|
|
19
49
|
if (readOnlyMigrated)
|
|
@@ -25,16 +55,20 @@ export async function repairSupabaseMcp(input) {
|
|
|
25
55
|
mutate: () => afterText
|
|
26
56
|
});
|
|
27
57
|
}
|
|
28
|
-
|
|
58
|
+
// A resolved collision comments the whole stdio block out, so the project no
|
|
59
|
+
// longer contributes an active supabase server at all.
|
|
60
|
+
const effectivelyConfigured = configured && !transportCollisionResolved;
|
|
61
|
+
const afterBlock = transportCollisionResolved ? '' : readOnlyMigrated ? mcpServerBlock(afterText, 'supabase') || '' : block;
|
|
29
62
|
const readOnlyAfter = /read[_-]?only\s*=\s*true|access_mode\s*=\s*"read-only"|--read-only/.test(afterBlock);
|
|
30
|
-
const unsafeWriteAccess =
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
63
|
+
const unsafeWriteAccess = effectivelyConfigured && !disabled && !readOnlyAfter && /write|service_role|SUPABASE_ACCESS_TOKEN/.test(afterBlock);
|
|
64
|
+
const transportCollisionUnresolved = stdioUrlTransportCollision && !transportCollisionResolved;
|
|
65
|
+
const writeScopeRequiresConfirmation = effectivelyConfigured && !disabled && (unsafeWriteAccessBefore || !readOnlyAfter);
|
|
66
|
+
const readyBlocking = unsafeWriteAccess || transportCollisionUnresolved;
|
|
67
|
+
const manualRequired = effectivelyConfigured && !disabled && (!tokenEnvPresent || writeScopeRequiresConfirmation);
|
|
34
68
|
let report = {
|
|
35
69
|
schema: 'sks.doctor-supabase-mcp-repair.v1',
|
|
36
70
|
generated_at: nowIso(),
|
|
37
|
-
ok: !configured || disabled || !unsafeWriteAccess,
|
|
71
|
+
ok: (!configured || disabled || !unsafeWriteAccess) && !transportCollisionUnresolved,
|
|
38
72
|
apply: input.apply === true,
|
|
39
73
|
configured,
|
|
40
74
|
disabled,
|
|
@@ -42,18 +76,26 @@ export async function repairSupabaseMcp(input) {
|
|
|
42
76
|
token_env_present: tokenEnvPresent,
|
|
43
77
|
unsafe_write_access: unsafeWriteAccess,
|
|
44
78
|
read_only_migrated: readOnlyMigrated,
|
|
79
|
+
stdio_url_transport_collision: stdioUrlTransportCollision,
|
|
80
|
+
transport_collision_resolved: transportCollisionResolved,
|
|
45
81
|
write_scope_requires_confirmation: writeScopeRequiresConfirmation,
|
|
46
82
|
ready_blocking: readyBlocking,
|
|
47
83
|
manual_required: manualRequired,
|
|
48
|
-
next_action:
|
|
49
|
-
?
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
84
|
+
next_action: transportCollisionUnresolved
|
|
85
|
+
? 'Project Supabase MCP uses stdio while the global config uses a streamable-http url; Codex rejects the merged config with "url is not supported for stdio". Run `sks doctor --fix` to disable the project stdio block so it inherits the safe global read-only url.'
|
|
86
|
+
: manualRequired
|
|
87
|
+
? tokenEnvPresent
|
|
88
|
+
? 'Set persistent Supabase MCP to read-only. Write-scoped Supabase MCP is allowed only through a mission-local MadDB runtime profile.'
|
|
89
|
+
: 'Set SUPABASE_ACCESS_TOKEN only when an explicit MadDB run needs Supabase MCP auth; otherwise keep persistent Supabase MCP disabled/read-only.'
|
|
90
|
+
: null,
|
|
91
|
+
blockers: [
|
|
92
|
+
...(unsafeWriteAccess ? ['supabase_mcp_write_access_not_safe_by_default'] : []),
|
|
93
|
+
...(transportCollisionUnresolved ? ['supabase_mcp_stdio_url_transport_collision'] : [])
|
|
94
|
+
],
|
|
54
95
|
warnings: [
|
|
55
|
-
...(
|
|
56
|
-
...(readOnlyMigrated ? ['supabase_mcp_migrated_to_read_only'] : [])
|
|
96
|
+
...(effectivelyConfigured && !tokenEnvPresent ? ['supabase_access_token_unset_write_features_manual_required'] : []),
|
|
97
|
+
...(readOnlyMigrated ? ['supabase_mcp_migrated_to_read_only'] : []),
|
|
98
|
+
...(transportCollisionResolved ? ['supabase_mcp_stdio_block_disabled_for_url_collision'] : [])
|
|
57
99
|
],
|
|
58
100
|
raw_secret_values_recorded: false
|
|
59
101
|
};
|
|
@@ -81,4 +123,31 @@ function setReadOnly(block) {
|
|
|
81
123
|
lines.push('read_only = true');
|
|
82
124
|
return `${lines.join('\n')}\n`;
|
|
83
125
|
}
|
|
126
|
+
async function readGlobalCodexConfigText() {
|
|
127
|
+
const home = process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex');
|
|
128
|
+
const file = path.join(home, 'config.toml');
|
|
129
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
130
|
+
return { path: file, text };
|
|
131
|
+
}
|
|
132
|
+
function blockTransport(block) {
|
|
133
|
+
if (!block)
|
|
134
|
+
return null;
|
|
135
|
+
if (/^\s*command\s*=/m.test(block))
|
|
136
|
+
return 'stdio';
|
|
137
|
+
if (/^\s*url\s*=/m.test(block))
|
|
138
|
+
return 'url';
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/** Comment every line of a supabase MCP block (header + child .env table) so
|
|
142
|
+
* Codex stops loading it as a stdio server, while keeping the original text —
|
|
143
|
+
* including the access token — recoverable in place. */
|
|
144
|
+
function commentOutStdioSupabaseBlock(block) {
|
|
145
|
+
const note = '# [sks doctor] Supabase MCP stdio block disabled: it collided with the global read-only URL Supabase entry (Codex: "url is not supported for stdio"). Re-add a read-only URL form (url = "https://mcp.supabase.com/mcp?project_ref=<ref>&read_only=true&features=database,docs") if this project needs its own Supabase MCP.\n';
|
|
146
|
+
const commented = String(block || '')
|
|
147
|
+
.replace(/\s+$/, '')
|
|
148
|
+
.split(/\r?\n/)
|
|
149
|
+
.map((line) => (line.length ? `# ${line}` : '#'))
|
|
150
|
+
.join('\n');
|
|
151
|
+
return `${note}${commented}\n`;
|
|
152
|
+
}
|
|
84
153
|
//# sourceMappingURL=supabase-mcp-repair.js.map
|
|
@@ -44,6 +44,14 @@ const FIXTURES = Object.freeze({
|
|
|
44
44
|
'cli-versioning': fixture('execute', 'sks versioning status --json', [], 'pass'),
|
|
45
45
|
'cli-aliases': fixture('execute', 'sks aliases', [], 'pass'),
|
|
46
46
|
'cli-fix-path': fixture('execute', 'sks fix-path --json', [], 'pass'),
|
|
47
|
+
// selftest --real executes this fixture directly against this real repo (no
|
|
48
|
+
// hermetic isolation for execute/execute_and_validate_artifacts fixtures - see
|
|
49
|
+
// feature-fixture-executor.ts), so a real full scan (~79 modules) needs a
|
|
50
|
+
// realistic budget; the 8000 default is sized for smaller/typical repos, not
|
|
51
|
+
// this one, so pass an explicit larger budget rather than lowering the gate.
|
|
52
|
+
'cli-wiki-code': fixture('execute_and_validate_artifacts', 'sks wiki refresh --code --token-budget 20000 --json', [{ path: '.sneakoscope/wiki/code-pack.json', schema: 'sks.code-pack.v1', optional: true }], 'pass'),
|
|
53
|
+
'cli-agent-bridge': fixture('execute_and_validate_artifacts', 'sks agent-bridge setup --json', [{ path: '.sneakoscope/agent-bridge/manifest.json', schema: 'sks.agent-manifest.v1', optional: true }], 'pass'),
|
|
54
|
+
'cli-mcp-server': fixture('execute', 'sks mcp-server --probe', [], 'pass'),
|
|
47
55
|
'cli-selftest': fixture('execute', 'sks selftest --mock', [], 'pass'),
|
|
48
56
|
'cli-git': fixture('execute', 'sks git policy --json', [], 'pass'),
|
|
49
57
|
'cli-uninstall': fixture('execute', 'sks uninstall --dry-run --json', [], 'pass'),
|
|
@@ -54,6 +54,7 @@ export async function buildFeatureRegistry({ root = packageRoot(), generatedAt =
|
|
|
54
54
|
features.push(agentProofEvidenceFeature());
|
|
55
55
|
features.push(doctorImagegenRepairFeature());
|
|
56
56
|
features.push(...imagegenWiringFeatures());
|
|
57
|
+
features.push(wikiCodePackFeature());
|
|
57
58
|
for (const skillName of skillNames) {
|
|
58
59
|
if (!skillCoveredByRoute(skillName))
|
|
59
60
|
features.push(skillFeature(skillName));
|
|
@@ -714,6 +715,26 @@ function agentProofEvidenceFeature() {
|
|
|
714
715
|
}
|
|
715
716
|
});
|
|
716
717
|
}
|
|
718
|
+
function wikiCodePackFeature() {
|
|
719
|
+
return baseFeature({
|
|
720
|
+
id: 'cli-wiki-code',
|
|
721
|
+
commands: ['sks wiki refresh --code --json', 'sks wiki validate --json'],
|
|
722
|
+
aliases: ['wiki.code_pack', 'code_pack_refresh'],
|
|
723
|
+
category: 'triwiki',
|
|
724
|
+
maturity: 'beta',
|
|
725
|
+
intent: 'Deterministic codebase scan (any repo, not just this one) turned into a source-cited, quality-gated code pack, wired into TriWiki attention as a dedicated code: sub-budget so LLM handoffs see accurate codebase context at low token cost.',
|
|
726
|
+
voxel_triwiki_integration: 'code: entries ranked into attention.use_first/hydrate_first by trust_score, independent of the policy-claim RGBA/geometric selection',
|
|
727
|
+
completion_proof_integration: 'sks wiki validate --json reports code_pack freshness (fresh/stale/missing) by comparing the pack\'s recorded git HEAD sha to the current one',
|
|
728
|
+
known_gaps: ['ranking is by trust_score only, not live per-prompt keyword relevance, since contextCapsule\'s call site here refreshes a project-wide pack rather than a per-mission one'],
|
|
729
|
+
source_refs: {
|
|
730
|
+
cli_command_names: ['wiki'],
|
|
731
|
+
handler_keys: ['wiki'],
|
|
732
|
+
dollar_commands: [],
|
|
733
|
+
app_skill_aliases: [],
|
|
734
|
+
skills: ['triwiki']
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
717
738
|
function doctorImagegenRepairFeature() {
|
|
718
739
|
return baseFeature({
|
|
719
740
|
id: 'doctor:imagegen-repair',
|
package/dist/core/fsx.js
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
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
export const PACKAGE_VERSION = '5.
|
|
8
|
+
export const PACKAGE_VERSION = '5.6.0';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
export function nowIso() {
|
|
@@ -88,7 +88,7 @@ export async function resolveMadDbMissionId(root, state = {}, explicitMissionId
|
|
|
88
88
|
return String(state.mad_db_capability_mission_id);
|
|
89
89
|
if (state?.mission_id)
|
|
90
90
|
return String(state.mission_id);
|
|
91
|
-
return findLatestMission(root);
|
|
91
|
+
return findLatestMission(root, { mode: 'mad-db' });
|
|
92
92
|
}
|
|
93
93
|
export function isMadDbCapabilityActive(capability, nowMs = Date.now()) {
|
|
94
94
|
if (!capability)
|
package/dist/core/mission.js
CHANGED
|
@@ -46,24 +46,40 @@ export async function loadMission(root, id) {
|
|
|
46
46
|
const mission = await readJson(path.join(dir, 'mission.json'));
|
|
47
47
|
return { id, dir, mission };
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
function normalizeMissionRoute(value) {
|
|
50
|
+
return String(value || '').replace(/^\$/, '').replace(/_/g, '-').toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
export async function findLatestMission(root, opts = {}) {
|
|
53
|
+
const { route = null, mode = null, gateFile = null } = opts || {};
|
|
50
54
|
const dir = missionsDir(root);
|
|
51
55
|
if (!(await exists(dir)))
|
|
52
56
|
return null;
|
|
53
57
|
const fs = await import('node:fs/promises');
|
|
54
58
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
55
59
|
const ids = entries.filter((e) => e.isDirectory() && e.name.startsWith('M-')).map((e) => e.name);
|
|
56
|
-
const candidates = await Promise.all(ids.map(async (id) => {
|
|
60
|
+
const candidates = (await Promise.all(ids.map(async (id) => {
|
|
57
61
|
const dirPath = missionDir(root, id);
|
|
58
62
|
const stat = await fs.stat(dirPath).catch(() => null);
|
|
59
63
|
const mission = await readJson(path.join(dirPath, 'mission.json'), {}).catch(() => ({}));
|
|
64
|
+
if (mode && mission.mode !== mode)
|
|
65
|
+
return null;
|
|
66
|
+
// Route scoping is indirect: mission.json never carries a route field, so the
|
|
67
|
+
// caller must name the route-specific gate artifact to probe for.
|
|
68
|
+
if (route && gateFile) {
|
|
69
|
+
const gate = await readJson(path.join(dirPath, gateFile), null).catch(() => null);
|
|
70
|
+
if (!gate)
|
|
71
|
+
return null;
|
|
72
|
+
const actual = normalizeMissionRoute(gate.route || gate.route_command || '');
|
|
73
|
+
if (actual && actual !== normalizeMissionRoute(route))
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
60
76
|
const createdMs = Date.parse(mission.created_at || mission.updated_at || '');
|
|
61
77
|
return {
|
|
62
78
|
id,
|
|
63
79
|
createdMs: Number.isFinite(createdMs) ? createdMs : 0,
|
|
64
80
|
mtimeMs: stat?.mtimeMs || 0
|
|
65
81
|
};
|
|
66
|
-
}));
|
|
82
|
+
}))).filter((candidate) => candidate !== null);
|
|
67
83
|
candidates.sort((a, b) => (a.createdMs - b.createdMs) || (a.mtimeMs - b.mtimeMs) || a.id.localeCompare(b.id));
|
|
68
84
|
return candidates.at(-1)?.id || null;
|
|
69
85
|
}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { appendJsonlBounded, ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
4
4
|
import { runCodexTask } from '../codex-control/codex-control-plane.js';
|
|
5
5
|
import { CODEX_AGENT_WORKER_RESULT_SCHEMA_ID, codexAgentWorkerResultSchema } from '../codex-control/schemas/agent-worker-result.schema.js';
|
|
6
|
+
import { normalizeWorkerPromptText } from './normalize-worker-prompt-text.js';
|
|
6
7
|
async function main() {
|
|
7
8
|
const intakePath = process.argv[2];
|
|
8
9
|
if (!intakePath)
|
|
@@ -78,6 +79,8 @@ async function main() {
|
|
|
78
79
|
sdk_run_id: taskResult.sdkRunId || null
|
|
79
80
|
},
|
|
80
81
|
changed_files: Array.isArray(workerResult?.changed_files) ? workerResult.changed_files : [],
|
|
82
|
+
parent_prompt_truncated: intake.parent_prompt_truncated === true,
|
|
83
|
+
parent_prompt_dropped_chars: Number(intake.parent_prompt_dropped_chars || 0),
|
|
81
84
|
blockers
|
|
82
85
|
});
|
|
83
86
|
finished = true;
|
|
@@ -167,7 +170,8 @@ function backendPreference(value) {
|
|
|
167
170
|
}
|
|
168
171
|
function buildNarutoWorkerPrompt(item, parentPrompt) {
|
|
169
172
|
const writeAllowed = item?.write_allowed === true;
|
|
170
|
-
const
|
|
173
|
+
const parentObjectiveNormalized = normalizeWorkerPromptText(parentPrompt);
|
|
174
|
+
const parentObjective = parentObjectiveNormalized.text;
|
|
171
175
|
return [
|
|
172
176
|
'You are a Naruto route worker. Complete only this assigned work item and return JSON matching the required schema.',
|
|
173
177
|
parentObjective ? `Parent Naruto objective:\n${parentObjective}` : null,
|
|
@@ -196,9 +200,6 @@ function buildNarutoWorkerPrompt(item, parentPrompt) {
|
|
|
196
200
|
'Include verification checks, rollback notes, blockers, findings, and changed_files.'
|
|
197
201
|
].filter(Boolean).join('\n');
|
|
198
202
|
}
|
|
199
|
-
function normalizeWorkerPromptText(value) {
|
|
200
|
-
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 4000);
|
|
201
|
-
}
|
|
202
203
|
main().then(() => {
|
|
203
204
|
process.exit(0);
|
|
204
205
|
}).catch((err) => {
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
6
6
|
import { allocateWorkerWorktree } from '../git/git-worktree-manager.js';
|
|
7
7
|
import { cleanupGitWorktree } from '../git/git-worktree-cleanup.js';
|
|
8
|
+
import { normalizeWorkerPromptText } from './normalize-worker-prompt-text.js';
|
|
8
9
|
export async function spawnActualNarutoWorker(input) {
|
|
9
10
|
const workerDir = path.join(input.root, '.sneakoscope', 'missions', input.missionId, 'agents', 'naruto-real-workers', input.item.id);
|
|
10
11
|
await ensureDir(workerDir);
|
|
@@ -26,12 +27,15 @@ export async function spawnActualNarutoWorker(input) {
|
|
|
26
27
|
const resultPath = path.join(workerDir, 'worker-result.json');
|
|
27
28
|
const heartbeatPath = path.join(workerDir, 'worker-heartbeat.jsonl');
|
|
28
29
|
const intakePath = path.join(workerDir, 'worker-intake.json');
|
|
30
|
+
const normalizedParentPrompt = normalizeWorkerPromptText(input.parentPrompt);
|
|
29
31
|
await writeJsonAtomic(intakePath, {
|
|
30
32
|
schema: 'sks.naruto-actual-worker-intake.v1',
|
|
31
33
|
generated_at: nowIso(),
|
|
32
34
|
mission_id: input.missionId,
|
|
33
35
|
item: input.item,
|
|
34
|
-
parent_prompt:
|
|
36
|
+
parent_prompt: normalizedParentPrompt.text,
|
|
37
|
+
parent_prompt_truncated: normalizedParentPrompt.truncated,
|
|
38
|
+
parent_prompt_dropped_chars: normalizedParentPrompt.dropped_chars,
|
|
35
39
|
placement: input.placement,
|
|
36
40
|
backend: input.backend,
|
|
37
41
|
result_path: resultPath,
|
|
@@ -88,9 +92,6 @@ export async function collectActualNarutoWorker(handle) {
|
|
|
88
92
|
blockers
|
|
89
93
|
};
|
|
90
94
|
}
|
|
91
|
-
function normalizeWorkerPromptText(value) {
|
|
92
|
-
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 4000);
|
|
93
|
-
}
|
|
94
95
|
function actualWorkerEntrypoint() {
|
|
95
96
|
return fileURLToPath(new URL('./naruto-real-worker-child.js', import.meta.url));
|
|
96
97
|
}
|