sneakoscope 0.7.43 → 0.7.45
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 +12 -3
- package/package.json +1 -1
- package/src/cli/codex-app-command.mjs +67 -0
- package/src/cli/install-helpers.mjs +77 -1
- package/src/cli/main.mjs +116 -7
- package/src/core/codex-app.mjs +86 -3
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +21 -4
- package/src/core/pipeline.mjs +97 -5
- package/src/core/ppt.mjs +571 -13
- package/src/core/questions.mjs +9 -2
- package/src/core/routes.mjs +2 -2
- package/src/core/tmux-ui.mjs +89 -84
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ sks selftest --mock
|
|
|
43
43
|
| Area | What it does |
|
|
44
44
|
| --- | --- |
|
|
45
45
|
| CLI runtime | Bare `sks` opens or reuses the default tmux Codex CLI workspace. `sks tmux open` remains the explicit form for session/workspace flags, and `sks --mad` launches the explicit full-access high-reasoning profile. |
|
|
46
|
-
| Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
|
|
46
|
+
| Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. `sks codex-app remote-control` wraps Codex CLI 0.130.0+ headless remote control without falling back to older app-server internals. |
|
|
47
47
|
| OpenClaw agents | Generates an OpenClaw skill package so OpenClaw agents can attach `sneakoscope-codex`, enable the `shell` tool, and discover/use SKS commands from the target repo root. |
|
|
48
48
|
| Pipeline plans | Writes `pipeline-plan.json` for stateful routes so the runtime lane, kept stages, skipped stages, verification commands, and no-unrequested-fallback invariant are visible with `sks pipeline plan`. |
|
|
49
49
|
| Team orchestration | Runs substantial work through score-based ambiguity handling, scouts, TriWiki refresh, debate, runtime task graphs, worker inboxes, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence to skip unrelated pipeline work instead of expanding Team. |
|
|
@@ -166,7 +166,7 @@ sks tmux check
|
|
|
166
166
|
sks tmux status --once
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. By default it launches Codex in the SKS fast-high runtime (`--model gpt-5.5 -c model_reasoning_effort="high"`) with a
|
|
169
|
+
Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. By default it launches Codex in the SKS fast-high runtime (`--model gpt-5.5 -c model_reasoning_effort="high"`) with a static SKS 3D ASCII intro inside tmux; the animated intro is reserved for non-tmux unauthenticated Codex launches and can be disabled with `SKS_TMUX_LOGO_ANIMATION=0`. Override the runtime with `SKS_CODEX_MODEL`, `SKS_CODEX_REASONING`, or disable the default model profile with `SKS_CODEX_FAST_HIGH=0`. Use `sks tmux open` when you need explicit `--workspace` / `--session` flags, `sks tmux check` for readiness without launching, and `sks help` for CLI help. Use `--no-attach` or `SKS_TMUX_NO_AUTO_ATTACH=1` when you only want SKS to create/reuse the session and print the manual attach command.
|
|
170
170
|
|
|
171
171
|
Before opening tmux, SKS checks the installed Codex CLI against npm `@openai/codex@latest`. If a newer version exists, it asks `Y/n`; answering `y` updates automatically with `npm i -g @openai/codex@latest` and then opens tmux with the updated Codex CLI.
|
|
172
172
|
|
|
@@ -183,7 +183,7 @@ Bare `sks` asks this before opening Codex when codex-lb is not configured:
|
|
|
183
183
|
Authenticate and route Codex through codex-lb? [y/N]
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
|
|
186
|
+
Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode selection visible by avoiding legacy top-level `model`, `model_reasoning_effort`, and `service_tier` locks in `~/.codex/config.toml`; route-specific reasoning stays in named profiles or explicit tmux launch args. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
|
|
187
187
|
|
|
188
188
|
```toml
|
|
189
189
|
model_provider = "codex-lb"
|
|
@@ -299,9 +299,18 @@ After installing, run:
|
|
|
299
299
|
```sh
|
|
300
300
|
sks bootstrap
|
|
301
301
|
sks codex-app check
|
|
302
|
+
sks codex-app remote-control --status
|
|
302
303
|
sks dollar-commands
|
|
303
304
|
```
|
|
304
305
|
|
|
306
|
+
For headless remotely controllable Codex App/server sessions on Codex CLI 0.130.0 or newer, run:
|
|
307
|
+
|
|
308
|
+
```sh
|
|
309
|
+
sks codex-app remote-control -- --help
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`sks codex-app check` reports whether the installed Codex CLI is new enough. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
|
|
313
|
+
|
|
305
314
|
Then open Codex App and use prompt commands directly in the chat. Examples:
|
|
306
315
|
|
|
307
316
|
```text
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.45",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { codexRemoteControlStatus, formatCodexRemoteControlStatus } from '../core/codex-app.mjs';
|
|
3
|
+
|
|
4
|
+
export async function codexAppRemoteControlCommand(args = [], opts = {}) {
|
|
5
|
+
const controlArgs = argsBeforeSeparator(args);
|
|
6
|
+
if (controlArgs.includes('--help') || controlArgs.includes('-h')) {
|
|
7
|
+
console.log(remoteControlHelp());
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const status = await codexRemoteControlStatus();
|
|
12
|
+
if (controlArgs.includes('--json')) {
|
|
13
|
+
console.log(JSON.stringify(status, null, 2));
|
|
14
|
+
if (!status.ok) process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (controlArgs.includes('--status') || controlArgs.includes('--check') || controlArgs.includes('--dry-run')) {
|
|
19
|
+
console.log(formatCodexRemoteControlStatus(status));
|
|
20
|
+
if (!status.ok) process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!status.ok) {
|
|
25
|
+
console.error(formatCodexRemoteControlStatus(status));
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const passthrough = stripSeparator(args);
|
|
31
|
+
const spawnFn = opts.spawn || spawn;
|
|
32
|
+
const code = await spawnInherited(spawnFn, status.codex_cli.bin, ['remote-control', ...passthrough], {
|
|
33
|
+
cwd: process.cwd(),
|
|
34
|
+
env: process.env
|
|
35
|
+
});
|
|
36
|
+
if (code) process.exitCode = code;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function remoteControlHelp() {
|
|
40
|
+
return [
|
|
41
|
+
'Usage: sks codex-app remote-control [--status|--check|--dry-run|--json] [-- <codex remote-control args>]',
|
|
42
|
+
'',
|
|
43
|
+
'Starts Codex CLI 0.130.0+ remote-control, the headless remotely controllable app-server entrypoint.',
|
|
44
|
+
'SKS only wraps the first-party command and refuses older Codex CLI versions instead of falling back to app-server internals.'
|
|
45
|
+
].join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripSeparator(args = []) {
|
|
49
|
+
const index = args.indexOf('--');
|
|
50
|
+
return index >= 0 ? args.slice(index + 1) : args;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function argsBeforeSeparator(args = []) {
|
|
54
|
+
const index = args.indexOf('--');
|
|
55
|
+
return index >= 0 ? args.slice(0, index) : args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function spawnInherited(spawnFn, command, args, opts) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const child = spawnFn(command, args, { ...opts, stdio: 'inherit' });
|
|
61
|
+
child.on('error', (err) => {
|
|
62
|
+
console.error(`codex remote-control failed to start: ${err.message}`);
|
|
63
|
+
resolve(1);
|
|
64
|
+
});
|
|
65
|
+
child.on('close', (code) => resolve(code || 0));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -30,6 +30,11 @@ export async function postinstall({ bootstrap }) {
|
|
|
30
30
|
else if (context7Install.status === 'codex_missing') console.log('Context7 MCP: Codex CLI missing. Install @openai/codex or set SKS_CODEX_BIN, then run `sks context7 setup --scope global` or `sks setup` in a project.');
|
|
31
31
|
else if (context7Install.status === 'skipped') console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
|
|
32
32
|
else if (context7Install.status === 'failed') console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
|
|
33
|
+
const fastModeRepair = await ensureGlobalCodexFastModeDuringInstall();
|
|
34
|
+
if (fastModeRepair.status === 'updated') console.log(`Codex App Fast mode: restored in ${fastModeRepair.config_path}.`);
|
|
35
|
+
else if (fastModeRepair.status === 'present') console.log('Codex App Fast mode: config already compatible.');
|
|
36
|
+
else if (fastModeRepair.status === 'skipped') console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
|
|
37
|
+
else if (fastModeRepair.status === 'failed') console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
|
|
33
38
|
const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
|
|
34
39
|
if (globalSkills.status === 'installed') console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).`);
|
|
35
40
|
else if (globalSkills.status === 'partial') console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${globalSkills.missing_skills.join(', ')}. Run \`sks doctor --fix\`.`);
|
|
@@ -138,7 +143,7 @@ export async function configureCodexLb(opts = {}) {
|
|
|
138
143
|
if (!apiKey) return { ok: false, status: 'missing_api_key', config_path: configPath, env_path: envPath };
|
|
139
144
|
await ensureDir(path.dirname(configPath));
|
|
140
145
|
const current = await readText(configPath, '');
|
|
141
|
-
const next = upsertCodexLbConfig(current, baseUrl);
|
|
146
|
+
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
|
|
142
147
|
await writeTextAtomic(configPath, next);
|
|
143
148
|
await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
|
|
144
149
|
await fsp.chmod(envPath, 0o600).catch(() => {});
|
|
@@ -225,6 +230,77 @@ function upsertCodexLbConfig(text = '', baseUrl) {
|
|
|
225
230
|
return `${next.trim()}\n`;
|
|
226
231
|
}
|
|
227
232
|
|
|
233
|
+
export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
|
|
234
|
+
if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1') return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
|
|
235
|
+
const home = opts.home || process.env.HOME || os.homedir();
|
|
236
|
+
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
237
|
+
try {
|
|
238
|
+
await ensureDir(path.dirname(configPath));
|
|
239
|
+
const current = await readText(configPath, '');
|
|
240
|
+
const next = normalizeCodexFastModeUiConfig(current);
|
|
241
|
+
if (next === ensureTrailingNewline(current)) return { status: 'present', config_path: configPath };
|
|
242
|
+
await writeTextAtomic(configPath, next);
|
|
243
|
+
return { status: 'updated', config_path: configPath };
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return { status: 'failed', config_path: configPath, error: err.message };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function normalizeCodexFastModeUiConfig(text = '') {
|
|
250
|
+
let next = removeLegacyTopLevelCodexModeLocks(text);
|
|
251
|
+
next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
|
|
252
|
+
next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
|
|
253
|
+
next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
|
|
254
|
+
return ensureTrailingNewline(next);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function removeLegacyTopLevelCodexModeLocks(text = '') {
|
|
258
|
+
const legacy = {
|
|
259
|
+
model: new Set(['gpt-5.5']),
|
|
260
|
+
model_reasoning_effort: new Set(['high']),
|
|
261
|
+
service_tier: new Set(['fast'])
|
|
262
|
+
};
|
|
263
|
+
const lines = String(text || '').split('\n');
|
|
264
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
265
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
266
|
+
return lines.filter((line, index) => {
|
|
267
|
+
if (index >= end) return true;
|
|
268
|
+
const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/);
|
|
269
|
+
if (!match) return true;
|
|
270
|
+
return !legacy[match[1]]?.has(match[2]);
|
|
271
|
+
}).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function upsertTomlTableKey(text, table, line) {
|
|
275
|
+
const key = String(line).split('=')[0].trim();
|
|
276
|
+
const lines = String(text || '').trimEnd().split('\n');
|
|
277
|
+
if (lines.length === 1 && lines[0] === '') lines.length = 0;
|
|
278
|
+
const header = `[${table}]`;
|
|
279
|
+
const start = lines.findIndex((x) => x.trim() === header);
|
|
280
|
+
if (start === -1) return [...lines, ...(lines.length ? [''] : []), header, line].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
281
|
+
let end = lines.length;
|
|
282
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
283
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
284
|
+
end = i;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
289
|
+
for (let i = start + 1; i < end; i++) {
|
|
290
|
+
if (keyRe.test(lines[i])) {
|
|
291
|
+
lines[i] = line;
|
|
292
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
lines.splice(end, 0, line);
|
|
296
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ensureTrailingNewline(text = '') {
|
|
300
|
+
const value = String(text || '').trimEnd();
|
|
301
|
+
return value ? `${value}\n` : '';
|
|
302
|
+
}
|
|
303
|
+
|
|
228
304
|
function upsertTopLevelTomlString(text, key, value) {
|
|
229
305
|
const line = `${key} = "${value}"`;
|
|
230
306
|
const lines = String(text || '').split('\n');
|
package/src/cli/main.mjs
CHANGED
|
@@ -26,10 +26,15 @@ import { buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, wri
|
|
|
26
26
|
import {
|
|
27
27
|
PPT_AUDIENCE_STRATEGY_ARTIFACT,
|
|
28
28
|
PPT_CLEANUP_REPORT_ARTIFACT,
|
|
29
|
+
PPT_FACT_LEDGER_ARTIFACT,
|
|
29
30
|
PPT_GATE_ARTIFACT,
|
|
30
31
|
PPT_HTML_ARTIFACT,
|
|
32
|
+
PPT_IMAGE_ASSET_LEDGER_ARTIFACT,
|
|
33
|
+
PPT_ITERATION_REPORT_ARTIFACT,
|
|
31
34
|
PPT_PARALLEL_REPORT_ARTIFACT,
|
|
32
35
|
PPT_PDF_ARTIFACT,
|
|
36
|
+
PPT_REVIEW_LEDGER_ARTIFACT,
|
|
37
|
+
PPT_REVIEW_POLICY_ARTIFACT,
|
|
33
38
|
PPT_RENDER_REPORT_ARTIFACT,
|
|
34
39
|
PPT_SOURCE_HTML_DIR,
|
|
35
40
|
PPT_TEMP_DIR,
|
|
@@ -57,6 +62,7 @@ import { buildPromptContext } from '../core/prompt-context-builder.mjs';
|
|
|
57
62
|
import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
|
|
58
63
|
import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
|
|
59
64
|
import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
|
|
65
|
+
import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
|
|
60
66
|
import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
|
|
61
67
|
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
62
68
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
@@ -417,6 +423,10 @@ async function pptCommand(sub = 'status', args = []) {
|
|
|
417
423
|
console.log(`Mission: ${id}`);
|
|
418
424
|
console.log(`HTML: ${path.relative(root, result.files.html)}`);
|
|
419
425
|
console.log(`PDF: ${path.relative(root, result.files.pdf)}`);
|
|
426
|
+
console.log(`Facts: ${path.relative(root, result.files.fact_ledger)}`);
|
|
427
|
+
console.log(`Images: ${path.relative(root, result.files.image_asset_ledger)}`);
|
|
428
|
+
console.log(`Review: ${path.relative(root, result.files.review_ledger)}`);
|
|
429
|
+
console.log(`Loop: ${path.relative(root, result.files.iteration_report)}`);
|
|
420
430
|
console.log(`Report: ${path.relative(root, result.files.render_report)}`);
|
|
421
431
|
console.log(`Cleanup: ${path.relative(root, result.files.cleanup_report)}`);
|
|
422
432
|
console.log(`Parallel:${' '.repeat(1)}${path.relative(root, result.files.parallel_report)}`);
|
|
@@ -435,6 +445,11 @@ async function pptCommand(sub = 'status', args = []) {
|
|
|
435
445
|
html: path.join(dir, PPT_HTML_ARTIFACT),
|
|
436
446
|
source_html: path.join(dir, PPT_HTML_ARTIFACT),
|
|
437
447
|
pdf: path.join(dir, PPT_PDF_ARTIFACT),
|
|
448
|
+
fact_ledger: path.join(dir, PPT_FACT_LEDGER_ARTIFACT),
|
|
449
|
+
image_asset_ledger: path.join(dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT),
|
|
450
|
+
review_policy: path.join(dir, PPT_REVIEW_POLICY_ARTIFACT),
|
|
451
|
+
review_ledger: path.join(dir, PPT_REVIEW_LEDGER_ARTIFACT),
|
|
452
|
+
iteration_report: path.join(dir, PPT_ITERATION_REPORT_ARTIFACT),
|
|
438
453
|
render_report: path.join(dir, PPT_RENDER_REPORT_ARTIFACT),
|
|
439
454
|
cleanup_report: path.join(dir, PPT_CLEANUP_REPORT_ARTIFACT),
|
|
440
455
|
parallel_report: path.join(dir, PPT_PARALLEL_REPORT_ARTIFACT),
|
|
@@ -447,6 +462,10 @@ async function pptCommand(sub = 'status', args = []) {
|
|
|
447
462
|
console.log(`Gate: ${status.ok ? 'passed' : 'not passed'}`);
|
|
448
463
|
console.log(`HTML: ${path.relative(root, status.files.html)}`);
|
|
449
464
|
console.log(`PDF: ${path.relative(root, status.files.pdf)}`);
|
|
465
|
+
console.log(`Facts: ${path.relative(root, status.files.fact_ledger)}`);
|
|
466
|
+
console.log(`Images: ${path.relative(root, status.files.image_asset_ledger)}`);
|
|
467
|
+
console.log(`Review: ${path.relative(root, status.files.review_ledger)}`);
|
|
468
|
+
console.log(`Loop: ${path.relative(root, status.files.iteration_report)}`);
|
|
450
469
|
console.log(`Report: ${path.relative(root, status.files.render_report)}`);
|
|
451
470
|
console.log(`Cleanup: ${path.relative(root, status.files.cleanup_report)}`);
|
|
452
471
|
console.log(`Parallel:${' '.repeat(1)}${path.relative(root, status.files.parallel_report)}`);
|
|
@@ -1233,6 +1252,7 @@ async function autoReviewCommand(sub = 'status', args = []) {
|
|
|
1233
1252
|
|
|
1234
1253
|
async function codexAppHelp(args = []) {
|
|
1235
1254
|
const action = args[0] || 'help';
|
|
1255
|
+
if (action === 'remote-control' || action === 'remote') return codexAppRemoteControlCommand(args.slice(1));
|
|
1236
1256
|
if (action === 'check' || action === 'status') {
|
|
1237
1257
|
const status = await codexAppIntegrationStatus();
|
|
1238
1258
|
const skills = await codexAppSkillReadiness();
|
|
@@ -1259,7 +1279,7 @@ async function codexAppHelp(args = []) {
|
|
|
1259
1279
|
'ㅅㅋㅅ Codex App', '',
|
|
1260
1280
|
formatCodexAppStatus(status), '',
|
|
1261
1281
|
`Skills: project=${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} global=${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`}`, '',
|
|
1262
|
-
'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', '',
|
|
1282
|
+
'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks tmux check', '',
|
|
1263
1283
|
'Generated files:', ' .codex/config.toml', ' .codex/hooks.json', ' .agents/skills/', ' .codex/agents/', ' .codex/SNEAKOSCOPE.md', ' AGENTS.md', '',
|
|
1264
1284
|
'Git ignore:', ' default setup writes .gitignore entries for .sneakoscope/, .codex/, .agents/, AGENTS.md', ' --local-only writes those patterns to .git/info/exclude instead', '',
|
|
1265
1285
|
'Prompt routes:', formatDollarCommandsCompact(' ')
|
|
@@ -1302,9 +1322,9 @@ function usage(args = []) {
|
|
|
1302
1322
|
openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
|
|
1303
1323
|
team: ['Team', '', ' sks team "task" executor:5 reviewer:2 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team runs questions -> contract -> scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
|
|
1304
1324
|
'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
|
|
1305
|
-
ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT asks delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, and
|
|
1325
|
+
ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT asks delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; imagegen/gpt-image-2 and Context7 are conditional only when the sealed PPT contract needs raster assets, slide visual critique, or current external docs. Missing required image-review evidence blocks instead of being simulated.'],
|
|
1306
1326
|
goal: ['Goal', '', ' sks goal create "task"', ' sks goal status latest', ' sks goal pause latest', ' sks goal resume latest', ' sks goal clear latest'],
|
|
1307
|
-
'codex-app': ['Codex App', '', ' sks bootstrap', ' sks codex-app check', ' sks dollar-commands', ' cat .codex/SNEAKOSCOPE.md'],
|
|
1327
|
+
'codex-app': ['Codex App', '', ' sks bootstrap', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks dollar-commands', ' cat .codex/SNEAKOSCOPE.md'],
|
|
1308
1328
|
dollar: ['Dollar Commands', '', formatDollarCommandsCompact(' '), '', 'Terminal: sks dollar-commands [--json]'],
|
|
1309
1329
|
wiki: ['TriWiki', '', ' sks wiki pack', ' sks wiki refresh [--prune]', ' sks wiki sweep latest --json', ' sks wiki validate .sneakoscope/wiki/context-pack.json', ' sks wiki prune --dry-run --json', '', 'Packs include attention.use_first and attention.hydrate_first for compact recall plus source hydration. Sweep records intentional forgetting and promotion candidates.'],
|
|
1310
1330
|
harness: ['Harness Growth', '', ' sks harness fixture --json', ' sks harness review --json', '', 'Runs deterministic fixtures for deliberate forgetting, skill cards, harness experiments, tool error taxonomy, permission profiles, MultiAgentV2, and tmux cockpit views.'],
|
|
@@ -1757,6 +1777,13 @@ async function safeReadText(file, fallback = '') {
|
|
|
1757
1777
|
try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
|
|
1758
1778
|
}
|
|
1759
1779
|
|
|
1780
|
+
function hasTopLevelCodexModeLock(text = '') {
|
|
1781
|
+
const lines = String(text || '').split('\n');
|
|
1782
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1783
|
+
const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
|
|
1784
|
+
return /^model\s*=|^model_reasoning_effort\s*=|^service_tier\s*=/m.test(top);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1760
1787
|
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
1761
1788
|
function readMaxCycles(args, fallback) {
|
|
1762
1789
|
const i = args.indexOf('--max-cycles');
|
|
@@ -1999,6 +2026,8 @@ async function selftest() {
|
|
|
1999
2026
|
const defaultFastHighPlan = await buildTmuxLaunchPlan({ root: tmp, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2000
2027
|
if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
|
|
2001
2028
|
const codexLbHome = path.join(tmp, 'codex-lb-home');
|
|
2029
|
+
await ensureDir(path.join(codexLbHome, '.codex'));
|
|
2030
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n');
|
|
2002
2031
|
const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
|
|
2003
2032
|
cwd: tmp,
|
|
2004
2033
|
env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
|
|
@@ -2011,6 +2040,7 @@ async function selftest() {
|
|
|
2011
2040
|
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
2012
2041
|
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
2013
2042
|
if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !codexLbAuth.includes('"auth_mode": "apikey"')) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
|
|
2043
|
+
if (!codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode UI');
|
|
2014
2044
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
2015
2045
|
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
2016
2046
|
if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
|
|
@@ -2053,6 +2083,31 @@ async function selftest() {
|
|
|
2053
2083
|
maxOutputBytes: 64 * 1024
|
|
2054
2084
|
});
|
|
2055
2085
|
if (!String(openClawAutoUpdate.stdout || '').includes('Codex CLI ready: 0.1.0 -> codex-cli 99.0.0')) throw new Error('selftest failed: OpenClaw mode did not auto-approve Codex CLI update before tmux launch');
|
|
2086
|
+
const remoteControlBin = path.join(tmp, 'remote-control-bin');
|
|
2087
|
+
await ensureDir(remoteControlBin);
|
|
2088
|
+
await writeTextAtomic(path.join(remoteControlBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.130.0"; exit 0; fi\nif [ "$1" = "remote-control" ]; then echo "remote-control $*"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2089
|
+
await fsp.chmod(path.join(remoteControlBin, 'codex'), 0o755);
|
|
2090
|
+
const remoteControlStatus = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--dry-run', '--json'], {
|
|
2091
|
+
cwd: globalCwd,
|
|
2092
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlBin },
|
|
2093
|
+
timeoutMs: 15000,
|
|
2094
|
+
maxOutputBytes: 64 * 1024
|
|
2095
|
+
});
|
|
2096
|
+
if (remoteControlStatus.code !== 0) throw new Error(`selftest failed: Codex remote-control status exited ${remoteControlStatus.code}: ${remoteControlStatus.stderr}`);
|
|
2097
|
+
const remoteControlJson = JSON.parse(remoteControlStatus.stdout);
|
|
2098
|
+
if (!remoteControlJson.ok || remoteControlJson.min_version !== '0.130.0' || !String(remoteControlJson.command || '').includes('remote-control')) throw new Error('selftest failed: Codex remote-control status did not report 0.130.0 readiness');
|
|
2099
|
+
const remoteControlOldBin = path.join(tmp, 'remote-control-old-bin');
|
|
2100
|
+
await ensureDir(remoteControlOldBin);
|
|
2101
|
+
await writeTextAtomic(path.join(remoteControlOldBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.129.0"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2102
|
+
await fsp.chmod(path.join(remoteControlOldBin, 'codex'), 0o755);
|
|
2103
|
+
const remoteControlOldStatus = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--dry-run'], {
|
|
2104
|
+
cwd: globalCwd,
|
|
2105
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlOldBin },
|
|
2106
|
+
timeoutMs: 15000,
|
|
2107
|
+
maxOutputBytes: 64 * 1024
|
|
2108
|
+
});
|
|
2109
|
+
if (remoteControlOldStatus.code !== 1 || !String(`${remoteControlOldStatus.stdout}\n${remoteControlOldStatus.stderr}`).includes('Codex CLI 0.130.0+')) throw new Error('selftest failed: Codex remote-control did not block older Codex CLI versions');
|
|
2110
|
+
if (!COMMAND_CATALOG.find((entry) => entry.name === 'codex-app')?.usage.includes('remote-control')) throw new Error('selftest failed: codex-app command catalog does not advertise remote-control');
|
|
2056
2111
|
const guardBlocked = await checkHarnessModification(tmp, { tool_name: 'apply_patch', command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' });
|
|
2057
2112
|
if (guardBlocked.action !== 'block') throw new Error('selftest failed: harness guard allowed skill tampering');
|
|
2058
2113
|
const setupBlocked = await checkHarnessModification(tmp, { command: 'sks setup --force' });
|
|
@@ -2407,7 +2462,8 @@ async function selftest() {
|
|
|
2407
2462
|
if (!hookKoreanSksContext.includes('Route: $Team')) throw new Error('selftest failed: Korean implementation prompt did not promote to Team route');
|
|
2408
2463
|
if (hookKoreanSksContext.includes('SKS answer-only pipeline active')) throw new Error('selftest failed: Korean implementation prompt still used answer-only pipeline');
|
|
2409
2464
|
const hookKoreanSksState = await readJson(stateFile(hookKoreanSksTmp), {});
|
|
2410
|
-
if (hookKoreanSksState.phase !== '
|
|
2465
|
+
if (hookKoreanSksState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookKoreanSksState.implementation_allowed !== true || !hookKoreanSksState.ambiguity_gate_passed || !hookKoreanSksState.team_plan_ready) throw new Error('selftest failed: Korean Team auto-seal did not materialize Team');
|
|
2466
|
+
if (!(await exists(path.join(missionDir(hookKoreanSksTmp, hookKoreanSksState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: Korean Team auto-seal did not write team-plan.json');
|
|
2411
2467
|
const hookPaymentTeamTmp = tmpdir();
|
|
2412
2468
|
await initProject(hookPaymentTeamTmp, {});
|
|
2413
2469
|
const hookPaymentTeamPayload = JSON.stringify({ cwd: hookPaymentTeamTmp, prompt: '$Team 결제 재시도 정책과 로그인 세션 만료 버그 수정 executor:2 reviewer:1 user:1' });
|
|
@@ -2418,9 +2474,10 @@ async function selftest() {
|
|
|
2418
2474
|
if (!hookPaymentTeamContext.includes('Ambiguity gate auto-sealed')) throw new Error('selftest failed: predictable payment/auth Team prompt did not auto-seal');
|
|
2419
2475
|
if (hookPaymentTeamContext.includes('PAYMENT_RETRY_POLICY') || hookPaymentTeamContext.includes('AUTH_PROTOCOL_CHANGE_ALLOWED')) throw new Error('selftest failed: predictable payment/auth policy defaults were asked instead of inferred');
|
|
2420
2476
|
const hookPaymentTeamState = await readJson(stateFile(hookPaymentTeamTmp), {});
|
|
2421
|
-
if (hookPaymentTeamState.phase !== '
|
|
2477
|
+
if (hookPaymentTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookPaymentTeamState.implementation_allowed !== true || !hookPaymentTeamState.ambiguity_gate_passed || !hookPaymentTeamState.team_plan_ready) throw new Error('selftest failed: predictable payment/auth Team did not materialize after auto-seal');
|
|
2422
2478
|
const hookPaymentTeamSchema = await readJson(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'required-answers.schema.json'));
|
|
2423
2479
|
if (hookPaymentTeamSchema.slots.length !== 0 || hookPaymentTeamSchema.inferred_answers?.PAYMENT_RETRY_POLICY === undefined || hookPaymentTeamSchema.inferred_answers?.AUTH_SESSION_EXPIRED_BEHAVIOR === undefined) throw new Error('selftest failed: predictable payment/auth defaults were not recorded as inferred answers');
|
|
2480
|
+
if (!(await exists(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: predictable payment/auth Team auto-seal did not write team-plan.json');
|
|
2424
2481
|
const hookTeamTmp = tmpdir();
|
|
2425
2482
|
await initProject(hookTeamTmp, {});
|
|
2426
2483
|
const hookTeamPayload = JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 발표자료 만들어줘 executor:2 reviewer:1 user:1' });
|
|
@@ -2633,11 +2690,12 @@ async function selftest() {
|
|
|
2633
2690
|
if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
|
|
2634
2691
|
const preservedConfigTmp = tmpdir();
|
|
2635
2692
|
await ensureDir(path.join(preservedConfigTmp, '.codex'));
|
|
2636
|
-
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), '[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
|
|
2693
|
+
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
|
|
2637
2694
|
await initProject(preservedConfigTmp, {});
|
|
2638
2695
|
const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
|
|
2639
2696
|
if (!preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true') || !preservedConfig.includes('enabled = true') || !preservedConfig.includes('default_profile = "sks-fast-high"')) throw new Error('selftest failed: Codex config merge dropped or failed to enable Fast mode settings');
|
|
2640
2697
|
if (!preservedConfig.includes('codex_hooks = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error('selftest failed: Codex config merge did not add SKS managed settings');
|
|
2698
|
+
if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy mode locks that hide Fast mode UI');
|
|
2641
2699
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
2642
2700
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
2643
2701
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
|
@@ -2991,6 +3049,10 @@ async function selftest() {
|
|
|
2991
3049
|
if (buttonUxSlotIds.length) throw new Error(`selftest failed: clear small UI work should auto-seal, got ${buttonUxSlotIds.join(',')}`);
|
|
2992
3050
|
if (buttonUxSchema.inferred_answers.UI_STATE_BEHAVIOR !== 'infer_from_task_context_and_existing_design_system; preserve existing loading/error/empty/retry behavior unless explicitly requested; add only standard states required by the touched surface') throw new Error('selftest failed: UI state default inference missing');
|
|
2993
3051
|
if (buttonUxSchema.inferred_answers.VISUAL_REGRESSION_REQUIRED !== 'yes_if_available') throw new Error('selftest failed: visual regression default inference missing');
|
|
3052
|
+
const predictableAuthCliSchema = buildQuestionSchema('회전 아스키 아트는 제일 처음 인증 안됐을때만 codex cli처럼 애니메이션으로 보이게 하고 tmux에서는 정적 3d 아스키 아트로 보여줘');
|
|
3053
|
+
const predictableAuthCliSlotIds = predictableAuthCliSchema.slots.map((s) => s.id);
|
|
3054
|
+
if (predictableAuthCliSlotIds.length) throw new Error(`selftest failed: clear auth-worded CLI rendering work should auto-seal, got ${predictableAuthCliSlotIds.join(',')}`);
|
|
3055
|
+
if (!predictableAuthCliSchema.inferred_answers.RISK_BOUNDARY?.includes('no destructive commands or live data writes')) throw new Error('selftest failed: predictable auth-worded CLI work did not infer conservative risk boundary');
|
|
2994
3056
|
const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
|
|
2995
3057
|
const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
|
|
2996
3058
|
if (!vagueSlotIds.includes('INTENT_TARGET') || vagueSlotIds.includes('GOAL_PRECISE') || vagueSlotIds.includes('ACCEPTANCE_CRITERIA')) throw new Error(`selftest failed: vague work should ask dynamic intent questions only, got ${vagueSlotIds.join(',')}`);
|
|
@@ -3009,6 +3071,7 @@ async function selftest() {
|
|
|
3009
3071
|
if (!pptSkillText.includes('simple, restrained, and information-first') || !pptSkillText.includes('over-designed decoration') || !pptSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !pptSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !pptSkillText.includes('only design decision SSOT') || !pptSkillText.includes('instead of treating references as parallel authorities')) throw new Error('selftest failed: generated PPT skill missing restrained design/imagegen/fused-SSOT guidance');
|
|
3010
3072
|
if (!pptSkillText.includes('PPT pipeline allowlist') || !pptSkillText.includes('ignore installed skills and MCPs') || !pptSkillText.includes('prevent AI-like generic presentation design') || !pptSkillText.includes('Do not use generic design skills such as design-artifact-expert')) throw new Error('selftest failed: generated PPT skill missing pipeline allowlist enforcement');
|
|
3011
3073
|
if (!pptSkillText.includes('source-html/') || !pptSkillText.includes('temporary build files') || !pptSkillText.includes('ppt-parallel-report.json')) throw new Error('selftest failed: generated PPT skill missing source preservation/temp cleanup/parallel guidance');
|
|
3074
|
+
if (!pptSkillText.includes('ppt-fact-ledger.json') || !pptSkillText.includes('ppt-image-asset-ledger.json') || !pptSkillText.includes('OpenAI Image API') || !pptSkillText.includes('ppt-review-ledger.json') || !pptSkillText.includes('ppt-iteration-report.json') || !pptSkillText.includes('never simulate missing gpt-image-2 output')) throw new Error('selftest failed: generated PPT skill missing fact/image/review loop anti-fake guidance');
|
|
3012
3075
|
if (routeRequiresSubagents(pptRoute, '$PPT 투자자용 피치덱 만들어줘')) throw new Error('selftest failed: PPT route should not require subagents by default');
|
|
3013
3076
|
if (!reflectionRequiredForRoute(pptRoute)) throw new Error('selftest failed: PPT route should require reflection');
|
|
3014
3077
|
const pptMission = await createMission(tmp, { mode: 'ppt', prompt: '$PPT 투자자용 피치덱 만들어줘' });
|
|
@@ -3028,10 +3091,20 @@ async function selftest() {
|
|
|
3028
3091
|
if (!pptAudienceStrategy?.source_answers?.PRESENTATION_STP_STRATEGY || pptAudienceStrategy.painpoint_solution_map.length !== 3) throw new Error('selftest failed: PPT audience strategy was not materialized from sealed answers');
|
|
3029
3092
|
const pptGate = await readJson(path.join(pptMission.dir, PPT_GATE_ARTIFACT));
|
|
3030
3093
|
if (pptGate.passed !== false || pptGate.audience_strategy_sealed !== true || pptGate.painpoint_count !== 3) throw new Error('selftest failed: PPT gate did not initialize with sealed audience strategy');
|
|
3094
|
+
await writeJsonAtomic(path.join(pptMission.dir, PPT_FACT_LEDGER_ARTIFACT), {
|
|
3095
|
+
schema_version: 1,
|
|
3096
|
+
web_research_performed: true,
|
|
3097
|
+
external_research_required: true,
|
|
3098
|
+
sources: [{ id: 'web-source-selftest', type: 'verified_web_source', url: 'https://example.com/ppt-source', support_status: 'verified' }],
|
|
3099
|
+
claims: [{ id: 'claim-selftest-market-risk', text: '시장 차별성과 실행 리스크는 외부 근거가 필요한 주장으로 분리된다.', source_ids: ['web-source-selftest'], support_status: 'supported', criticality: 'high', slide_refs: [2] }],
|
|
3100
|
+
unsupported_critical_claims: [],
|
|
3101
|
+
unsupported_critical_claims_count: 0,
|
|
3102
|
+
passed: true
|
|
3103
|
+
});
|
|
3031
3104
|
const pptBuildResult = await runProcess(process.execPath, [hookBin, 'ppt', 'build', pptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3032
3105
|
if (pptBuildResult.code !== 0) throw new Error(`selftest failed: sks ppt build failed: ${pptBuildResult.stderr || pptBuildResult.stdout}`);
|
|
3033
3106
|
const pptBuild = JSON.parse(pptBuildResult.stdout);
|
|
3034
|
-
if (!pptBuild.ok || !pptBuild.gate?.passed || !pptBuild.gate?.parallel_build_recorded || !pptBuild.gate?.html_artifact_created || !pptBuild.gate?.source_html_preserved || !pptBuild.gate?.pdf_exported_or_explicitly_deferred || !pptBuild.gate?.render_qa_recorded || !pptBuild.gate?.temp_cleanup_recorded) throw new Error('selftest failed: PPT build did not pass artifact gate');
|
|
3107
|
+
if (!pptBuild.ok || !pptBuild.gate?.passed || !pptBuild.gate?.fact_ledger_created || !pptBuild.gate?.unsupported_critical_claims_zero || !pptBuild.gate?.image_asset_ledger_created || !pptBuild.gate?.image_asset_policy_satisfied || !pptBuild.gate?.review_policy_created || !pptBuild.gate?.review_ledger_created || !pptBuild.gate?.bounded_iteration_complete || !pptBuild.gate?.critical_review_issues_zero || !pptBuild.gate?.parallel_build_recorded || !pptBuild.gate?.html_artifact_created || !pptBuild.gate?.source_html_preserved || !pptBuild.gate?.pdf_exported_or_explicitly_deferred || !pptBuild.gate?.render_qa_recorded || !pptBuild.gate?.temp_cleanup_recorded) throw new Error('selftest failed: PPT build did not pass artifact gate');
|
|
3035
3108
|
if (!PPT_HTML_ARTIFACT.startsWith(`${PPT_SOURCE_HTML_DIR}/`)) throw new Error('selftest failed: PPT HTML source must be stored in source-html folder');
|
|
3036
3109
|
const pptHtml = await safeReadText(path.join(pptMission.dir, PPT_HTML_ARTIFACT));
|
|
3037
3110
|
if (!pptHtml.includes('<html') || pptHtml.includes('gradient')) throw new Error('selftest failed: PPT HTML artifact missing or over-designed');
|
|
@@ -3042,8 +3115,19 @@ async function selftest() {
|
|
|
3042
3115
|
const audienceScript = pptHtml.match(/id="ppt-audience-strategy">([^<]+)<\/script>/);
|
|
3043
3116
|
if (!audienceScript) throw new Error('selftest failed: PPT HTML missing audience strategy script data');
|
|
3044
3117
|
JSON.parse(audienceScript[1]);
|
|
3118
|
+
if (!pptHtml.includes('id="ppt-fact-ledger"') || !pptHtml.includes('id="ppt-image-asset-ledger"') || !pptHtml.includes('id="ppt-review-policy"')) throw new Error('selftest failed: PPT HTML missing fact/image/review embedded ledgers');
|
|
3045
3119
|
const pptPdfBytes = await fsp.readFile(path.join(pptMission.dir, PPT_PDF_ARTIFACT));
|
|
3046
3120
|
if (pptPdfBytes.subarray(0, 5).toString('utf8') !== '%PDF-') throw new Error('selftest failed: PPT PDF artifact does not have a PDF header');
|
|
3121
|
+
const pptFactLedger = await readJson(path.join(pptMission.dir, PPT_FACT_LEDGER_ARTIFACT));
|
|
3122
|
+
if (!pptFactLedger.passed || pptFactLedger.unsupported_critical_claims_count !== 0 || !Array.isArray(pptFactLedger.claims)) throw new Error('selftest failed: PPT fact ledger did not pass unsupported-claim gate');
|
|
3123
|
+
const pptImageAssetLedger = await readJson(path.join(pptMission.dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT));
|
|
3124
|
+
if (!pptImageAssetLedger.passed || pptImageAssetLedger.required !== false || pptImageAssetLedger.planned_count !== 0 || pptImageAssetLedger.provider?.model !== 'gpt-image-2') throw new Error('selftest failed: PPT image asset ledger did not pass optional no-cost state');
|
|
3125
|
+
const pptReviewPolicy = await readJson(path.join(pptMission.dir, PPT_REVIEW_POLICY_ARTIFACT));
|
|
3126
|
+
if (pptReviewPolicy.visual_review?.model !== 'gpt-image-2' || pptReviewPolicy.max_full_deck_passes !== 2 || pptReviewPolicy.max_slide_retries !== 2 || pptReviewPolicy.score_threshold < 0.88) throw new Error('selftest failed: PPT review policy missing bounded gpt-image-2 loop settings');
|
|
3127
|
+
const pptReviewLedger = await readJson(path.join(pptMission.dir, PPT_REVIEW_LEDGER_ARTIFACT));
|
|
3128
|
+
if (!pptReviewLedger.passed || !pptReviewLedger.p0_p1_zero || pptReviewLedger.image_review_status !== 'not_required_or_not_available') throw new Error('selftest failed: PPT review ledger did not pass deterministic no-blocker state');
|
|
3129
|
+
const pptIterationReport = await readJson(path.join(pptMission.dir, PPT_ITERATION_REPORT_ARTIFACT));
|
|
3130
|
+
if (!pptIterationReport.passed || pptIterationReport.loop_policy?.max_full_deck_passes !== 2 || pptIterationReport.stop_reason !== 'score_threshold_met_and_no_p0_p1_issues') throw new Error('selftest failed: PPT iteration report did not record bounded pass termination');
|
|
3047
3131
|
const pptRenderReport = await readJson(path.join(pptMission.dir, PPT_RENDER_REPORT_ARTIFACT));
|
|
3048
3132
|
if (!pptRenderReport.passed || !pptRenderReport.design_policy_checks.every((check) => check.passed)) throw new Error('selftest failed: PPT render report did not pass design policy checks');
|
|
3049
3133
|
const pptParallelReport = await readJson(path.join(pptMission.dir, PPT_PARALLEL_REPORT_ARTIFACT));
|
|
@@ -3054,6 +3138,31 @@ async function selftest() {
|
|
|
3054
3138
|
if (await exists(path.join(pptMission.dir, 'artifact.html'))) throw new Error('selftest failed: legacy root PPT HTML should not remain after source-html preservation');
|
|
3055
3139
|
const pptStatusResult = await runProcess(process.execPath, [hookBin, 'ppt', 'status', pptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3056
3140
|
if (pptStatusResult.code !== 0 || !JSON.parse(pptStatusResult.stdout).ok) throw new Error('selftest failed: sks ppt status did not report the built gate');
|
|
3141
|
+
const requiredImagePptMission = await createMission(tmp, { mode: 'ppt', prompt: '$PPT 이미지 리소스 포함 투자자용 피치덱 만들어줘' });
|
|
3142
|
+
await writeQuestions(requiredImagePptMission.dir, pptSchema);
|
|
3143
|
+
await writeJsonAtomic(path.join(requiredImagePptMission.dir, 'answers.json'), {
|
|
3144
|
+
...pptAnswers,
|
|
3145
|
+
PRESENTATION_IMAGE_ASSETS_REQUIRED: 'yes',
|
|
3146
|
+
PRESENTATION_IMAGE_ASSET_REQUESTS: ['한국 B2B SaaS 운영 효율을 상징하는 첫 장용 히어로 이미지']
|
|
3147
|
+
});
|
|
3148
|
+
const requiredImageSeal = await sealContract(requiredImagePptMission.dir, requiredImagePptMission.mission);
|
|
3149
|
+
if (!requiredImageSeal.ok) throw new Error('selftest failed: PPT required-image answers rejected');
|
|
3150
|
+
await materializeAfterPipelineAnswer(tmp, requiredImagePptMission.id, requiredImagePptMission.dir, requiredImagePptMission.mission, pptRoute, { route: 'PPT', command: '$PPT', mode: 'PPT', task: requiredImagePptMission.mission.prompt, context7_required: false }, requiredImageSeal.contract);
|
|
3151
|
+
await writeJsonAtomic(path.join(requiredImagePptMission.dir, PPT_FACT_LEDGER_ARTIFACT), {
|
|
3152
|
+
schema_version: 1,
|
|
3153
|
+
web_research_performed: true,
|
|
3154
|
+
external_research_required: true,
|
|
3155
|
+
sources: [{ id: 'web-source-required-image-selftest', type: 'verified_web_source', url: 'https://example.com/ppt-source-image', support_status: 'verified' }],
|
|
3156
|
+
claims: [{ id: 'claim-required-image-selftest', text: '이미지 리소스 요구사항은 사실 검증과 별도 게이트로 차단되어야 한다.', source_ids: ['web-source-required-image-selftest'], support_status: 'supported', criticality: 'high', slide_refs: [1] }],
|
|
3157
|
+
unsupported_critical_claims: [],
|
|
3158
|
+
unsupported_critical_claims_count: 0,
|
|
3159
|
+
passed: true
|
|
3160
|
+
});
|
|
3161
|
+
const requiredImageBuildResult = await runProcess(process.execPath, [hookBin, 'ppt', 'build', requiredImagePptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1', OPENAI_API_KEY: '' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3162
|
+
if (requiredImageBuildResult.code !== 0) throw new Error(`selftest failed: required-image PPT build command failed: ${requiredImageBuildResult.stderr || requiredImageBuildResult.stdout}`);
|
|
3163
|
+
const requiredImageBuild = JSON.parse(requiredImageBuildResult.stdout);
|
|
3164
|
+
const requiredImageLedger = await readJson(path.join(requiredImagePptMission.dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT));
|
|
3165
|
+
if (requiredImageBuild.ok || requiredImageBuild.gate?.passed || !requiredImageBuild.gate?.image_asset_ledger_created || requiredImageBuild.gate?.image_asset_policy_satisfied !== false || !requiredImageLedger.required || requiredImageLedger.passed || !requiredImageLedger.blockers?.includes('missing_OPENAI_API_KEY_for_required_gpt_image_2_assets') || requiredImageLedger.generated_count !== 0) throw new Error('selftest failed: required PPT image assets were not blocked without real gpt-image-2 credentials');
|
|
3057
3166
|
const installUxSchema = buildQuestionSchema('SKS first install/bootstrap UX and Context7 MCP setup improvement');
|
|
3058
3167
|
const installUxSlotIds = installUxSchema.slots.map((s) => s.id);
|
|
3059
3168
|
if (installUxSchema.domain_hints.includes('uiux') || installUxSlotIds.includes('VISUAL_REGRESSION_REQUIRED')) throw new Error('selftest failed: CLI UX install prompt should not ask visual UI questions');
|