sneakoscope 0.7.45 → 0.7.48
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 +6 -5
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +27 -2
- package/src/cli/main.mjs +190 -61
- package/src/cli/maintenance-commands.mjs +2 -7
- package/src/core/auto-review.mjs +1 -1
- package/src/core/db-safety.mjs +11 -33
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +10 -3
- package/src/core/init.mjs +47 -8
- package/src/core/permission-gates.mjs +99 -0
- package/src/core/pipeline.mjs +115 -33
- package/src/core/questions.mjs +6 -5
- package/src/core/routes.mjs +2 -1
- package/src/core/tmux-ui.mjs +24 -2
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex`
|
|
|
79
79
|
- Checks npm for newer `sneakoscope` and `@openai/codex` versions before launch and asks whether to update when the terminal can answer y/n.
|
|
80
80
|
- Installs the latest Codex CLI with `npm i -g @openai/codex@latest` when it is missing and you approve or pass `--yes`.
|
|
81
81
|
- Requires tmux 3.x or newer before opening the session.
|
|
82
|
-
- Creates or reuses a named detached tmux session,
|
|
82
|
+
- Creates or reuses a named detached tmux session and prints only the session, gate, attach, and blocker details needed to act.
|
|
83
83
|
|
|
84
84
|
## Installation
|
|
85
85
|
|
|
@@ -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"
|
|
@@ -204,7 +205,7 @@ sks --mad
|
|
|
204
205
|
sks --mad --yes
|
|
205
206
|
```
|
|
206
207
|
|
|
207
|
-
This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal.
|
|
208
|
+
This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, and needed migrations are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active. Repeat launches reuse the same named SKS MAD tmux session.
|
|
208
209
|
|
|
209
210
|
MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
|
|
210
211
|
|
|
@@ -229,9 +230,9 @@ sks team dashboard latest
|
|
|
229
230
|
sks team log latest
|
|
230
231
|
```
|
|
231
232
|
|
|
232
|
-
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
233
|
+
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
233
234
|
|
|
234
|
-
The tmux Team launch is a live orchestration screen: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while
|
|
235
|
+
The tmux Team launch is a live orchestration screen in one tmux window: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. Pane headers show only mission, lane, phase, follow command, and cleanup command. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while detailed evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
|
|
235
236
|
|
|
236
237
|
Agent sessions communicate through the bounded Team transcript. Use `sks team message <mission-id|latest> --from <agent> --to <agent|all> --message "..."` to add direct or broadcast messages; lane panes show messages addressed to that agent plus the fallback global tail.
|
|
237
238
|
|
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.48",
|
|
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';
|
|
@@ -56,6 +56,7 @@ import { createSkillCandidate, decideSkillInjection, skillDreamFixture, writeSki
|
|
|
56
56
|
import { classifyToolError, harnessGrowthReport } from '../core/evaluation.mjs';
|
|
57
57
|
import { runWorkflowPerfBench, validateWorkflowPerfReport } from '../core/perf-bench.mjs';
|
|
58
58
|
import { buildProofField, proofFieldFixture, validateProofFieldReport } from '../core/proof-field.mjs';
|
|
59
|
+
import { permissionGateSummary } from '../core/permission-gates.mjs';
|
|
59
60
|
import { recordMistake, writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
|
|
60
61
|
import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/mistake-recall.mjs';
|
|
61
62
|
import { buildPromptContext } from '../core/prompt-context-builder.mjs';
|
|
@@ -168,7 +169,7 @@ Usage:
|
|
|
168
169
|
sks ppt status <mission-id|latest> [--json]
|
|
169
170
|
sks context7 check|setup|tools|resolve|docs|evidence ...
|
|
170
171
|
sks pipeline status|resume|plan [--json] [--proof-field]
|
|
171
|
-
sks pipeline answer <mission-id|latest> <answers.json>
|
|
172
|
+
sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">
|
|
172
173
|
sks guard check [--json]
|
|
173
174
|
sks conflicts check|prompt [--json]
|
|
174
175
|
sks versioning status|bump|pre-commit [--json]
|
|
@@ -591,9 +592,14 @@ function printPipelinePlan(root, id, plan) {
|
|
|
591
592
|
async function pipelineAnswer(root, args = []) {
|
|
592
593
|
const [missionArg, answerFile] = args;
|
|
593
594
|
const id = await resolveMissionId(root, missionArg);
|
|
594
|
-
if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json>');
|
|
595
|
+
if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">');
|
|
595
596
|
const { dir, mission } = await loadMission(root, id);
|
|
596
|
-
const
|
|
597
|
+
const schema = await readJson(path.join(dir, 'required-answers.schema.json'));
|
|
598
|
+
const answers = answerFile === '--stdin'
|
|
599
|
+
? parseAnswersText(schema, await readStdin())
|
|
600
|
+
: answerFile === '--text'
|
|
601
|
+
? parseAnswersText(schema, args.slice(2).join(' '))
|
|
602
|
+
: await readJson(path.resolve(answerFile));
|
|
597
603
|
await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
|
|
598
604
|
const result = await sealContract(dir, mission);
|
|
599
605
|
if (!result.ok) {
|
|
@@ -645,6 +651,48 @@ async function pipelineAnswer(root, args = []) {
|
|
|
645
651
|
console.log('Next: continue the original route lifecycle using decision-contract.json.');
|
|
646
652
|
}
|
|
647
653
|
|
|
654
|
+
function parseAnswersText(schema = {}, text = '') {
|
|
655
|
+
const body = String(text || '').trim();
|
|
656
|
+
const slots = Array.isArray(schema.slots) ? schema.slots : [];
|
|
657
|
+
const slotById = new Map(slots.map((slot) => [slot.id, slot]));
|
|
658
|
+
const answers = {};
|
|
659
|
+
let currentId = null;
|
|
660
|
+
let currentLines = [];
|
|
661
|
+
const flush = () => {
|
|
662
|
+
if (!currentId) return;
|
|
663
|
+
answers[currentId] = normalizeTextAnswerValue(slotById.get(currentId), currentLines.join('\n').trim());
|
|
664
|
+
currentId = null;
|
|
665
|
+
currentLines = [];
|
|
666
|
+
};
|
|
667
|
+
for (const line of body.split(/\r?\n/)) {
|
|
668
|
+
const match = line.match(/^\s*([A-Z][A-Z0-9_]{2,})\s*[::]\s*(.*)$/);
|
|
669
|
+
if (match && slotById.has(match[1])) {
|
|
670
|
+
flush();
|
|
671
|
+
currentId = match[1];
|
|
672
|
+
currentLines = [match[2] || ''];
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (currentId) currentLines.push(line);
|
|
676
|
+
}
|
|
677
|
+
flush();
|
|
678
|
+
if (!Object.keys(answers).length && slots.length === 1 && body) {
|
|
679
|
+
answers[slots[0].id] = normalizeTextAnswerValue(slots[0], body.replace(new RegExp(`^\\s*${slots[0].id}\\s*`, 'i'), '').trim());
|
|
680
|
+
}
|
|
681
|
+
return answers;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function normalizeTextAnswerValue(slot = {}, raw = '') {
|
|
685
|
+
const value = String(raw || '').trim();
|
|
686
|
+
if (slot.type === 'array') {
|
|
687
|
+
return value.split(/\r?\n|,/).map((x) => x.replace(/^\s*[-*]\s*/, '').trim()).filter(Boolean);
|
|
688
|
+
}
|
|
689
|
+
if (slot.type === 'array_or_string') {
|
|
690
|
+
const bulletLines = value.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
691
|
+
if (bulletLines.length > 1 && bulletLines.every((line) => /^[-*]\s+/.test(line))) return bulletLines.map((line) => line.replace(/^[-*]\s+/, '').trim());
|
|
692
|
+
}
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
|
|
648
696
|
async function materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext = {}, contract = {}) {
|
|
649
697
|
const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, contract);
|
|
650
698
|
if (route?.id === 'MadSKS') {
|
|
@@ -655,7 +703,11 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
|
|
|
655
703
|
permissions_deactivated: false,
|
|
656
704
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
657
705
|
direct_execute_sql_allowed: true,
|
|
706
|
+
normal_db_writes_allowed: true,
|
|
707
|
+
live_server_writes_allowed: true,
|
|
708
|
+
migration_apply_allowed: true,
|
|
658
709
|
catastrophic_safety_guard_active: true,
|
|
710
|
+
permission_profile: permissionGateSummary(),
|
|
659
711
|
contract_hash: contract.sealed_hash || null
|
|
660
712
|
});
|
|
661
713
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
@@ -674,6 +726,9 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
|
|
|
674
726
|
mad_sks_gate_ready: true,
|
|
675
727
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
676
728
|
direct_execute_sql_allowed: true,
|
|
729
|
+
normal_db_writes_allowed: true,
|
|
730
|
+
live_server_writes_allowed: true,
|
|
731
|
+
migration_apply_allowed: true,
|
|
677
732
|
catastrophic_safety_guard_active: true
|
|
678
733
|
}
|
|
679
734
|
};
|
|
@@ -766,7 +821,11 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
|
|
|
766
821
|
deactivates_when_gate_passed: gateFile,
|
|
767
822
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
768
823
|
direct_execute_sql_allowed: true,
|
|
824
|
+
normal_db_writes_allowed: true,
|
|
825
|
+
live_server_writes_allowed: true,
|
|
826
|
+
migration_apply_allowed: true,
|
|
769
827
|
catastrophic_safety_guard_active: true,
|
|
828
|
+
permission_profile: permissionGateSummary(),
|
|
770
829
|
contract_hash: contract.sealed_hash || null
|
|
771
830
|
};
|
|
772
831
|
await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), artifact);
|
|
@@ -783,6 +842,9 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
|
|
|
783
842
|
mad_sks_gate_file: gateFile,
|
|
784
843
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
785
844
|
direct_execute_sql_allowed: true,
|
|
845
|
+
normal_db_writes_allowed: true,
|
|
846
|
+
live_server_writes_allowed: true,
|
|
847
|
+
migration_apply_allowed: true,
|
|
786
848
|
catastrophic_safety_guard_active: true
|
|
787
849
|
};
|
|
788
850
|
}
|
|
@@ -1024,8 +1086,9 @@ async function madHighCommand(args = []) {
|
|
|
1024
1086
|
return;
|
|
1025
1087
|
}
|
|
1026
1088
|
const profile = await enableMadHighProfile();
|
|
1027
|
-
|
|
1028
|
-
console.log(
|
|
1089
|
+
const madLaunch = await activateMadTmuxPermissionState(process.cwd());
|
|
1090
|
+
console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
|
|
1091
|
+
console.log('Live full-access active; catastrophic DB wipe/all-row/project-management guards remain.');
|
|
1029
1092
|
const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
|
|
1030
1093
|
return launchTmuxUi([...cleanArgs, '--workspace', workspace], {
|
|
1031
1094
|
codexArgs: profile.launch_args,
|
|
@@ -1034,6 +1097,67 @@ async function madHighCommand(args = []) {
|
|
|
1034
1097
|
});
|
|
1035
1098
|
}
|
|
1036
1099
|
|
|
1100
|
+
async function activateMadTmuxPermissionState(cwd = process.cwd()) {
|
|
1101
|
+
const root = await sksRoot();
|
|
1102
|
+
if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
|
|
1103
|
+
const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad tmux live full-access session' });
|
|
1104
|
+
const gate = {
|
|
1105
|
+
schema_version: 1,
|
|
1106
|
+
passed: false,
|
|
1107
|
+
mad_sks_permission_active: true,
|
|
1108
|
+
permissions_deactivated: false,
|
|
1109
|
+
live_server_writes_allowed: true,
|
|
1110
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
1111
|
+
direct_execute_sql_allowed: true,
|
|
1112
|
+
normal_db_writes_allowed: true,
|
|
1113
|
+
migration_apply_allowed: true,
|
|
1114
|
+
catastrophic_safety_guard_active: true,
|
|
1115
|
+
permission_profile: permissionGateSummary(),
|
|
1116
|
+
activated_by: 'sks --mad',
|
|
1117
|
+
cwd: path.resolve(cwd || process.cwd())
|
|
1118
|
+
};
|
|
1119
|
+
await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), gate);
|
|
1120
|
+
await writeJsonAtomic(path.join(dir, 'route-context.json'), {
|
|
1121
|
+
route: 'MadSKS',
|
|
1122
|
+
command: '$MAD-SKS',
|
|
1123
|
+
mode: 'MADSKS',
|
|
1124
|
+
task: gate.activated_by,
|
|
1125
|
+
mad_sks_authorization: true,
|
|
1126
|
+
tmux_launch: true,
|
|
1127
|
+
permission_profile: gate.permission_profile
|
|
1128
|
+
});
|
|
1129
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
1130
|
+
ts: nowIso(),
|
|
1131
|
+
type: 'mad_sks.tmux_permission_opened',
|
|
1132
|
+
route: 'MadSKS',
|
|
1133
|
+
live_server_writes_allowed: true,
|
|
1134
|
+
catastrophic_safety_guard_active: true
|
|
1135
|
+
});
|
|
1136
|
+
await setCurrent(root, {
|
|
1137
|
+
mission_id: id,
|
|
1138
|
+
route: 'MadSKS',
|
|
1139
|
+
route_command: '$MAD-SKS',
|
|
1140
|
+
mode: 'MADSKS',
|
|
1141
|
+
phase: 'MADSKS_TMUX_PERMISSION_ACTIVE',
|
|
1142
|
+
questions_allowed: false,
|
|
1143
|
+
implementation_allowed: true,
|
|
1144
|
+
mad_sks_active: true,
|
|
1145
|
+
mad_sks_modifier: true,
|
|
1146
|
+
mad_sks_gate_file: 'mad-sks-gate.json',
|
|
1147
|
+
mad_sks_gate_ready: true,
|
|
1148
|
+
live_server_writes_allowed: true,
|
|
1149
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
1150
|
+
direct_execute_sql_allowed: true,
|
|
1151
|
+
normal_db_writes_allowed: true,
|
|
1152
|
+
migration_apply_allowed: true,
|
|
1153
|
+
catastrophic_safety_guard_active: true,
|
|
1154
|
+
permission_profile: gate.permission_profile,
|
|
1155
|
+
stop_gate: 'mad-sks-gate.json',
|
|
1156
|
+
prompt: gate.activated_by
|
|
1157
|
+
});
|
|
1158
|
+
return { mission_id: id, dir, gate };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1037
1161
|
async function maybePromptSksUpdateForLaunch(args = [], opts = {}) {
|
|
1038
1162
|
if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
|
|
1039
1163
|
const latest = await npmPackageVersion('sneakoscope');
|
|
@@ -1781,7 +1905,7 @@ function hasTopLevelCodexModeLock(text = '') {
|
|
|
1781
1905
|
const lines = String(text || '').split('\n');
|
|
1782
1906
|
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1783
1907
|
const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
|
|
1784
|
-
return /^model\s*=|^model_reasoning_effort\s
|
|
1908
|
+
return /^model\s*=|^model_reasoning_effort\s*=/m.test(top);
|
|
1785
1909
|
}
|
|
1786
1910
|
|
|
1787
1911
|
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
@@ -1847,7 +1971,7 @@ async function selftest() {
|
|
|
1847
1971
|
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
1972
|
}
|
|
1849
1973
|
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\
|
|
1974
|
+
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
1975
|
if (visibleQuestionStop?.continue !== true) throw new Error('selftest failed: visible clarification question block did not allow the question-only turn to stop');
|
|
1852
1976
|
await setCurrent(tmp, loopState);
|
|
1853
1977
|
const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
|
|
@@ -2027,7 +2151,7 @@ async function selftest() {
|
|
|
2027
2151
|
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
2152
|
const codexLbHome = path.join(tmp, 'codex-lb-home');
|
|
2029
2153
|
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');
|
|
2154
|
+
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
2155
|
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
2156
|
cwd: tmp,
|
|
2033
2157
|
env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
|
|
@@ -2040,7 +2164,7 @@ async function selftest() {
|
|
|
2040
2164
|
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
2041
2165
|
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
2042
2166
|
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
|
|
2167
|
+
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
2168
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
2045
2169
|
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
2046
2170
|
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 +2419,23 @@ async function selftest() {
|
|
|
2295
2419
|
if (routePrompt('$MAD-SKS Supabase MCP main 작업')?.id !== 'MadSKS') throw new Error('selftest failed: $MAD-SKS route did not resolve');
|
|
2296
2420
|
if (routePrompt('$MAD-SKS $Team Supabase MCP main 작업')?.id !== 'Team') throw new Error('selftest failed: $MAD-SKS did not compose with $Team');
|
|
2297
2421
|
if (routePrompt('$DB Supabase 점검 $MAD-SKS')?.id !== 'DB') throw new Error('selftest failed: trailing $MAD-SKS changed primary route');
|
|
2422
|
+
const madStandaloneTmp = tmpdir();
|
|
2423
|
+
await initProject(madStandaloneTmp, {});
|
|
2424
|
+
const madStandalonePayload = JSON.stringify({ cwd: madStandaloneTmp, prompt: '$MAD-SKS main 권한 열어줘' });
|
|
2425
|
+
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 });
|
|
2426
|
+
if (madStandaloneResult.code !== 0) throw new Error(`selftest failed: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
|
|
2427
|
+
const madStandaloneState = await readJson(stateFile(madStandaloneTmp), {});
|
|
2428
|
+
if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json' || madStandaloneState.normal_db_writes_allowed !== true || madStandaloneState.live_server_writes_allowed !== true || madStandaloneState.migration_apply_allowed !== true) throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate live full-access scoped permissions');
|
|
2429
|
+
const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
|
|
2430
|
+
const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
|
|
2431
|
+
if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest failed: standalone MAD-SKS did not allow ordinary DDL');
|
|
2432
|
+
const madModifierTmp = tmpdir();
|
|
2433
|
+
await initProject(madModifierTmp, {});
|
|
2434
|
+
const madModifierPayload = JSON.stringify({ cwd: madModifierTmp, prompt: '$MAD-SKS $Team 회전 아스키 아트는 제일 처음 인증 안됐을때만 codex cli처럼 애니메이션으로 보이게 하고 tmux에서는 정적 3d 아스키 아트로 보여줘' });
|
|
2435
|
+
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 });
|
|
2436
|
+
if (madModifierResult.code !== 0) throw new Error(`selftest failed: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
|
|
2437
|
+
const madModifierState = await readJson(stateFile(madModifierTmp), {});
|
|
2438
|
+
if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json' || madModifierState.normal_db_writes_allowed !== true || madModifierState.live_server_writes_allowed !== true || madModifierState.migration_apply_allowed !== true) throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate live full-access scoped permissions');
|
|
2298
2439
|
if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
|
|
2299
2440
|
const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
|
|
2300
2441
|
if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
|
|
@@ -2358,9 +2499,9 @@ async function selftest() {
|
|
|
2358
2499
|
const hookGoalDelegationContext = hookGoalDelegationJson.hookSpecificOutput?.additionalContext || '';
|
|
2359
2500
|
const hookGoalDelegationBridgeMatch = hookGoalDelegationContext.match(/Goal bridge mission: (M-[A-Za-z0-9-]+)/);
|
|
2360
2501
|
if (!hookGoalDelegationBridgeMatch || !hookGoalDelegationContext.includes('Delegated execution route: $Team')) throw new Error('selftest failed: $Goal implementation prompt did not prepare a bridge plus Team delegation');
|
|
2361
|
-
if (
|
|
2502
|
+
if (hookGoalDelegationContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalDelegationContext.includes('$Team route prepared')) throw new Error('selftest failed: $Goal implementation delegation did not prepare direct Team route');
|
|
2362
2503
|
const hookGoalDelegationState = await readJson(stateFile(hookGoalDelegationTmp), {});
|
|
2363
|
-
if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== '
|
|
2504
|
+
if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalDelegationState.implementation_allowed === false || !hookGoalDelegationState.team_plan_ready) throw new Error('selftest failed: $Goal implementation delegation did not leave direct Team ready');
|
|
2364
2505
|
if (!(await exists(path.join(missionDir(hookGoalDelegationTmp, hookGoalDelegationBridgeMatch[1]), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest failed: $Goal implementation delegation did not write bridge workflow artifact');
|
|
2365
2506
|
const activeGoalMissionId = hookState.mission_id;
|
|
2366
2507
|
const hookGoalOverlayPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Team 발표자료 만들어줘' });
|
|
@@ -2368,12 +2509,11 @@ async function selftest() {
|
|
|
2368
2509
|
if (hookGoalOverlayResult.code !== 0) throw new Error(`selftest failed: active Goal overlay hook exited ${hookGoalOverlayResult.code}: ${hookGoalOverlayResult.stderr}`);
|
|
2369
2510
|
const hookGoalOverlayJson = JSON.parse(hookGoalOverlayResult.stdout);
|
|
2370
2511
|
const hookGoalOverlayContext = hookGoalOverlayJson.hookSpecificOutput?.additionalContext || '';
|
|
2371
|
-
if (
|
|
2512
|
+
if (hookGoalOverlayContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalOverlayContext.includes('$Team route prepared')) throw new Error('selftest failed: active Goal hijacked a plain Korean implementation prompt instead of preparing direct Team');
|
|
2372
2513
|
if (!hookGoalOverlayContext.includes(`Active Goal overlay: existing Goal mission ${activeGoalMissionId}`) || !hookGoalOverlayContext.includes('goal-workflow.json')) throw new Error('selftest failed: active Goal overlay context was not included with the new route');
|
|
2373
|
-
if (hookGoalOverlayContext.indexOf('MANDATORY ambiguity-removal gate activated') > hookGoalOverlayContext.indexOf('Active Goal overlay:')) throw new Error('selftest failed: active Goal overlay appeared before the newly prepared Team gate');
|
|
2374
2514
|
const hookGoalOverlayState = await readJson(stateFile(hookGoalTmp), {});
|
|
2375
|
-
if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== '
|
|
2376
|
-
if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), '
|
|
2515
|
+
if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalOverlayState.implementation_allowed === false || !hookGoalOverlayState.team_plan_ready) throw new Error('selftest failed: active Goal overlay did not leave a new direct Team mission current');
|
|
2516
|
+
if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: active Goal overlay Team mission did not write team-plan.json');
|
|
2377
2517
|
const hookUpdateCurrentTmp = tmpdir();
|
|
2378
2518
|
await initProject(hookUpdateCurrentTmp, {});
|
|
2379
2519
|
const hookUpdateCurrentEnv = { SKS_DISABLE_UPDATE_CHECK: '0', SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' };
|
|
@@ -2458,7 +2598,7 @@ async function selftest() {
|
|
|
2458
2598
|
if (hookKoreanSksResult.code !== 0) throw new Error(`selftest failed: Korean SKS hook exited ${hookKoreanSksResult.code}: ${hookKoreanSksResult.stderr}`);
|
|
2459
2599
|
const hookKoreanSksJson = JSON.parse(hookKoreanSksResult.stdout);
|
|
2460
2600
|
const hookKoreanSksContext = hookKoreanSksJson.hookSpecificOutput?.additionalContext || '';
|
|
2461
|
-
if (!hookKoreanSksContext.includes('
|
|
2601
|
+
if (!hookKoreanSksContext.includes('$Team route prepared') || hookKoreanSksContext.includes('GOAL_PRECISE: 이번 작업의 최종 목표') || hookKoreanSksContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: Korean prompt did not prepare direct Team route');
|
|
2462
2602
|
if (!hookKoreanSksContext.includes('Route: $Team')) throw new Error('selftest failed: Korean implementation prompt did not promote to Team route');
|
|
2463
2603
|
if (hookKoreanSksContext.includes('SKS answer-only pipeline active')) throw new Error('selftest failed: Korean implementation prompt still used answer-only pipeline');
|
|
2464
2604
|
const hookKoreanSksState = await readJson(stateFile(hookKoreanSksTmp), {});
|
|
@@ -2471,12 +2611,10 @@ async function selftest() {
|
|
|
2471
2611
|
if (hookPaymentTeamResult.code !== 0) throw new Error(`selftest failed: payment/auth Team hook exited ${hookPaymentTeamResult.code}: ${hookPaymentTeamResult.stderr}`);
|
|
2472
2612
|
const hookPaymentTeamJson = JSON.parse(hookPaymentTeamResult.stdout);
|
|
2473
2613
|
const hookPaymentTeamContext = hookPaymentTeamJson.hookSpecificOutput?.additionalContext || '';
|
|
2474
|
-
if (!hookPaymentTeamContext.includes('
|
|
2614
|
+
if (!hookPaymentTeamContext.includes('$Team route prepared') || hookPaymentTeamContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: predictable payment/auth Team prompt did not prepare direct Team route');
|
|
2475
2615
|
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');
|
|
2476
2616
|
const hookPaymentTeamState = await readJson(stateFile(hookPaymentTeamTmp), {});
|
|
2477
2617
|
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');
|
|
2478
|
-
const hookPaymentTeamSchema = await readJson(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'required-answers.schema.json'));
|
|
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
2618
|
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');
|
|
2481
2619
|
const hookTeamTmp = tmpdir();
|
|
2482
2620
|
await initProject(hookTeamTmp, {});
|
|
@@ -2484,63 +2622,48 @@ async function selftest() {
|
|
|
2484
2622
|
const hookTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: hookTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2485
2623
|
if (hookTeamResult.code !== 0) throw new Error(`selftest failed: $Team hook exited ${hookTeamResult.code}: ${hookTeamResult.stderr}`);
|
|
2486
2624
|
const hookTeamJson = JSON.parse(hookTeamResult.stdout);
|
|
2487
|
-
if (
|
|
2488
|
-
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('
|
|
2489
|
-
if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team goal');
|
|
2490
|
-
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: missing Team presentation question');
|
|
2491
|
-
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('Codex plan-tool interaction')) throw new Error('selftest failed: $Team ambiguity gate did not inject plan-tool guidance');
|
|
2625
|
+
if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY ambiguity-removal gate activated') || hookTeamJson.hookSpecificOutput?.additionalContext?.includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: $Team hook still forced ambiguity questions');
|
|
2626
|
+
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: $Team hook did not prepare direct Team route');
|
|
2492
2627
|
const hookTeamState = await readJson(stateFile(hookTeamTmp), {});
|
|
2493
|
-
if (hookTeamState.phase !== '
|
|
2494
|
-
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a
|
|
2495
|
-
if (await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) throw new Error('selftest failed: Team plan was created
|
|
2628
|
+
if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest failed: $Team hook did not prepare direct Team mission');
|
|
2629
|
+
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pipeline plan');
|
|
2630
|
+
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: Team plan was not created directly');
|
|
2496
2631
|
const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2497
2632
|
if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
|
|
2498
2633
|
const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
|
|
2499
2634
|
const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
|
|
2500
2635
|
const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
|
|
2501
|
-
if (hookTeamPendingState.mission_id
|
|
2502
|
-
if (
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
if (!String(hookTeamStopJson.reason || '').includes('sks pipeline answer')) throw new Error('selftest failed: Stop hook did not provide pipeline answer command');
|
|
2511
|
-
if (!String(hookTeamStopJson.reason || '').includes('Codex plan-tool interaction')) throw new Error('selftest failed: Stop hook did not reprint plan-tool guidance');
|
|
2512
|
-
if (!String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook did not force visible clarification response');
|
|
2513
|
-
const hookTeamSchema = await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'required-answers.schema.json'));
|
|
2636
|
+
if (hookTeamPendingState.mission_id === hookTeamState.mission_id || hookTeamPendingContext.includes('Required questions still pending') || hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: direct Team follow-up was blocked by stale clarification behavior');
|
|
2637
|
+
if (hookTeamPendingState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !hookTeamPendingState.team_plan_ready) throw new Error('selftest failed: direct Team follow-up did not prepare a fresh Team mission');
|
|
2638
|
+
const qaClarificationTmp = tmpdir();
|
|
2639
|
+
await initProject(qaClarificationTmp, {});
|
|
2640
|
+
const hookQaClarificationResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, prompt: '$QA-LOOP 로그인 QA 해줘' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2641
|
+
if (hookQaClarificationResult.code !== 0) throw new Error(`selftest failed: QA clarification hook exited ${hookQaClarificationResult.code}: ${hookQaClarificationResult.stderr}`);
|
|
2642
|
+
const hookQaClarificationState = await readJson(stateFile(qaClarificationTmp), {});
|
|
2643
|
+
const hookQaClarificationSchema = await readJson(path.join(missionDir(qaClarificationTmp, hookQaClarificationState.mission_id), 'required-answers.schema.json'));
|
|
2644
|
+
const hookTeamSchema = hookQaClarificationSchema;
|
|
2514
2645
|
const visibleQuestionsBlock = [
|
|
2515
2646
|
'Required questions',
|
|
2516
2647
|
...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
|
|
2517
|
-
'Reply by slot id, then I will
|
|
2648
|
+
'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
|
|
2518
2649
|
].join('\n');
|
|
2519
|
-
const visibleQuestionDecision = await evaluateStop(
|
|
2650
|
+
const visibleQuestionDecision = await evaluateStop(qaClarificationTmp, hookQaClarificationState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
|
|
2520
2651
|
if (!visibleQuestionDecision?.continue) throw new Error('selftest failed: visible Required questions block was not accepted by clarification stop gate');
|
|
2521
|
-
const hookTeamPreToolBlocked = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd:
|
|
2652
|
+
const hookTeamPreToolBlocked = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, command: 'npm run selftest' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2522
2653
|
if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
|
|
2523
2654
|
const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
|
|
2524
2655
|
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:
|
|
2656
|
+
const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, command: 'node ./bin/sks.mjs pipeline answer latest --stdin' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2526
2657
|
if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
|
|
2527
2658
|
const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
|
|
2528
2659
|
if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
|
|
2529
2660
|
const nonGoalsSlot = hookTeamSchema.slots.find((s) => s.id === 'NON_GOALS');
|
|
2530
2661
|
if (nonGoalsSlot && !nonGoalsSlot.allow_empty) throw new Error('selftest failed: NON_GOALS does not allow an empty array answer');
|
|
2531
2662
|
if (!nonGoalsSlot && !Array.isArray(hookTeamSchema.inferred_answers?.NON_GOALS)) throw new Error('selftest failed: NON_GOALS was neither asked nor inferred');
|
|
2532
|
-
const
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
await writeJsonAtomic(hookTeamAnswersPath, hookTeamAnswers);
|
|
2537
|
-
const pipelineAnswerResult = await runProcess(process.execPath, [hookBin, 'pipeline', 'answer', 'latest', hookTeamAnswersPath], { cwd: hookTeamTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2538
|
-
if (pipelineAnswerResult.code !== 0) throw new Error(`selftest failed: pipeline answer exited ${pipelineAnswerResult.code}: ${pipelineAnswerResult.stderr}`);
|
|
2539
|
-
const answeredTeamState = await readJson(stateFile(hookTeamTmp), {});
|
|
2540
|
-
if (answeredTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !answeredTeamState.ambiguity_gate_passed || answeredTeamState.implementation_allowed !== true || !answeredTeamState.team_plan_ready || !answeredTeamState.pipeline_plan_ready) throw new Error('selftest failed: pipeline answer did not materialize Team after ambiguity gate');
|
|
2541
|
-
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'decision-contract.json')))) throw new Error('selftest failed: pipeline answer did not seal decision contract');
|
|
2542
|
-
if (validatePipelinePlan(await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT))).ok !== true) throw new Error('selftest failed: pipeline answer did not refresh a valid pipeline plan');
|
|
2543
|
-
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-live.md')))) throw new Error('selftest failed: Team artifacts missing after ambiguity gate passed');
|
|
2663
|
+
const textParsedAnswers = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'INTENT_TARGET: compact contract sealing');
|
|
2664
|
+
if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
|
|
2665
|
+
const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
|
|
2666
|
+
if (textParsedImplicitAnswer.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not infer the only missing slot');
|
|
2544
2667
|
const honestLoopTmp = tmpdir();
|
|
2545
2668
|
await initProject(honestLoopTmp, {});
|
|
2546
2669
|
const { id: honestLoopId, dir: honestLoopDir } = await createMission(honestLoopTmp, { mode: 'sks', prompt: 'honest loopback selftest' });
|
|
@@ -2690,12 +2813,13 @@ async function selftest() {
|
|
|
2690
2813
|
if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
|
|
2691
2814
|
const preservedConfigTmp = tmpdir();
|
|
2692
2815
|
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');
|
|
2816
|
+
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
2817
|
await initProject(preservedConfigTmp, {});
|
|
2695
2818
|
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
|
|
2819
|
+
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');
|
|
2820
|
+
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
2821
|
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
|
|
2822
|
+
if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
|
|
2699
2823
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
2700
2824
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
2701
2825
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
|
@@ -2957,6 +3081,8 @@ async function selftest() {
|
|
|
2957
3081
|
const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
|
|
2958
3082
|
if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team tmux view did not expose agent live lanes');
|
|
2959
3083
|
if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team tmux view did not expose orchestration overview plus agent lanes');
|
|
3084
|
+
if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'tiled' || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest failed: Team tmux view did not expose single-window split UI metadata');
|
|
3085
|
+
if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest failed: Team tmux pane banner is too noisy or missing compact follow hint');
|
|
2960
3086
|
if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team tmux role palette did not classify lane roles');
|
|
2961
3087
|
if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team tmux view did not expose color/title metadata and cleanup policy');
|
|
2962
3088
|
if (tmuxTeam.session !== `sks-team-${teamId}` || !tmuxTeam.attach_command?.includes(`sks-team-${teamId}`)) throw new Error('selftest failed: Team tmux session is not named for visibility');
|
|
@@ -3056,6 +3182,7 @@ async function selftest() {
|
|
|
3056
3182
|
const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
|
|
3057
3183
|
const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
|
|
3058
3184
|
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(',')}`);
|
|
3185
|
+
if (vagueSlotIds.length !== 1) throw new Error(`selftest failed: vague work should ask only the execution-changing intent question, got ${vagueSlotIds.join(',')}`);
|
|
3059
3186
|
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
3187
|
const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
|
|
3061
3188
|
if (pptRoute?.id !== 'PPT') throw new Error('selftest failed: $PPT did not route to presentation pipeline');
|
|
@@ -3193,7 +3320,9 @@ async function selftest() {
|
|
|
3193
3320
|
const madState = { mission_id: madMission.id, mode: 'TEAM', route_command: '$Team', stop_gate: 'team-gate.json', mad_sks_active: true, mad_sks_modifier: true, mad_sks_gate_file: 'team-gate.json' };
|
|
3194
3321
|
const columnCleanupSql = 'alter table users ' + 'dr' + 'op column legacy_name;';
|
|
3195
3322
|
const madColumnCleanupDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: columnCleanupSql }, { duringNoQuestion: false });
|
|
3196
|
-
if (madColumnCleanupDecision.action !== 'allow') throw new Error('selftest failed: MAD-SKS column cleanup was not allowed');
|
|
3323
|
+
if (madColumnCleanupDecision.action !== 'allow' || !madColumnCleanupDecision.mad_sks?.permission_profile?.allowed?.includes('direct_execute_sql_writes')) throw new Error('selftest failed: MAD-SKS column cleanup was not allowed through the modular permission gate');
|
|
3324
|
+
const madLiveDmlDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'fixed' where id = 'selftest';" }, { duringNoQuestion: false });
|
|
3325
|
+
if (madLiveDmlDecision.action !== 'allow' || !madLiveDmlDecision.mad_sks?.live_server_writes_allowed) throw new Error('selftest failed: MAD-SKS targeted live DML was not allowed');
|
|
3197
3326
|
const tableRemovalSql = 'dr' + 'op table users;';
|
|
3198
3327
|
const madTableRemovalDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringNoQuestion: false });
|
|
3199
3328
|
if (madTableRemovalDecision.action !== 'block') throw new Error('selftest failed: MAD-SKS catastrophic table removal was not blocked');
|