sneakoscope 0.7.45 → 0.7.46
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 +2 -1
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +27 -2
- package/src/cli/main.mjs +86 -16
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +1 -1
- package/src/core/init.mjs +38 -3
- package/src/core/pipeline.mjs +91 -27
- package/src/core/questions.mjs +6 -5
package/README.md
CHANGED
|
@@ -183,10 +183,11 @@ 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. SKS keeps Codex App Fast mode
|
|
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 visible and defaulted by writing `service_tier = "fast"`, `[features].fast_mode = true`, and the `sks-fast-high` profile while removing only legacy top-level `model` and `model_reasoning_effort` locks; 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"
|
|
190
|
+
service_tier = "fast"
|
|
190
191
|
|
|
191
192
|
[model_providers.codex-lb]
|
|
192
193
|
name = "OpenAI"
|
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.46",
|
|
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",
|
|
@@ -248,17 +248,25 @@ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
|
|
|
248
248
|
|
|
249
249
|
export function normalizeCodexFastModeUiConfig(text = '') {
|
|
250
250
|
let next = removeLegacyTopLevelCodexModeLocks(text);
|
|
251
|
+
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
252
|
+
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
253
|
+
next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
|
|
251
254
|
next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
|
|
252
255
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
|
|
253
256
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
|
|
257
|
+
next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
|
|
258
|
+
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
|
|
259
|
+
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
|
|
260
|
+
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
|
|
261
|
+
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'sandbox_mode = "workspace-write"');
|
|
262
|
+
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model_reasoning_effort = "high"');
|
|
254
263
|
return ensureTrailingNewline(next);
|
|
255
264
|
}
|
|
256
265
|
|
|
257
266
|
function removeLegacyTopLevelCodexModeLocks(text = '') {
|
|
258
267
|
const legacy = {
|
|
259
268
|
model: new Set(['gpt-5.5']),
|
|
260
|
-
model_reasoning_effort: new Set(['high'])
|
|
261
|
-
service_tier: new Set(['fast'])
|
|
269
|
+
model_reasoning_effort: new Set(['high'])
|
|
262
270
|
};
|
|
263
271
|
const lines = String(text || '').split('\n');
|
|
264
272
|
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
@@ -271,6 +279,23 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
|
|
|
271
279
|
}).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
272
280
|
}
|
|
273
281
|
|
|
282
|
+
function removeTomlTableKey(text, table, key) {
|
|
283
|
+
const lines = String(text || '').trimEnd().split('\n');
|
|
284
|
+
if (lines.length === 1 && lines[0] === '') return '';
|
|
285
|
+
const header = `[${table}]`;
|
|
286
|
+
const start = lines.findIndex((x) => x.trim() === header);
|
|
287
|
+
if (start === -1) return String(text || '');
|
|
288
|
+
let end = lines.length;
|
|
289
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
290
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
291
|
+
end = i;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
|
|
296
|
+
return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
274
299
|
function upsertTomlTableKey(text, table, line) {
|
|
275
300
|
const key = String(line).split('=')[0].trim();
|
|
276
301
|
const lines = String(text || '').trimEnd().split('\n');
|
package/src/cli/main.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import fsp from 'node:fs/promises';
|
|
4
4
|
import readline from 'node:readline/promises';
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
-
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot } from '../core/fsx.mjs';
|
|
6
|
+
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot, readStdin } from '../core/fsx.mjs';
|
|
7
7
|
import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
|
|
8
8
|
import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
9
9
|
import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
|
|
@@ -168,7 +168,7 @@ Usage:
|
|
|
168
168
|
sks ppt status <mission-id|latest> [--json]
|
|
169
169
|
sks context7 check|setup|tools|resolve|docs|evidence ...
|
|
170
170
|
sks pipeline status|resume|plan [--json] [--proof-field]
|
|
171
|
-
sks pipeline answer <mission-id|latest> <answers.json>
|
|
171
|
+
sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">
|
|
172
172
|
sks guard check [--json]
|
|
173
173
|
sks conflicts check|prompt [--json]
|
|
174
174
|
sks versioning status|bump|pre-commit [--json]
|
|
@@ -591,9 +591,14 @@ function printPipelinePlan(root, id, plan) {
|
|
|
591
591
|
async function pipelineAnswer(root, args = []) {
|
|
592
592
|
const [missionArg, answerFile] = args;
|
|
593
593
|
const id = await resolveMissionId(root, missionArg);
|
|
594
|
-
if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json>');
|
|
594
|
+
if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">');
|
|
595
595
|
const { dir, mission } = await loadMission(root, id);
|
|
596
|
-
const
|
|
596
|
+
const schema = await readJson(path.join(dir, 'required-answers.schema.json'));
|
|
597
|
+
const answers = answerFile === '--stdin'
|
|
598
|
+
? parseAnswersText(schema, await readStdin())
|
|
599
|
+
: answerFile === '--text'
|
|
600
|
+
? parseAnswersText(schema, args.slice(2).join(' '))
|
|
601
|
+
: await readJson(path.resolve(answerFile));
|
|
597
602
|
await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
|
|
598
603
|
const result = await sealContract(dir, mission);
|
|
599
604
|
if (!result.ok) {
|
|
@@ -645,6 +650,48 @@ async function pipelineAnswer(root, args = []) {
|
|
|
645
650
|
console.log('Next: continue the original route lifecycle using decision-contract.json.');
|
|
646
651
|
}
|
|
647
652
|
|
|
653
|
+
function parseAnswersText(schema = {}, text = '') {
|
|
654
|
+
const body = String(text || '').trim();
|
|
655
|
+
const slots = Array.isArray(schema.slots) ? schema.slots : [];
|
|
656
|
+
const slotById = new Map(slots.map((slot) => [slot.id, slot]));
|
|
657
|
+
const answers = {};
|
|
658
|
+
let currentId = null;
|
|
659
|
+
let currentLines = [];
|
|
660
|
+
const flush = () => {
|
|
661
|
+
if (!currentId) return;
|
|
662
|
+
answers[currentId] = normalizeTextAnswerValue(slotById.get(currentId), currentLines.join('\n').trim());
|
|
663
|
+
currentId = null;
|
|
664
|
+
currentLines = [];
|
|
665
|
+
};
|
|
666
|
+
for (const line of body.split(/\r?\n/)) {
|
|
667
|
+
const match = line.match(/^\s*([A-Z][A-Z0-9_]{2,})\s*[::]\s*(.*)$/);
|
|
668
|
+
if (match && slotById.has(match[1])) {
|
|
669
|
+
flush();
|
|
670
|
+
currentId = match[1];
|
|
671
|
+
currentLines = [match[2] || ''];
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (currentId) currentLines.push(line);
|
|
675
|
+
}
|
|
676
|
+
flush();
|
|
677
|
+
if (!Object.keys(answers).length && slots.length === 1 && body) {
|
|
678
|
+
answers[slots[0].id] = normalizeTextAnswerValue(slots[0], body.replace(new RegExp(`^\\s*${slots[0].id}\\s*`, 'i'), '').trim());
|
|
679
|
+
}
|
|
680
|
+
return answers;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function normalizeTextAnswerValue(slot = {}, raw = '') {
|
|
684
|
+
const value = String(raw || '').trim();
|
|
685
|
+
if (slot.type === 'array') {
|
|
686
|
+
return value.split(/\r?\n|,/).map((x) => x.replace(/^\s*[-*]\s*/, '').trim()).filter(Boolean);
|
|
687
|
+
}
|
|
688
|
+
if (slot.type === 'array_or_string') {
|
|
689
|
+
const bulletLines = value.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
690
|
+
if (bulletLines.length > 1 && bulletLines.every((line) => /^[-*]\s+/.test(line))) return bulletLines.map((line) => line.replace(/^[-*]\s+/, '').trim());
|
|
691
|
+
}
|
|
692
|
+
return value;
|
|
693
|
+
}
|
|
694
|
+
|
|
648
695
|
async function materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext = {}, contract = {}) {
|
|
649
696
|
const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, contract);
|
|
650
697
|
if (route?.id === 'MadSKS') {
|
|
@@ -1781,7 +1828,7 @@ function hasTopLevelCodexModeLock(text = '') {
|
|
|
1781
1828
|
const lines = String(text || '').split('\n');
|
|
1782
1829
|
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1783
1830
|
const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
|
|
1784
|
-
return /^model\s*=|^model_reasoning_effort\s
|
|
1831
|
+
return /^model\s*=|^model_reasoning_effort\s*=/m.test(top);
|
|
1785
1832
|
}
|
|
1786
1833
|
|
|
1787
1834
|
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
@@ -1847,7 +1894,7 @@ async function selftest() {
|
|
|
1847
1894
|
if (stop?.decision !== 'block' || !String(stop.reason || '').includes('waiting for mandatory ambiguity-removal answers')) throw new Error('selftest failed: clarification gate did not hard-pause without visible questions');
|
|
1848
1895
|
}
|
|
1849
1896
|
if (await exists(path.join(clarificationMission.dir, 'hard-blocker.json'))) throw new Error('selftest failed: clarification gate used compliance hard-blocker instead of waiting for answers');
|
|
1850
|
-
const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions still pending:\n1. GOAL_PRECISE: What should be changed?\n\
|
|
1897
|
+
const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions still pending:\n1. GOAL_PRECISE: What should be changed?\n\nReply by slot id; I will seal the contract with sks pipeline answer latest --stdin.' });
|
|
1851
1898
|
if (visibleQuestionStop?.continue !== true) throw new Error('selftest failed: visible clarification question block did not allow the question-only turn to stop');
|
|
1852
1899
|
await setCurrent(tmp, loopState);
|
|
1853
1900
|
const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
|
|
@@ -2027,7 +2074,7 @@ async function selftest() {
|
|
|
2027
2074
|
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');
|
|
2028
2075
|
const codexLbHome = path.join(tmp, 'codex-lb-home');
|
|
2029
2076
|
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');
|
|
2077
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n');
|
|
2031
2078
|
const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
|
|
2032
2079
|
cwd: tmp,
|
|
2033
2080
|
env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
|
|
@@ -2040,7 +2087,7 @@ async function selftest() {
|
|
|
2040
2087
|
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
2041
2088
|
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
2042
2089
|
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
|
|
2090
|
+
if (!codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode defaults');
|
|
2044
2091
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
2045
2092
|
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
2046
2093
|
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');
|
|
@@ -2295,6 +2342,23 @@ async function selftest() {
|
|
|
2295
2342
|
if (routePrompt('$MAD-SKS Supabase MCP main 작업')?.id !== 'MadSKS') throw new Error('selftest failed: $MAD-SKS route did not resolve');
|
|
2296
2343
|
if (routePrompt('$MAD-SKS $Team Supabase MCP main 작업')?.id !== 'Team') throw new Error('selftest failed: $MAD-SKS did not compose with $Team');
|
|
2297
2344
|
if (routePrompt('$DB Supabase 점검 $MAD-SKS')?.id !== 'DB') throw new Error('selftest failed: trailing $MAD-SKS changed primary route');
|
|
2345
|
+
const madStandaloneTmp = tmpdir();
|
|
2346
|
+
await initProject(madStandaloneTmp, {});
|
|
2347
|
+
const madStandalonePayload = JSON.stringify({ cwd: madStandaloneTmp, prompt: '$MAD-SKS main 권한 열어줘' });
|
|
2348
|
+
const madStandaloneResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madStandaloneTmp, input: madStandalonePayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2349
|
+
if (madStandaloneResult.code !== 0) throw new Error(`selftest failed: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
|
|
2350
|
+
const madStandaloneState = await readJson(stateFile(madStandaloneTmp), {});
|
|
2351
|
+
if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json') throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate scoped permissions');
|
|
2352
|
+
const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
|
|
2353
|
+
const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
|
|
2354
|
+
if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest failed: standalone MAD-SKS did not allow ordinary DDL');
|
|
2355
|
+
const madModifierTmp = tmpdir();
|
|
2356
|
+
await initProject(madModifierTmp, {});
|
|
2357
|
+
const madModifierPayload = JSON.stringify({ cwd: madModifierTmp, prompt: '$MAD-SKS $Team 회전 아스키 아트는 제일 처음 인증 안됐을때만 codex cli처럼 애니메이션으로 보이게 하고 tmux에서는 정적 3d 아스키 아트로 보여줘' });
|
|
2358
|
+
const madModifierResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madModifierTmp, input: madModifierPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2359
|
+
if (madModifierResult.code !== 0) throw new Error(`selftest failed: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
|
|
2360
|
+
const madModifierState = await readJson(stateFile(madModifierTmp), {});
|
|
2361
|
+
if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json') throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate scoped permissions');
|
|
2298
2362
|
if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
|
|
2299
2363
|
const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
|
|
2300
2364
|
if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
|
|
@@ -2499,7 +2563,8 @@ async function selftest() {
|
|
|
2499
2563
|
const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
|
|
2500
2564
|
const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
|
|
2501
2565
|
if (hookTeamPendingState.mission_id !== hookTeamState.mission_id) throw new Error('selftest failed: pending clarification allowed a new route mission to replace the visible question sheet');
|
|
2502
|
-
if (!hookTeamPendingContext.includes('Required questions still pending') || !hookTeamPendingContext.includes('
|
|
2566
|
+
if (!hookTeamPendingContext.includes('Required questions still pending') || !hookTeamPendingContext.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: pending clarification did not re-expose the question sheet');
|
|
2567
|
+
if (hookTeamPendingContext.includes('VISIBLE RESPONSE CONTRACT') || hookTeamPendingContext.includes('Codex plan-tool interaction')) throw new Error('selftest failed: pending clarification reprinted verbose guidance instead of a compact retry');
|
|
2503
2568
|
if (hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: pending clarification prepared a new ambiguity gate instead of reusing the active one');
|
|
2504
2569
|
const hookTeamStopResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, last_assistant_message: 'I need three decisions before implementation, but I will not paste the Required questions block.' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2505
2570
|
if (hookTeamStopResult.code !== 0) throw new Error(`selftest failed: Team stop hook exited ${hookTeamStopResult.code}: ${hookTeamStopResult.stderr}`);
|
|
@@ -2508,13 +2573,12 @@ async function selftest() {
|
|
|
2508
2573
|
if (!String(hookTeamStopJson.reason || '').includes('Required questions') || !String(hookTeamStopJson.reason || '').includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: missing Team stop presentation question');
|
|
2509
2574
|
if (String(hookTeamStopJson.reason || '').includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team stop goal');
|
|
2510
2575
|
if (!String(hookTeamStopJson.reason || '').includes('sks pipeline answer')) throw new Error('selftest failed: Stop hook did not provide pipeline answer command');
|
|
2511
|
-
if (
|
|
2512
|
-
if (!String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook did not force visible clarification response');
|
|
2576
|
+
if (String(hookTeamStopJson.reason || '').includes('Codex plan-tool interaction') || String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook reprinted verbose clarification guidance');
|
|
2513
2577
|
const hookTeamSchema = await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'required-answers.schema.json'));
|
|
2514
2578
|
const visibleQuestionsBlock = [
|
|
2515
2579
|
'Required questions',
|
|
2516
2580
|
...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
|
|
2517
|
-
'Reply by slot id, then I will
|
|
2581
|
+
'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
|
|
2518
2582
|
].join('\n');
|
|
2519
2583
|
const visibleQuestionDecision = await evaluateStop(hookTeamTmp, hookTeamState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
|
|
2520
2584
|
if (!visibleQuestionDecision?.continue) throw new Error('selftest failed: visible Required questions block was not accepted by clarification stop gate');
|
|
@@ -2522,13 +2586,17 @@ async function selftest() {
|
|
|
2522
2586
|
if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
|
|
2523
2587
|
const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
|
|
2524
2588
|
if (hookTeamPreToolBlockedJson.decision !== 'block' || !String(hookTeamPreToolBlockedJson.reason || '').includes('ambiguity gate is paused')) throw new Error('selftest failed: pending clarification allowed implementation tool use before answers');
|
|
2525
|
-
const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, command: 'node ./bin/sks.mjs pipeline answer latest
|
|
2589
|
+
const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, command: 'node ./bin/sks.mjs pipeline answer latest --stdin' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2526
2590
|
if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
|
|
2527
2591
|
const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
|
|
2528
2592
|
if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
|
|
2529
2593
|
const nonGoalsSlot = hookTeamSchema.slots.find((s) => s.id === 'NON_GOALS');
|
|
2530
2594
|
if (nonGoalsSlot && !nonGoalsSlot.allow_empty) throw new Error('selftest failed: NON_GOALS does not allow an empty array answer');
|
|
2531
2595
|
if (!nonGoalsSlot && !Array.isArray(hookTeamSchema.inferred_answers?.NON_GOALS)) throw new Error('selftest failed: NON_GOALS was neither asked nor inferred');
|
|
2596
|
+
const textParsedAnswers = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'INTENT_TARGET: compact contract sealing');
|
|
2597
|
+
if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
|
|
2598
|
+
const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
|
|
2599
|
+
if (textParsedImplicitAnswer.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not infer the only missing slot');
|
|
2532
2600
|
const hookTeamAnswers = {};
|
|
2533
2601
|
for (const s of hookTeamSchema.slots) hookTeamAnswers[s.id] = s.options ? (s.type === 'array' ? [s.options[0]] : s.options[0]) : (s.type.includes('array') ? ['selftest'] : (s.id === 'DB_MAX_BLAST_RADIUS' ? 'no_live_dml' : 'selftest'));
|
|
2534
2602
|
hookTeamAnswers.NON_GOALS = [];
|
|
@@ -2690,12 +2758,13 @@ async function selftest() {
|
|
|
2690
2758
|
if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
|
|
2691
2759
|
const preservedConfigTmp = tmpdir();
|
|
2692
2760
|
await ensureDir(path.join(preservedConfigTmp, '.codex'));
|
|
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');
|
|
2761
|
+
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
|
|
2694
2762
|
await initProject(preservedConfigTmp, {});
|
|
2695
2763
|
const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
|
|
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
|
|
2764
|
+
if (!preservedConfig.includes('service_tier = "fast"') || !preservedConfig.includes('fast_mode = true') || !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"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(preservedConfig)) throw new Error('selftest failed: Codex config merge dropped or failed to enable Fast mode defaults');
|
|
2765
|
+
if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest failed: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
|
|
2697
2766
|
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
|
|
2767
|
+
if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
|
|
2699
2768
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
2700
2769
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
2701
2770
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
|
@@ -3056,6 +3125,7 @@ async function selftest() {
|
|
|
3056
3125
|
const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
|
|
3057
3126
|
const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
|
|
3058
3127
|
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(',')}`);
|
|
3128
|
+
if (vagueSlotIds.length !== 1) throw new Error(`selftest failed: vague work should ask only the execution-changing intent question, got ${vagueSlotIds.join(',')}`);
|
|
3059
3129
|
if (vagueSchema.ambiguity_assessment?.method !== 'weighted_clarity_interview' || !vagueSchema.ambiguity_assessment?.adversarial_lenses?.includes('challenge_framing')) throw new Error('selftest failed: ambiguity schema missing weighted clarity / planning lenses');
|
|
3060
3130
|
const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
|
|
3061
3131
|
if (pptRoute?.id !== 'PPT') throw new Error('selftest failed: $PPT did not route to presentation pipeline');
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.7.
|
|
8
|
+
export const PACKAGE_VERSION = '0.7.46';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -270,7 +270,7 @@ function payloadMentionsAnswersJson(payload = {}) {
|
|
|
270
270
|
function clarificationPauseBlockReason(state = {}) {
|
|
271
271
|
const id = state?.mission_id || 'latest';
|
|
272
272
|
const route = state.route_command || state.route || state.mode || 'route';
|
|
273
|
-
return `SKS ${route} ambiguity gate is paused and waiting for explicit user answers. Do not run implementation, tests, route materialization, or unrelated tools yet. The only allowed
|
|
273
|
+
return `SKS ${route} ambiguity gate is paused and waiting for explicit user answers. Do not run implementation, tests, route materialization, or unrelated tools yet. The only allowed action is sealing the user's reply with "sks pipeline answer ${id} --stdin"; elapsed time or repeated hook resumes never count as answers.`;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
async function hookStop(root, state, payload, noQuestion) {
|
package/src/core/init.mjs
CHANGED
|
@@ -428,8 +428,11 @@ function installPolicy(scope, commandPrefix) {
|
|
|
428
428
|
|
|
429
429
|
function mergeManagedCodexConfigToml(existingContent = '') {
|
|
430
430
|
let next = removeLegacyTopLevelCodexModeLocks(String(existingContent || '').trimEnd());
|
|
431
|
+
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
432
|
+
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
431
433
|
next = upsertTomlTableKey(next, 'features', 'codex_hooks = true');
|
|
432
434
|
next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
|
|
435
|
+
next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
|
|
433
436
|
next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
|
|
434
437
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
|
|
435
438
|
next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
|
|
@@ -445,8 +448,7 @@ function mergeManagedCodexConfigToml(existingContent = '') {
|
|
|
445
448
|
function removeLegacyTopLevelCodexModeLocks(text = '') {
|
|
446
449
|
const legacy = {
|
|
447
450
|
model: new Set(['gpt-5.5']),
|
|
448
|
-
model_reasoning_effort: new Set(['high'])
|
|
449
|
-
service_tier: new Set(['fast'])
|
|
451
|
+
model_reasoning_effort: new Set(['high'])
|
|
450
452
|
};
|
|
451
453
|
const lines = String(text || '').split('\n');
|
|
452
454
|
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
@@ -459,6 +461,38 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
|
|
|
459
461
|
}).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
460
462
|
}
|
|
461
463
|
|
|
464
|
+
function upsertTopLevelTomlString(text, key, value) {
|
|
465
|
+
const line = `${key} = "${value}"`;
|
|
466
|
+
const lines = String(text || '').split('\n');
|
|
467
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
468
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
469
|
+
for (let i = 0; i < end; i += 1) {
|
|
470
|
+
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
|
|
471
|
+
lines[i] = line;
|
|
472
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
lines.splice(end, 0, line);
|
|
476
|
+
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function removeTomlTableKey(text, table, key) {
|
|
480
|
+
const lines = String(text || '').trimEnd().split('\n');
|
|
481
|
+
if (lines.length === 1 && lines[0] === '') return '';
|
|
482
|
+
const header = `[${table}]`;
|
|
483
|
+
const start = lines.findIndex((x) => x.trim() === header);
|
|
484
|
+
if (start === -1) return String(text || '');
|
|
485
|
+
let end = lines.length;
|
|
486
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
487
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
488
|
+
end = i;
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
|
|
493
|
+
return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
|
|
494
|
+
}
|
|
495
|
+
|
|
462
496
|
function managedCodexConfigBlocks() {
|
|
463
497
|
return [
|
|
464
498
|
{ table: 'mcp_servers.context7', text: context7ConfigToml().trim() },
|
|
@@ -470,7 +504,7 @@ function managedCodexConfigBlocks() {
|
|
|
470
504
|
{ table: 'profiles.sks-task-low', text: profileConfigBlock('sks-task-low', 'low') },
|
|
471
505
|
{ table: 'profiles.sks-task-medium', text: profileConfigBlock('sks-task-medium', 'medium') },
|
|
472
506
|
{ table: 'profiles.sks-logic-high', text: profileConfigBlock('sks-logic-high', 'high') },
|
|
473
|
-
{ table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high') },
|
|
507
|
+
{ table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high', { serviceTier: 'fast' }) },
|
|
474
508
|
{ table: 'profiles.sks-research-xhigh', text: profileConfigBlock('sks-research-xhigh', 'xhigh') },
|
|
475
509
|
{ table: 'profiles.sks-research', text: profileConfigBlock('sks-research', 'xhigh', { approval: 'never' }) },
|
|
476
510
|
{ table: 'profiles.sks-team', text: profileConfigBlock('sks-team', 'high') },
|
|
@@ -496,6 +530,7 @@ function profileConfigBlock(profile, effort, opts = {}) {
|
|
|
496
530
|
return [
|
|
497
531
|
`[profiles.${profile}]`,
|
|
498
532
|
'model = "gpt-5.5"',
|
|
533
|
+
...(opts.serviceTier ? [`service_tier = "${opts.serviceTier}"`] : []),
|
|
499
534
|
`approval_policy = "${opts.approval || 'on-request'}"`,
|
|
500
535
|
...(opts.approvalsReviewer ? [`approvals_reviewer = "${opts.approvalsReviewer}"`] : []),
|
|
501
536
|
`sandbox_mode = "${opts.sandbox || 'workspace-write'}"`,
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -235,7 +235,7 @@ function planVerification(route, proof) {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function planNextActions(route, task, ambiguity, lane) {
|
|
238
|
-
if (ambiguity.required && !ambiguity.passed) return ['ask
|
|
238
|
+
if (ambiguity.required && !ambiguity.passed) return ['ask only execution-changing ambiguity questions', 'seal the decision contract from the user reply'];
|
|
239
239
|
const actions = ['read pipeline-plan.json before work', 'execute kept stages only', 'run listed verification'];
|
|
240
240
|
if (!lane.fast_lane_allowed && routeRequiresSubagents(route, task)) actions.splice(1, 0, 'materialize full Team artifacts before implementation');
|
|
241
241
|
actions.push('refresh/validate TriWiki when required', 'finish with completion summary and Honest Mode');
|
|
@@ -255,8 +255,8 @@ export function promptPipelineContext(prompt, route = routePrompt(prompt)) {
|
|
|
255
255
|
'Before work, load the required SKS skill context and follow the route lifecycle instead of treating the command as plain text.',
|
|
256
256
|
'Codex App visibility: briefly surface what SKS is doing before tools run, mirror important worker/tool status to mission artifacts, and keep progress legible to the user.',
|
|
257
257
|
'Hook visibility limit: hooks can inject context/status or block/continue a turn, but they cannot create arbitrary live chat bubbles; use team events, mission files, or normal assistant updates for live transcript details.',
|
|
258
|
-
'Ambiguity gate:
|
|
259
|
-
'Plan-first interaction: when ambiguity questions are required,
|
|
258
|
+
'Ambiguity gate: execution routes must infer obvious contract answers first and ask only missing answers that can change target, scope, safety, behavior, or acceptance. DFix and Answer bypass this gate because they do not start implementation.',
|
|
259
|
+
'Plan-first interaction: when ambiguity questions are truly required, show the user only the missing human decision(s), then seal the decision contract internally and execute/verify.',
|
|
260
260
|
'Question-shaped directive policy: before using Answer, decide whether a question is a real information request or an implicit instruction/complaint about broken behavior. Rhetorical bug reports, mandatory-policy statements, and "why is this not happening?" execution complaints must route to Team, not Answer.',
|
|
261
261
|
'Best-practice prompt shape: extract Goal, Context, Constraints, and Done-when before implementation; keep questions compact and only ask for answers that can change scope, safety, user-facing behavior, or acceptance criteria.',
|
|
262
262
|
chatCaptureIntakeText(),
|
|
@@ -504,9 +504,16 @@ async function prepareClarificationGate(root, route, task, required, opts = {})
|
|
|
504
504
|
if (schema.slots.length === 0) {
|
|
505
505
|
await writeJsonAtomic(path.join(dir, 'answers.json'), schema.inferred_answers || {});
|
|
506
506
|
const result = await sealContract(dir, mission);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
507
|
+
let materialized = {};
|
|
508
|
+
if (result.ok && route?.id === 'Team') {
|
|
509
|
+
materialized = await materializeAutoSealedTeam(root, id, dir, route, task, result.contract?.sealed_hash || null);
|
|
510
|
+
if (opts.madSksAuthorization) {
|
|
511
|
+
const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, result.contract || {});
|
|
512
|
+
materialized = { ...materialized, state: { ...(materialized.state || {}), ...madSksState } };
|
|
513
|
+
}
|
|
514
|
+
} else if (result.ok && route?.id === 'MadSKS') {
|
|
515
|
+
materialized = await materializeAutoSealedMadSks(dir, id, route, routeContext, result.contract || {});
|
|
516
|
+
}
|
|
510
517
|
const effectiveTask = materialized.prompt || task;
|
|
511
518
|
const plan = await writePipelinePlan(dir, { missionId: id, route, task: effectiveTask, required, ambiguity: { required: true, slots: 0, auto_sealed: result.ok, passed: result.ok, contract_hash: result.contract?.sealed_hash || null } });
|
|
512
519
|
await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'route.clarification.auto_sealed', route: route.id, slots: 0, ok: result.ok });
|
|
@@ -542,7 +549,7 @@ Next atomic action: continue the original route lifecycle with the sealed decisi
|
|
|
542
549
|
await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'route.clarification.questions_created', route: route.id, slots: schema.slots.length });
|
|
543
550
|
const phase = `${route.mode}_CLARIFICATION_AWAITING_ANSWERS`;
|
|
544
551
|
await setCurrent(root, routeState(id, route, phase, required, { prompt: task, questions_allowed: true, implementation_allowed: false, clarification_required: true, ambiguity_gate_required: true, pipeline_plan_ready: validatePipelinePlan(plan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, original_stop_gate: route.stopGate, stop_gate: 'clarification-gate' }));
|
|
545
|
-
const answerCommand = 'sks pipeline answer latest
|
|
552
|
+
const answerCommand = 'sks pipeline answer latest --stdin';
|
|
546
553
|
const title = 'MANDATORY ambiguity-removal gate activated.';
|
|
547
554
|
return {
|
|
548
555
|
route,
|
|
@@ -555,7 +562,7 @@ Question file: .sneakoscope/missions/${id}/questions.md
|
|
|
555
562
|
Answer schema: .sneakoscope/missions/${id}/required-answers.schema.json
|
|
556
563
|
Pipeline plan: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}
|
|
557
564
|
|
|
558
|
-
Do not execute the route yet. Ask
|
|
565
|
+
Do not execute the route yet. Ask only the required ambiguity-removal questions now. After the user answers, seal the decision contract internally with "${answerCommand}".
|
|
559
566
|
${clarificationVisibleResponseContract(id)}
|
|
560
567
|
${context7RequirementText(required)}
|
|
561
568
|
${clarificationPlanHint(route, id)}
|
|
@@ -588,6 +595,74 @@ function applyMadSksAuthorizationToSchema(schema = {}) {
|
|
|
588
595
|
return schema;
|
|
589
596
|
}
|
|
590
597
|
|
|
598
|
+
async function materializeAutoSealedMadSks(dir, id, route, routeContext = {}, contract = {}) {
|
|
599
|
+
await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), {
|
|
600
|
+
schema_version: 1,
|
|
601
|
+
passed: false,
|
|
602
|
+
mad_sks_permission_active: true,
|
|
603
|
+
permissions_deactivated: false,
|
|
604
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
605
|
+
direct_execute_sql_allowed: true,
|
|
606
|
+
normal_db_writes_allowed: true,
|
|
607
|
+
catastrophic_safety_guard_active: true,
|
|
608
|
+
contract_hash: contract.sealed_hash || null
|
|
609
|
+
});
|
|
610
|
+
await appendJsonl(path.join(dir, 'events.jsonl'), {
|
|
611
|
+
ts: nowIso(),
|
|
612
|
+
type: 'mad_sks.scoped_permission_opened',
|
|
613
|
+
route: route?.id || 'MadSKS',
|
|
614
|
+
catastrophic_safety_guard_active: true
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
phase: 'MADSKS_SCOPED_PERMISSION_ACTIVE',
|
|
618
|
+
prompt: routeContext.task || '',
|
|
619
|
+
state: {
|
|
620
|
+
mad_sks_active: true,
|
|
621
|
+
mad_sks_modifier: true,
|
|
622
|
+
mad_sks_gate_file: 'mad-sks-gate.json',
|
|
623
|
+
mad_sks_gate_ready: true,
|
|
624
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
625
|
+
direct_execute_sql_allowed: true,
|
|
626
|
+
normal_db_writes_allowed: true,
|
|
627
|
+
catastrophic_safety_guard_active: true
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function materializeMadSksAuthorization(dir, id, route, routeContext = {}, contract = {}) {
|
|
633
|
+
if (!routeContext.mad_sks_authorization || route?.id === 'MadSKS') return {};
|
|
634
|
+
const gateFile = route?.stopGate || 'done-gate.json';
|
|
635
|
+
await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), {
|
|
636
|
+
schema_version: 1,
|
|
637
|
+
mission_id: id,
|
|
638
|
+
route: route?.command || route?.id || null,
|
|
639
|
+
status: 'active',
|
|
640
|
+
active_only_for_current_route: true,
|
|
641
|
+
deactivates_when_gate_passed: gateFile,
|
|
642
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
643
|
+
direct_execute_sql_allowed: true,
|
|
644
|
+
normal_db_writes_allowed: true,
|
|
645
|
+
catastrophic_safety_guard_active: true,
|
|
646
|
+
contract_hash: contract.sealed_hash || null
|
|
647
|
+
});
|
|
648
|
+
await appendJsonl(path.join(dir, 'events.jsonl'), {
|
|
649
|
+
ts: nowIso(),
|
|
650
|
+
type: 'mad_sks.modifier_authorization_opened',
|
|
651
|
+
route: route?.id || null,
|
|
652
|
+
gate: gateFile,
|
|
653
|
+
catastrophic_safety_guard_active: true
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
mad_sks_active: true,
|
|
657
|
+
mad_sks_modifier: true,
|
|
658
|
+
mad_sks_gate_file: gateFile,
|
|
659
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
660
|
+
direct_execute_sql_allowed: true,
|
|
661
|
+
normal_db_writes_allowed: true,
|
|
662
|
+
catastrophic_safety_guard_active: true
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
591
666
|
async function materializeAutoSealedTeam(root, id, dir, route, task, contractHash = null) {
|
|
592
667
|
const spec = parseTeamSpecText(task);
|
|
593
668
|
const cleanTask = spec.prompt || task;
|
|
@@ -809,11 +884,11 @@ async function clarificationAwaitingAnswersContext(root, state) {
|
|
|
809
884
|
const schema = await readJson(path.join(missionDir(root, id), 'required-answers.schema.json'), null);
|
|
810
885
|
const questionBlock = schema ? `\n\nRequired questions still pending:\n${formatRequiredQuestions(schema)}` : '';
|
|
811
886
|
const planNote = await activePipelinePlanNote(root, state);
|
|
812
|
-
return `Active SKS route ${state.route_command || state.route || state.mode} is
|
|
887
|
+
return `Active SKS route ${state.route_command || state.route || state.mode} is still paused for ambiguity answers. Keep this retry compact: if the user answered, seal the contract with "sks pipeline answer ${id} --stdin"; otherwise ask only the missing slot ids. Do not expose internal answer files to the user and do not execute the route before this gate passes.${planNote}${questionBlock}`;
|
|
813
888
|
}
|
|
814
889
|
|
|
815
890
|
function clarificationVisibleResponseContract(id) {
|
|
816
|
-
const answerCommand = `sks pipeline answer ${id}
|
|
891
|
+
const answerCommand = `sks pipeline answer ${id} --stdin`;
|
|
817
892
|
return `
|
|
818
893
|
|
|
819
894
|
VISIBLE RESPONSE CONTRACT:
|
|
@@ -821,17 +896,17 @@ VISIBLE RESPONSE CONTRACT:
|
|
|
821
896
|
- Do not call tools, do not start implementation, and do not advance to the next route phase.
|
|
822
897
|
- Elapsed time, repeated hook resumes, or assistant self-continuation do not count as answers.
|
|
823
898
|
- Reply to the user with the Required questions block so it is visible in chat.
|
|
824
|
-
- Tell the user they can answer directly by slot id; after they answer,
|
|
899
|
+
- Tell the user they can answer directly by slot id; after they answer, seal the contract internally with \`${answerCommand}\`.`;
|
|
825
900
|
}
|
|
826
901
|
|
|
827
902
|
function clarificationPlanHint(route, id) {
|
|
828
|
-
const command = `sks pipeline answer ${id}
|
|
903
|
+
const command = `sks pipeline answer ${id} --stdin`;
|
|
829
904
|
return `
|
|
830
905
|
|
|
831
906
|
Codex plan-tool interaction:
|
|
832
907
|
Before asking the user, call update_plan with:
|
|
833
908
|
- in_progress: Ask mandatory ambiguity-removal questions for ${route.command || '$SKS'}
|
|
834
|
-
- pending:
|
|
909
|
+
- pending: Seal the user's answers internally with \`${command}\`
|
|
835
910
|
- pending: Continue the original route lifecycle with the sealed decision-contract.json
|
|
836
911
|
Then ask the questions in one compact message.`;
|
|
837
912
|
}
|
|
@@ -852,23 +927,12 @@ async function clarificationStopReason(root, state, kind) {
|
|
|
852
927
|
const files = state?.mission_id ? `
|
|
853
928
|
Question file: .sneakoscope/missions/${state.mission_id}/questions.md
|
|
854
929
|
Answer schema: .sneakoscope/missions/${state.mission_id}/required-answers.schema.json` : '';
|
|
855
|
-
const command = `sks pipeline answer ${id}
|
|
930
|
+
const command = `sks pipeline answer ${id} --stdin`;
|
|
856
931
|
const title = `SKS ${routeName} is waiting for mandatory ambiguity-removal answers.`;
|
|
857
932
|
return `${title}
|
|
858
|
-
Do not finish or implement yet.
|
|
859
|
-
|
|
860
|
-
${clarificationVisibleResponseContract(id)}
|
|
861
|
-
|
|
862
|
-
The user can answer directly in chat as plain text, for example:
|
|
863
|
-
GOAL_PRECISE: ...
|
|
864
|
-
ACCEPTANCE_CRITERIA:
|
|
865
|
-
- ...
|
|
866
|
-
NON_GOALS:
|
|
867
|
-
- ...
|
|
868
|
-
|
|
869
|
-
${clarificationPlanHint({ command: routeName, route: routeName }, id)}
|
|
933
|
+
Do not finish or implement yet. Keep retries compact: show only the missing questions if they are not already visible, then wait for the user's answer.${files}
|
|
870
934
|
|
|
871
|
-
After the user answers,
|
|
935
|
+
After the user answers, seal the contract internally with "${command}", then continue the original ${routeName} route.${questionBlock}`;
|
|
872
936
|
}
|
|
873
937
|
|
|
874
938
|
export async function recordContext7Evidence(root, state, payload) {
|
package/src/core/questions.mjs
CHANGED
|
@@ -412,12 +412,13 @@ export function buildQuestionSchema(prompt) {
|
|
|
412
412
|
const ambiguity = buildAmbiguityAssessment(prompt);
|
|
413
413
|
const slots = [];
|
|
414
414
|
const presentationSpecific = domainHints.includes('presentation');
|
|
415
|
-
|
|
415
|
+
const intentMissing = ambiguity.unresolved_dimensions.includes('intent_target_or_required_outcome');
|
|
416
|
+
if (!presentationSpecific && intentMissing) {
|
|
416
417
|
slots.push(
|
|
417
418
|
{ id: 'INTENT_TARGET', question: '실제로 바꿀 대상과 원하는 결과를 한 문장으로만 적어주세요. 파일/화면/기능명이 있으면 같이 적어주세요.', required: true, type: 'string' }
|
|
418
419
|
);
|
|
419
420
|
}
|
|
420
|
-
if (!presentationSpecific && ambiguity.unresolved_dimensions.includes('success_criteria_or_acceptance')) {
|
|
421
|
+
if (!presentationSpecific && !intentMissing && ambiguity.unresolved_dimensions.includes('success_criteria_or_acceptance')) {
|
|
421
422
|
slots.push(
|
|
422
423
|
{ id: 'SUCCESS_CRITERIA_OR_ACCEPTANCE', question: '완료라고 판단할 수 있는 관찰 가능한 기준을 1~3개만 적어주세요. 모르면 “현재 코드 기준으로 판단”이라고 적어도 됩니다.', required: true, type: 'array_or_string' }
|
|
423
424
|
);
|
|
@@ -427,7 +428,7 @@ export function buildQuestionSchema(prompt) {
|
|
|
427
428
|
{ id: 'RISK_AND_BOUNDARY', question: '여러 선택지가 있거나 위험한 변경이 있다면 반드시 지켜야 할 경계만 적어주세요. 없으면 “기존 동작 보존, 파괴적 작업 금지”라고 답해주세요.', required: true, type: 'string' }
|
|
428
429
|
);
|
|
429
430
|
}
|
|
430
|
-
if (ambiguity.unresolved_dimensions.includes('codebase_context_target')) {
|
|
431
|
+
if (!intentMissing && ambiguity.unresolved_dimensions.includes('codebase_context_target')) {
|
|
431
432
|
slots.push(
|
|
432
433
|
{ id: 'CODEBASE_CONTEXT_TARGET', question: '이 요청이 가리키는 repo/브랜치/화면/파일/최근 오류 맥락을 알려주세요.', required: true, type: 'string' }
|
|
433
434
|
);
|
|
@@ -504,7 +505,7 @@ export function questionsMarkdown(schema) {
|
|
|
504
505
|
lines.push('UI 수준 E2E와 시각 검증은 Codex Computer Use 증거가 없으면 검증 완료로 주장할 수 없습니다. Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, 기타 브라우저 자동화는 UI/브라우저 검증 증거로 인정하지 않습니다.');
|
|
505
506
|
lines.push('개발 서버가 아닌 배포/스테이징 도메인에서는 삭제성 테스트를 절대 실행하지 않습니다.');
|
|
506
507
|
} else {
|
|
507
|
-
lines.push('
|
|
508
|
+
lines.push('실행을 바꿀 수 있는 질문에만 답변하면 Decision Contract가 봉인된 뒤 실행됩니다.');
|
|
508
509
|
lines.push('봉인 후 실행 중에는 사용자에게 새 질문을 하지 않고 decision ladder로 해결합니다.');
|
|
509
510
|
lines.push('사용자 의도가 실제로 모호한 항목만 묻고, 나머지는 TriWiki/current-code 기본값으로 추론합니다.');
|
|
510
511
|
}
|
|
@@ -540,7 +541,7 @@ export function questionsMarkdown(schema) {
|
|
|
540
541
|
lines.push(`- type: ${s.type}`);
|
|
541
542
|
lines.push('');
|
|
542
543
|
}
|
|
543
|
-
lines.push('##
|
|
544
|
+
lines.push('## Internal Answer Payload Template');
|
|
544
545
|
lines.push('');
|
|
545
546
|
lines.push('```json');
|
|
546
547
|
const example = {};
|