sneakoscope 0.7.57 → 0.7.59

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 CHANGED
@@ -42,7 +42,7 @@ sks selftest --mock
42
42
 
43
43
  | Area | What it does |
44
44
  | --- | --- |
45
- | CLI runtime | Bare `sks` opens or reuses the default tmux Codex CLI workspace. `sks tmux open` remains the explicit form for session/workspace flags, and `sks --mad` launches a multi-pane MAD tmux cockpit with the explicit full-access high-reasoning profile. |
45
+ | CLI runtime | Bare `sks` opens or reuses the default tmux Codex CLI workspace. `sks tmux open` remains the explicit form for session/workspace flags, and `sks --mad` launches a single-pane MAD tmux session with the explicit full-access high-reasoning profile. Split panes are reserved for active Team scout/worker lanes. |
46
46
  | Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Image-UX-Review`, `$UX-Review`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. `sks codex-app remote-control` wraps Codex CLI 0.130.0+ headless remote control without falling back to older app-server internals. |
47
47
  | OpenClaw agents | Generates an OpenClaw skill package so OpenClaw agents can attach `sneakoscope-codex`, enable the `shell` tool, and discover/use SKS commands from the target repo root. |
48
48
  | Pipeline plans | Writes `pipeline-plan.json` for stateful routes so the runtime lane, kept stages, skipped stages, verification commands, and no-unrequested-fallback invariant are visible with `sks pipeline plan`. |
@@ -80,7 +80,7 @@ The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex`
80
80
  - Checks npm for newer `sneakoscope` and `@openai/codex` versions before launch and asks whether to update when the terminal can answer y/n.
81
81
  - Installs the latest Codex CLI with `npm i -g @openai/codex@latest` when it is missing and you approve or pass `--yes`.
82
82
  - Requires tmux 3.x or newer before opening the session.
83
- - Creates a named detached tmux cockpit with multiple panes and prints only the session, gate, attach, and blocker details needed to act.
83
+ - Creates a named detached single-pane tmux session and prints only the session, gate, attach, and blocker details needed to act.
84
84
 
85
85
  ## Installation
86
86
 
@@ -211,7 +211,7 @@ sks --mad
211
211
  sks --mad --yes
212
212
  ```
213
213
 
214
- This syncs existing codex-lb/Codex CLI auth before launch, creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux cockpit with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches a tiled cockpit: Codex CLI, MAD permission gate status, and live guide panes. The cockpit recreates the named session on launch so stale single-pane sessions do not hide the split layout, then attaches in an interactive terminal. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, Supabase MCP `apply_migration`, and required Supabase CLI migration application such as `supabase migration up` or `supabase db push` are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active.
214
+ This syncs existing codex-lb/Codex CLI auth before launch, 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 a single Codex CLI pane. The session recreates the named session on launch so stale split-pane MAD sessions collapse back to the single-pane default, then attaches in an interactive terminal. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, Supabase MCP `apply_migration`, and required Supabase CLI migration application such as `supabase migration up` or `supabase db push` are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active.
215
215
 
216
216
  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.
217
217
 
@@ -287,7 +287,7 @@ The design borrows two useful ideas from external planning systems without copyi
287
287
 
288
288
  `sks goal` and `$Goal` only prepare/control the native `/goal` persistence bridge. They do not replace Team, QA, DB, or other implementation routes; use the selected execution route for the actual work and verification. Context7 is only needed for Goal when external API/library documentation becomes relevant.
289
289
 
290
- Use `$Computer-Use` or `$CU` inside Codex App when the task specifically needs Codex Computer Use speed for UI/browser/visual work. This lane intentionally skips Team debate, QA-LOOP clarification, subagents, and upfront TriWiki refresh. It still requires Codex Computer Use as the evidence source, and it defers TriWiki refresh/validate plus Honest Mode to the final closeout.
290
+ Use `$Computer-Use` or `$CU` inside Codex App when the task specifically needs Codex Computer Use speed for UI/browser/visual work. This lane intentionally skips Team debate, QA-LOOP clarification, subagents, and upfront TriWiki refresh. It still requires Codex Computer Use as the evidence source, and it defers TriWiki refresh/validate plus Honest Mode to the final closeout. SKS does not install a generated skill named `computer-use`, because that name is reserved for the first-party Codex Computer Use plugin; use `$CU` or `$computer-use-fast` from the SKS picker for the SKS route, and use `@Computer` for the OpenAI plugin.
291
291
 
292
292
  ### Create A Presentation
293
293
 
@@ -555,6 +555,8 @@ codex mcp list
555
555
 
556
556
  Codex App workflows need the app installed. QA and UI/browser visual-evidence workflows require first-party Codex Computer Use; Browser Use may support non-UI browser context, but it is not valid UI/browser verification evidence. Generated raster assets and image-review evidence require real Codex App `$imagegen`/`gpt-image-2` output, or the route must stay blocked/unverified.
557
557
 
558
+ SKS setup removes old SKS-generated `computer-use` skills from `.agents/skills` so they cannot shadow the first-party Computer Use plugin. If a running Codex App thread was opened before setup or upgrade, start a fresh thread and invoke `@Computer` or Browser again so the host reloads plugin tools.
559
+
558
560
  ### Setup is blocked by another harness
559
561
 
560
562
  ```sh
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.57",
4
+ "version": "0.7.59",
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",
@@ -36,7 +36,11 @@ export async function postinstall({ bootstrap }) {
36
36
  else if (fastModeRepair.status === 'skipped') console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
37
37
  else if (fastModeRepair.status === 'failed') console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
38
38
  const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
39
- if (globalSkills.status === 'installed') console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).`);
39
+ if (globalSkills.status === 'installed') {
40
+ const removed = globalSkills.removed_stale_generated_skills || [];
41
+ const cleanup = removed.length ? ` Removed stale generated skill shadow(s): ${removed.join(', ')}.` : '';
42
+ console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).${cleanup}`);
43
+ }
40
44
  else if (globalSkills.status === 'partial') console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${globalSkills.missing_skills.join(', ')}. Run \`sks doctor --fix\`.`);
41
45
  else if (globalSkills.status === 'skipped') console.log(`Codex App global $ skills: skipped (${globalSkills.reason}).`);
42
46
  else if (globalSkills.status === 'failed') console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
package/src/cli/main.mjs CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  } from '../core/image-ux-review.mjs';
53
53
  import { contextCapsule } from '../core/triwiki-attention.mjs';
54
54
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
55
- import { ALLOWED_REASONING_EFFORTS, AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, COMMAND_CATALOG, DESIGN_SYSTEM_SSOT, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, GETDESIGN_REFERENCE, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
55
+ import { ALLOWED_REASONING_EFFORTS, AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, COMMAND_CATALOG, DESIGN_SYSTEM_SSOT, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, GETDESIGN_REFERENCE, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, stripVisibleDecisionAnswerBlocks, triwikiContextTracking } from '../core/routes.mjs';
56
56
  import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, projectGateStatus, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
57
57
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
58
58
  import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamWatch } from '../core/team-live.mjs';
@@ -283,7 +283,7 @@ async function wizard(args = []) {
283
283
  const update = await askChoice(rl, 'Update SKS before setup?', ['yes', 'no'], 'yes');
284
284
  if (update === 'yes') {
285
285
  console.log('\nRun this update command, then rerun `sks`:');
286
- console.log(' npm i -g sneakoscope\n');
286
+ console.log(' npm i -g sneakoscope@latest\n');
287
287
  return;
288
288
  }
289
289
  console.log('Skipping update for this setup run.\n');
@@ -345,10 +345,10 @@ async function updateCheck(args = []) {
345
345
  console.log(`Latest: ${result.latest || 'unknown'}`);
346
346
  console.log(`Update: ${result.update_available ? 'available' : 'not needed'}`);
347
347
  if (result.error) console.log(`Error: ${result.error}`);
348
- if (result.update_available) console.log('Run: npm i -g sneakoscope');
348
+ if (result.update_available) console.log('Run: npm i -g sneakoscope@latest');
349
349
  }
350
350
 
351
- const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: questions -> $Answer, tiny Direct Fix edits -> $DFix, presentation/PDF artifacts -> $PPT, image-generation UI/UX reviews -> $Image-UX-Review/$UX-Review, Computer Use UI/browser speed work -> $Computer-Use, code -> $Team. Use $From-Chat-IMG only for chat screenshot plus original attachments. Use $MAD-SKS only as an explicit scoped DB authorization modifier that can be combined with another $ route. No route may invent unrequested fallback implementation code.';
351
+ const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: direct answers -> $Answer, tiny Direct Fix edits -> $DFix, presentation/PDF artifacts -> $PPT, image-generation UI/UX reviews -> $Image-UX-Review/$UX-Review, Computer Use UI/browser speed work -> $Computer-Use, code -> $Team. Execution routes infer their contract from prompt, TriWiki/current-code defaults, and conservative policy instead of surfacing prequestion sheets. Use $From-Chat-IMG only for chat screenshot plus original attachments. Use $MAD-SKS only as an explicit scoped DB authorization modifier that can be combined with another $ route. No route may invent unrequested fallback implementation code.';
352
352
 
353
353
  function commands(args = []) {
354
354
  if (flag(args, '--json')) return console.log(JSON.stringify({ aliases: ['sks', 'sneakoscope'], dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES, commands: COMMAND_CATALOG }, null, 2));
@@ -1450,9 +1450,9 @@ function usage(args = []) {
1450
1450
  deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
1451
1451
  tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session in fast-high mode: --model gpt-5.5 -c model_reasoning_effort="high". SKS always forces gpt-5.5; SKS_CODEX_MODEL and SKS_CODEX_FAST_HIGH=0 cannot downgrade or remove that model pin. Use SKS_CODEX_REASONING only for reasoning effort. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
1452
1452
  openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
1453
- team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team runs questions -> contract -> scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1453
+ team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1454
1454
  'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
1455
- ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT asks delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; imagegen/gpt-image-2 and Context7 are conditional only when the sealed PPT contract needs raster assets, slide visual critique, or current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
1455
+ ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT infers delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; Codex App $imagegen/gpt-image-2 and Context7 are conditional only when the sealed PPT contract needs raster assets, slide visual critique, or current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
1456
1456
  'image-ux-review': ['Image UX Review', '', ' $Image-UX-Review localhost 화면을 이미지 생성 리뷰 루프로 검수해줘', ' $UX-Review 이 스크린샷을 gpt-image-2 콜아웃 리뷰로 분석하고 고쳐줘', ' sks image-ux-review status latest --json', '', '$Image-UX-Review captures or receives source UI screenshots, runs Codex App $imagegen/gpt-image-2 to create generated annotated review images with numbered callouts, then extracts those generated images into image-ux-issue-ledger.json. Text-only screenshot critique cannot pass image-ux-review-gate.json; missing generated review images remain an explicit blocker.'],
1457
1457
  goal: ['Goal', '', ' sks goal create "task"', ' sks goal status latest', ' sks goal pause latest', ' sks goal resume latest', ' sks goal clear latest'],
1458
1458
  'codex-app': ['Codex App', '', ' sks bootstrap', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks dollar-commands', ' cat .codex/SNEAKOSCOPE.md'],
@@ -1720,7 +1720,11 @@ async function doctor(args) {
1720
1720
  console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
1721
1721
  console.log(`Conflicts: ${result.harness_conflicts.hard_block ? 'blocked' : 'ok'} ${result.harness_conflicts.conflicts.length} finding(s)`);
1722
1722
  if (repairApplied) console.log('Repair: regenerated SKS managed files from the installed package template');
1723
- if (globalSkillsRepair) console.log(`Global $ repair: ${globalSkillsRepair.status} ${globalSkillsRepair.root || ''}`.trimEnd());
1723
+ if (globalSkillsRepair) {
1724
+ const removed = globalSkillsRepair.removed_stale_generated_skills || [];
1725
+ const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
1726
+ console.log(`Global $ repair: ${globalSkillsRepair.status} ${globalSkillsRepair.root || ''}${cleanup}`.trimEnd());
1727
+ }
1724
1728
  if (flag(args, '--fix') && result.harness_conflicts.hard_block) console.log('Repair: skipped because another Codex harness needs human-approved removal first');
1725
1729
  console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
1726
1730
  console.log(`State: ${result.sneakoscope.ok ? 'ok' : 'missing .sneakoscope'}`);
@@ -1982,11 +1986,10 @@ async function selftest() {
1982
1986
  };
1983
1987
  for (let i = 0; i < 5; i++) {
1984
1988
  const stop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'continuing implementation without visible questions' });
1985
- 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');
1989
+ if (stop?.gate === 'clarification' || /ambiguity|clarification|question/i.test(String(stop?.reason || ''))) throw new Error('selftest failed: stale clarification gate still hard-paused without visible questions');
1986
1990
  }
1987
- 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');
1988
1991
  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.' });
1989
- if (visibleQuestionStop?.continue !== true) throw new Error('selftest failed: visible clarification question block did not allow the question-only turn to stop');
1992
+ if (visibleQuestionStop?.gate === 'clarification' || /ambiguity|clarification/i.test(String(visibleQuestionStop?.reason || ''))) throw new Error('selftest failed: visible stale clarification wording still blocked stop');
1990
1993
  await setCurrent(tmp, loopState);
1991
1994
  const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
1992
1995
  cwd: tmp,
@@ -2025,12 +2028,14 @@ async function selftest() {
2025
2028
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
2026
2029
  await ensureDir(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated'));
2027
2030
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'), '---\nname: stale-sks-generated\ndescription: Old SKS generated skill that should disappear on update.\n---\n');
2031
+ await ensureDir(path.join(repairTmp, '.agents', 'skills', 'computer-use'));
2032
+ await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'computer-use', 'SKILL.md'), '---\nname: computer-use\ndescription: Maximum-speed $Computer-Use/$CU lane for Codex Computer Use UI/browser/visual tasks.\n---\n');
2028
2033
  await writeJsonAtomic(path.join(repairTmp, '.agents', 'skills', '.sks-generated.json'), {
2029
2034
  schema_version: 1,
2030
2035
  generated_by: 'sneakoscope',
2031
2036
  version: '0.0.1',
2032
- skills: ['team', 'stale-sks-generated'],
2033
- files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md']
2037
+ skills: ['team', 'stale-sks-generated', 'computer-use'],
2038
+ files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md', '.agents/skills/computer-use/SKILL.md']
2034
2039
  });
2035
2040
  const staleCodexAgentRel = '.codex/agents/stale-generated.toml';
2036
2041
  await writeTextAtomic(path.join(repairTmp, staleCodexAgentRel), 'name = "stale_generated"\n');
@@ -2051,6 +2056,8 @@ async function selftest() {
2051
2056
  await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'policy.json'), { broken: true });
2052
2057
  const existingAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
2053
2058
  await writeTextAtomic(path.join(repairTmp, 'AGENTS.md'), existingAgentsMd.replace(/<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->[\s\S]*?<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n?/, '<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->\ntampered managed block\n<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n'));
2059
+ const stalePluginSkillNames = ['computer-use', 'browser-use', 'browser'];
2060
+ const stalePluginSkillContent = (name) => `---\nname: ${name}\ndescription: Sneakoscope generated stale plugin collision for selftest.\n---\n\nCodex App pipeline activation:\n- stale selftest marker\n`;
2054
2061
  const doctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--local-only', '--json'], {
2055
2062
  cwd: repairTmp,
2056
2063
  env: { HOME: path.join(repairTmp, 'home'), SKS_DISABLE_UPDATE_CHECK: '1' },
@@ -2066,6 +2073,7 @@ async function selftest() {
2066
2073
  if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest failed: doctor repair did not regenerate team skill');
2067
2074
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove deprecated agent-team alias skill');
2068
2075
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not prune stale generated skill from previous SKS manifest');
2076
+ if (await exists(path.join(repairTmp, '.agents', 'skills', 'computer-use', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove generated computer-use skill that shadows the first-party plugin');
2069
2077
  if (await exists(path.join(repairTmp, staleCodexAgentRel))) throw new Error('selftest failed: doctor repair did not prune stale generated agent file from previous SKS manifest');
2070
2078
  if (!doctorRepairJson.repair?.project?.skill_install?.removed_stale_generated_skills?.includes('.agents/skills/stale-sks-generated')) throw new Error('selftest failed: doctor repair did not report stale generated skill pruning');
2071
2079
  const generatedCleanupReport = doctorRepairJson.repair?.project?.generated_cleanup || {};
@@ -2080,6 +2088,29 @@ async function selftest() {
2080
2088
  if (repairedPolicy.broken || repairedPolicy.installation?.scope !== 'project' || !repairedPolicy.prompt_pipeline?.dollar_commands?.includes('$Team')) throw new Error('selftest failed: doctor --fix did not regenerate policy');
2081
2089
  const repairedAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
2082
2090
  if (!repairedAgentsMd.includes('Do not create unrequested fallback implementation code') || repairedAgentsMd.includes('tampered managed block')) throw new Error('selftest failed: doctor --fix did not repair AGENTS managed block');
2091
+ const doctorGlobalTmp = tmpdir();
2092
+ await writeJsonAtomic(path.join(doctorGlobalTmp, 'package.json'), { name: 'doctor-global-skill-repair-smoke', version: '0.0.0' });
2093
+ await initProject(doctorGlobalTmp, { installScope: 'global' });
2094
+ const doctorGlobalHome = path.join(doctorGlobalTmp, 'home');
2095
+ for (const name of stalePluginSkillNames) {
2096
+ await ensureDir(path.join(doctorGlobalHome, '.agents', 'skills', name));
2097
+ await writeTextAtomic(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
2098
+ }
2099
+ const doctorGlobalRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
2100
+ cwd: doctorGlobalTmp,
2101
+ env: { HOME: doctorGlobalHome, SKS_DISABLE_UPDATE_CHECK: '1' },
2102
+ timeoutMs: 30000,
2103
+ maxOutputBytes: 1024 * 1024
2104
+ });
2105
+ if (doctorGlobalRepair.code !== 0) throw new Error(`selftest failed: doctor --fix global skill repair exited ${doctorGlobalRepair.code}: ${doctorGlobalRepair.stderr}`);
2106
+ const doctorGlobalRepairJson = JSON.parse(doctorGlobalRepair.stdout || '{}');
2107
+ for (const name of stalePluginSkillNames) {
2108
+ if (await exists(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest failed: doctor --fix did not remove global generated ${name} plugin shadow skill`);
2109
+ }
2110
+ const doctorGlobalRemoved = doctorGlobalRepairJson.repair?.global_skills?.removed_stale_generated_skills || [];
2111
+ for (const name of stalePluginSkillNames) {
2112
+ if (!doctorGlobalRemoved.includes(`.agents/skills/${name}`)) throw new Error(`selftest failed: doctor --fix did not report global ${name} plugin shadow cleanup`);
2113
+ }
2083
2114
  const conflictTmp = tmpdir();
2084
2115
  await ensureDir(path.join(conflictTmp, '.omx'));
2085
2116
  const conflictScan = await scanHarnessConflicts(conflictTmp, { home: path.join(conflictTmp, 'home') });
@@ -2093,12 +2124,18 @@ async function selftest() {
2093
2124
  const postinstallSetupTmp = tmpdir();
2094
2125
  await writeJsonAtomic(path.join(postinstallSetupTmp, 'package.json'), { name: 'postinstall-setup-smoke', version: '0.0.0' });
2095
2126
  await ensureDir(path.join(postinstallSetupTmp, '.git'));
2127
+ const postinstallSetupHome = path.join(postinstallSetupTmp, 'home');
2128
+ for (const name of stalePluginSkillNames) {
2129
+ await ensureDir(path.join(postinstallSetupHome, '.agents', 'skills', name));
2130
+ await writeTextAtomic(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
2131
+ }
2096
2132
  const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
2097
2133
  if (postinstallSetup.code !== 0) throw new Error(`selftest failed: postinstall setup exited ${postinstallSetup.code}: ${postinstallSetup.stderr}`);
2098
2134
  if (await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest failed: postinstall installed deprecated agent-team fallback skill');
2099
2135
  if (!String(postinstallSetup.stdout || '').includes('SKS bootstrap: auto-running sks setup --bootstrap --install-scope global --force') || !String(postinstallSetup.stdout || '').includes('SKS Ready')) throw new Error('selftest failed: postinstall did not auto-run global forced bootstrap');
2100
2136
  if (!(await exists(path.join(postinstallSetupTmp, '.codex', 'hooks.json')))) throw new Error('selftest failed: postinstall did not create project hooks during automatic bootstrap');
2101
2137
  if (!String(postinstallSetup.stdout || '').includes('Codex App global $ skills: installed')) throw new Error('selftest failed: postinstall did not report automatic global Codex App skills');
2138
+ if (!String(postinstallSetup.stdout || '').includes('Removed stale generated skill shadow(s):')) throw new Error('selftest failed: postinstall did not report stale first-party plugin shadow cleanup');
2102
2139
  const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
2103
2140
  if (postinstallSetupManifest.installation?.scope !== 'global') throw new Error('selftest failed: postinstall automatic bootstrap did not use global install scope');
2104
2141
  if (postinstallSetupManifest.design_system_ssot?.authority_file !== DESIGN_SYSTEM_SSOT.authority_file || postinstallSetupManifest.design_system_ssot?.builder_prompt !== DESIGN_SYSTEM_SSOT.builder_prompt) throw new Error('selftest failed: postinstall manifest missing design SSOT policy');
@@ -2109,9 +2146,11 @@ async function selftest() {
2109
2146
  }
2110
2147
  const postinstallSetupGitignore = await safeReadText(path.join(postinstallSetupTmp, '.gitignore'));
2111
2148
  if (!postinstallSetupGitignore.includes('.sneakoscope/') || !postinstallSetupGitignore.includes('.codex/') || !postinstallSetupGitignore.includes('.agents/') || !postinstallSetupGitignore.includes('AGENTS.md')) throw new Error('selftest failed: automatic postinstall bootstrap did not ignore SKS generated files');
2112
- for (const { command } of DOLLAR_COMMANDS) {
2113
- const skillName = command.slice(1).toLowerCase();
2114
- if (!(await exists(path.join(postinstallSetupTmp, 'home', '.agents', 'skills', skillName, 'SKILL.md')))) throw new Error(`selftest failed: postinstall global ${command} skill not installed`);
2149
+ for (const skillName of new Set(DOLLAR_SKILL_NAMES)) {
2150
+ if (!(await exists(path.join(postinstallSetupTmp, 'home', '.agents', 'skills', skillName, 'SKILL.md')))) throw new Error(`selftest failed: postinstall global ${skillName} skill not installed`);
2151
+ }
2152
+ for (const name of stalePluginSkillNames) {
2153
+ if (await exists(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest failed: postinstall global skills shadow the first-party ${name} plugin`);
2115
2154
  }
2116
2155
  if (!(await exists(path.join(postinstallSetupTmp, 'home', '.agents', 'skills', 'getdesign-reference', 'SKILL.md')))) throw new Error('selftest failed: postinstall global getdesign-reference skill not installed');
2117
2156
  const oldNoBootstrap = process.env.SKS_POSTINSTALL_NO_BOOTSTRAP;
@@ -2376,15 +2415,16 @@ async function selftest() {
2376
2415
  if (globalSkillsResult.status !== 'installed') throw new Error(`selftest failed: global Codex App skills not installed: ${globalSkillsResult.status}`);
2377
2416
  const globalSkillStatus = await checkRequiredSkills(globalSkillsTmp, path.join(globalSkillsTmp, '.agents', 'skills'));
2378
2417
  if (!globalSkillStatus.ok) throw new Error(`selftest failed: global Codex App skills missing: ${globalSkillStatus.missing.join(', ')}`);
2418
+ if (await exists(path.join(globalSkillsTmp, '.agents', 'skills', 'computer-use', 'SKILL.md'))) throw new Error('selftest failed: global generated skills shadow the first-party computer-use plugin');
2379
2419
  const codexSkillMirrorExists = await exists(path.join(tmp, '.codex', 'skills', 'research-discovery', 'SKILL.md'));
2380
2420
  if (codexSkillMirrorExists) throw new Error('selftest failed: generated .codex/skills mirror still installed');
2381
2421
  const codexAppSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'research-discovery', 'SKILL.md'));
2382
2422
  if (!codexAppSkillExists) throw new Error('selftest failed: Codex App skill not installed');
2383
- for (const { command } of DOLLAR_COMMANDS) {
2384
- const skillName = command.slice(1).toLowerCase();
2423
+ for (const skillName of new Set(DOLLAR_SKILL_NAMES)) {
2385
2424
  const dollarSkillExists = await exists(path.join(tmp, '.agents', 'skills', skillName, 'SKILL.md'));
2386
- if (!dollarSkillExists) throw new Error(`selftest failed: ${command} skill not installed`);
2425
+ if (!dollarSkillExists) throw new Error(`selftest failed: ${skillName} skill not installed`);
2387
2426
  }
2427
+ if (await exists(path.join(tmp, '.agents', 'skills', 'computer-use', 'SKILL.md'))) throw new Error('selftest failed: project generated skills shadow the first-party computer-use plugin');
2388
2428
  const promptPipelineSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'prompt-pipeline', 'SKILL.md'));
2389
2429
  if (!promptPipelineSkillExists) throw new Error('selftest failed: prompt pipeline skill not installed');
2390
2430
  const promptPipelineText = await safeReadText(path.join(tmp, '.agents', 'skills', 'prompt-pipeline', 'SKILL.md'));
@@ -2405,7 +2445,7 @@ async function selftest() {
2405
2445
  if (!(await exists(path.join(tmp, '.agents', 'skills', supportSkill, 'SKILL.md')))) throw new Error(`selftest failed: ${supportSkill} skill not installed`);
2406
2446
  }
2407
2447
  const imagegenSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'imagegen', 'SKILL.md'));
2408
- if (!imagegenSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !imagegenSkillText.includes('$imagegen') || !imagegenSkillText.includes('gpt-image-2') || !imagegenSkillText.includes('OPENAI_API_KEY') || !imagegenSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY)) throw new Error('selftest failed: imagegen skill missing official Codex App image generation priority');
2448
+ if (!imagegenSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !imagegenSkillText.includes('$imagegen') || !imagegenSkillText.includes('gpt-image-2') || !imagegenSkillText.includes('Direct API fallback does not satisfy SKS route evidence') || !imagegenSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY)) throw new Error('selftest failed: imagegen skill missing official Codex App image generation priority');
2409
2449
  const imageUxReviewSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'image-ux-review', 'SKILL.md'));
2410
2450
  if (!imageUxReviewSkillText.includes('gpt-image-2') || !imageUxReviewSkillText.includes('$imagegen') || !imageUxReviewSkillText.includes('generated annotated review image') || !imageUxReviewSkillText.includes('Text-only screenshot critique cannot satisfy this route') || !imageUxReviewSkillText.includes(IMAGE_UX_REVIEW_GATE_ARTIFACT) || !imageUxReviewSkillText.includes(IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT) || !imageUxReviewSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY)) throw new Error('selftest failed: image-ux-review skill missing gpt-image-2 generated-image review gate guidance');
2411
2451
  const getdesignSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'getdesign-reference', 'SKILL.md')); if (!getdesignSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !getdesignSkillText.includes('only design decision SSOT') || !getdesignSkillText.includes('source inputs')) throw new Error('selftest failed: getdesign-reference skill missing design SSOT source-input guidance');
@@ -2430,6 +2470,7 @@ async function selftest() {
2430
2470
  if (routePrompt('$agent-team run specialists')) throw new Error('selftest failed: deprecated $agent-team route still resolved');
2431
2471
  if (routePrompt('$QA-LOOP run UI E2E')?.id !== 'QALoop' || routePrompt('$QALoop deployed smoke')) throw new Error('selftest failed: QA-LOOP route is not standardized to $QA-LOOP');
2432
2472
  if (routePrompt('[$qa-loop](/tmp/qa-loop/SKILL.md) localhost UI 검증, Codex Computer Use만 사용')?.id !== 'QALoop') throw new Error('selftest failed: markdown-linked $QA-LOOP was hijacked by heuristic routing');
2473
+ if (stripVisibleDecisionAnswerBlocks('qa-loop [GOAL_PRECISE: local QA QA_SCOPE: ui_e2e_only TARGET_BASE_URL: http://localhost:3000] 다시 실행').includes('GOAL_PRECISE')) throw new Error('selftest failed: visible decision answer block sanitizer did not remove slot payload');
2433
2474
  if (routePrompt('[$research](/tmp/research/SKILL.md) Codex Computer Use 도구 노출 문제를 QA루프 관점에서 연구')?.id !== 'Research') throw new Error('selftest failed: markdown-linked $Research was not treated as explicit route');
2434
2475
  if (routePrompt('$WikiRefresh 갱신')) throw new Error('selftest failed: deprecated $WikiRefresh route still resolved');
2435
2476
  if (routePrompt('$MAD-SKS Supabase MCP main 작업')?.id !== 'MadSKS') throw new Error('selftest failed: $MAD-SKS route did not resolve');
@@ -2652,6 +2693,19 @@ async function selftest() {
2652
2693
  if (!String(hookUpdateOldContext).includes('Update SKS now') || !String(hookUpdateOldContext).includes('Skip update for this conversation')) throw new Error('selftest failed: hook did not prompt when installed SKS is stale');
2653
2694
  const hookUpdateOldState = await readJson(path.join(hookUpdateOldTmp, '.sneakoscope', 'state', 'update-check.json'), {});
2654
2695
  if (hookUpdateOldState.pending_offer?.latest !== '9.9.9') throw new Error('selftest failed: stale installed SKS did not persist pending update offer');
2696
+ const hookUpdateAcceptPayload = JSON.stringify({ cwd: hookUpdateOldTmp, prompt: 'Update SKS now' });
2697
+ const hookUpdateAcceptResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
2698
+ cwd: hookUpdateOldTmp,
2699
+ input: hookUpdateAcceptPayload,
2700
+ env: { ...hookUpdateCurrentEnv, SKS_INSTALLED_SKS_VERSION: '0.0.0' },
2701
+ timeoutMs: 15000,
2702
+ maxOutputBytes: 256 * 1024
2703
+ });
2704
+ if (hookUpdateAcceptResult.code !== 0) throw new Error(`selftest failed: accepted update hook exited ${hookUpdateAcceptResult.code}: ${hookUpdateAcceptResult.stderr}`);
2705
+ const hookUpdateAcceptJson = JSON.parse(hookUpdateAcceptResult.stdout);
2706
+ const hookUpdateAcceptContext = hookUpdateAcceptJson.hookSpecificOutput?.additionalContext || '';
2707
+ if (!String(hookUpdateAcceptContext).includes('npm i -g sneakoscope@latest')) throw new Error('selftest failed: accepted update hook did not specify the exact npm latest command');
2708
+ if (String(hookUpdateAcceptContext).includes('sks setup') || String(hookUpdateAcceptContext).includes('sks doctor') || String(hookUpdateAcceptContext).includes('npm i -D sneakoscope')) throw new Error('selftest failed: accepted update hook still routes through setup/doctor/project update commands');
2655
2709
  const hookKoreanSksTmp = tmpdir();
2656
2710
  await initProject(hookKoreanSksTmp, {});
2657
2711
  const hookKoreanSksPayload = JSON.stringify({ cwd: hookKoreanSksTmp, prompt: koreanReadmeInstallPrompt });
@@ -2700,31 +2754,20 @@ async function selftest() {
2700
2754
  const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
2701
2755
  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');
2702
2756
  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');
2703
- const qaClarificationTmp = tmpdir();
2704
- await initProject(qaClarificationTmp, {});
2705
- 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 });
2706
- if (hookQaClarificationResult.code !== 0) throw new Error(`selftest failed: QA clarification hook exited ${hookQaClarificationResult.code}: ${hookQaClarificationResult.stderr}`);
2707
- const hookQaClarificationState = await readJson(stateFile(qaClarificationTmp), {});
2708
- const hookQaClarificationSchema = await readJson(path.join(missionDir(qaClarificationTmp, hookQaClarificationState.mission_id), 'required-answers.schema.json'));
2709
- const hookTeamSchema = hookQaClarificationSchema;
2710
- const visibleQuestionsBlock = [
2711
- 'Required questions',
2712
- ...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
2713
- 'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
2714
- ].join('\n');
2715
- const visibleQuestionDecision = await evaluateStop(qaClarificationTmp, hookQaClarificationState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
2716
- if (!visibleQuestionDecision?.continue) throw new Error('selftest failed: visible Required questions block was not accepted by clarification stop gate');
2717
- 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 });
2718
- if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
2719
- const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
2720
- 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');
2721
- 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 });
2722
- if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
2723
- const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
2724
- if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
2725
- const nonGoalsSlot = hookTeamSchema.slots.find((s) => s.id === 'NON_GOALS');
2757
+ const pptClarificationTmp = tmpdir();
2758
+ await initProject(pptClarificationTmp, {});
2759
+ const hookPptClarificationResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: pptClarificationTmp, input: JSON.stringify({ cwd: pptClarificationTmp, prompt: '$PPT 투자 제안서 만들어줘' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2760
+ if (hookPptClarificationResult.code !== 0) throw new Error(`selftest failed: PPT clarification hook exited ${hookPptClarificationResult.code}: ${hookPptClarificationResult.stderr}`);
2761
+ const hookPptClarificationState = await readJson(stateFile(pptClarificationTmp), {});
2762
+ const hookPptClarificationJson = JSON.parse(hookPptClarificationResult.stdout);
2763
+ const hookPptContext = hookPptClarificationJson.hookSpecificOutput?.additionalContext || '';
2764
+ const hookPptSchema = await readJson(path.join(missionDir(pptClarificationTmp, hookPptClarificationState.mission_id), 'required-answers.schema.json'));
2765
+ if (hookPptClarificationState.phase !== 'PPT_AUDIENCE_STRATEGY_READY' || hookPptClarificationState.implementation_allowed !== true || hookPptSchema.slots.length !== 0) throw new Error('selftest failed: PPT hook did not auto-seal without visible questions');
2766
+ if (hookPptContext.includes('Required questions') || hookPptContext.includes('VISIBLE RESPONSE CONTRACT') || hookPptContext.includes('MANDATORY ambiguity-removal gate')) throw new Error('selftest failed: PPT hook still exposed prequestion wording');
2767
+ if (!(await exists(path.join(missionDir(pptClarificationTmp, hookPptClarificationState.mission_id), 'ppt-audience-strategy.json')))) throw new Error('selftest failed: PPT auto-seal did not materialize audience strategy');
2768
+ const nonGoalsSlot = hookPptSchema.slots.find((s) => s.id === 'NON_GOALS');
2726
2769
  if (nonGoalsSlot && !nonGoalsSlot.allow_empty) throw new Error('selftest failed: NON_GOALS does not allow an empty array answer');
2727
- if (!nonGoalsSlot && !Array.isArray(hookTeamSchema.inferred_answers?.NON_GOALS)) throw new Error('selftest failed: NON_GOALS was neither asked nor inferred');
2770
+ if (!nonGoalsSlot && !Array.isArray(hookPptSchema.inferred_answers?.NON_GOALS)) throw new Error('selftest failed: NON_GOALS was neither asked nor inferred');
2728
2771
  const textParsedAnswers = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'INTENT_TARGET: compact contract sealing');
2729
2772
  if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
2730
2773
  const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
@@ -2773,26 +2816,18 @@ async function selftest() {
2773
2816
  if (honestSummaryCaseJson.decision === 'block') throw new Error('selftest failed: summary block/pass wording was treated as unresolved gap');
2774
2817
  const hookQaTmp = tmpdir();
2775
2818
  await initProject(hookQaTmp, {});
2776
- const hookQaPayload = JSON.stringify({ cwd: hookQaTmp, prompt: '$QA-LOOP run UI and API E2E against local dev' });
2819
+ const hookQaPayload = JSON.stringify({ cwd: hookQaTmp, prompt: '$QA-LOOP run API E2E against local dev' });
2777
2820
  const hookQaResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookQaTmp, input: hookQaPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2778
2821
  if (hookQaResult.code !== 0) throw new Error(`selftest failed: $QA-LOOP hook exited ${hookQaResult.code}: ${hookQaResult.stderr}`);
2779
2822
  const hookQaJson = JSON.parse(hookQaResult.stdout);
2780
2823
  const hookQaContext = hookQaJson.hookSpecificOutput?.additionalContext || '';
2781
- if (!hookQaContext.includes('MANDATORY ambiguity-removal gate activated') || !hookQaContext.includes('QA_SCOPE') || !hookQaContext.includes('UI_COMPUTER_USE_ACK')) throw new Error('selftest failed: $QA-LOOP hook did not provide QA-specific questions');
2824
+ if (!hookQaContext.includes('Route contract auto-sealed') || hookQaContext.includes('MANDATORY ambiguity-removal gate activated') || hookQaContext.includes('Required questions:') || hookQaContext.includes('QA_SCOPE:') || hookQaContext.includes('UI_COMPUTER_USE_ACK:')) throw new Error('selftest failed: $QA-LOOP hook did not auto-seal without visible answer slots');
2782
2825
  if (!hookQaContext.includes('Codex Computer Use') || !hookQaContext.includes('Playwright') || !hookQaContext.includes('Chrome MCP')) throw new Error('selftest failed: $QA-LOOP hook did not state Computer Use-only UI policy');
2783
2826
  if (hookQaContext.includes('Browser Use 또는 Computer Use') || hookQaContext.includes('Browser/Computer Use evidence')) throw new Error('selftest failed: $QA-LOOP hook still allows Browser Use as UI evidence');
2784
2827
  const hookQaState = await readJson(stateFile(hookQaTmp), {});
2785
- if (hookQaState.phase !== 'QALOOP_CLARIFICATION_AWAITING_ANSWERS' || hookQaState.implementation_allowed !== false) throw new Error('selftest failed: $QA-LOOP hook did not lock execution behind ambiguity gate');
2828
+ if (hookQaState.phase !== 'QALOOP_CLARIFICATION_CONTRACT_SEALED' || hookQaState.implementation_allowed !== true || hookQaState.clarification_required !== false || !hookQaState.ambiguity_gate_passed) throw new Error('selftest failed: $QA-LOOP hook did not auto-seal the ambiguity gate');
2786
2829
  const hookQaSchema = await readJson(path.join(missionDir(hookQaTmp, hookQaState.mission_id), 'required-answers.schema.json'));
2787
- const hookQaAnswers = {};
2788
- for (const s of hookQaSchema.slots) hookQaAnswers[s.id] = s.options ? (s.type === 'array' ? [s.options[0]] : s.options[0]) : (s.type.includes('array') ? ['selftest'] : 'selftest');
2789
- hookQaAnswers.QA_SCOPE = 'all_available';
2790
- hookQaAnswers.TARGET_BASE_URL = 'none';
2791
- hookQaAnswers.API_BASE_URL = 'same_as_target';
2792
- const hookQaAnswersPath = path.join(hookQaTmp, 'qa-answers.json');
2793
- await writeJsonAtomic(hookQaAnswersPath, hookQaAnswers);
2794
- const qaAnswerResult = await runProcess(process.execPath, [hookBin, 'pipeline', 'answer', 'latest', hookQaAnswersPath], { cwd: hookQaTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2795
- if (qaAnswerResult.code !== 0) throw new Error(`selftest failed: QA pipeline answer exited ${qaAnswerResult.code}: ${qaAnswerResult.stderr}`);
2830
+ if (hookQaSchema.slots.length !== 0 || hookQaSchema.inferred_answers?.QA_SCOPE !== 'api_e2e_only') throw new Error('selftest failed: $QA-LOOP schema did not infer QA answers without visible slots');
2796
2831
  const qaMissionDir = missionDir(hookQaTmp, hookQaState.mission_id);
2797
2832
  const initialQaGate = await readJson(path.join(qaMissionDir, 'qa-gate.json'));
2798
2833
  const qaReportFile = initialQaGate.qa_report_file;
@@ -3196,7 +3231,7 @@ async function selftest() {
3196
3231
  await writeTextAtomic(fakeTmuxLog, '');
3197
3232
  const madCockpit = await launchMadTmuxUi(['--workspace', 'sks-mad-selftest-ui', '--no-attach'], { root: tmp, tmux: { ok: true, bin: fakeTmuxBin, version: '3.4' }, codex: { bin: process.execPath }, app: { ok: true, guidance: [] }, missionId: 'M-MAD-SELFTEST' });
3198
3233
  const madTmuxLogText = await safeReadText(fakeTmuxLog);
3199
- if (!madCockpit.created || madCockpit.opened?.panes?.length !== 3 || !madTmuxLogText.includes('new-session') || !madTmuxLogText.includes('split-window')) throw new Error('selftest failed: MAD tmux launch did not create a multi-pane cockpit');
3234
+ if (!madCockpit.created || madCockpit.mode !== 'mad_session' || madCockpit.opened?.panes?.length !== 1 || !madTmuxLogText.includes('new-session') || madTmuxLogText.includes('split-window')) throw new Error('selftest failed: MAD tmux launch should create one pane and leave split panes to Team lanes');
3200
3235
  if (previousFakeTmuxLog === undefined) delete process.env.SKS_FAKE_TMUX_LOG;
3201
3236
  else process.env.SKS_FAKE_TMUX_LOG = previousFakeTmuxLog;
3202
3237
  const codexLaunchArgs = defaultCodexLaunchArgs({ SKS_CODEX_REASONING: 'low' }).join(' ');
@@ -3304,8 +3339,8 @@ async function selftest() {
3304
3339
  if (!predictableAuthCliSchema.inferred_answers.RISK_BOUNDARY?.includes('no destructive commands or live data writes')) throw new Error('selftest failed: predictable auth-worded CLI work did not infer conservative risk boundary');
3305
3340
  const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
3306
3341
  const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
3307
- 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(',')}`);
3308
- if (vagueSlotIds.length !== 1) throw new Error(`selftest failed: vague work should ask only the execution-changing intent question, got ${vagueSlotIds.join(',')}`);
3342
+ if (vagueSlotIds.length !== 0) throw new Error(`selftest failed: vague work should auto-seal inferred defaults without visible questions, got ${vagueSlotIds.join(',')}`);
3343
+ if (!vagueSchema.inferred_answers?.GOAL_PRECISE || !vagueSchema.inferred_answers?.ACCEPTANCE_CRITERIA) throw new Error('selftest failed: vague work did not infer core contract defaults');
3309
3344
  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');
3310
3345
  const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
3311
3346
  if (pptRoute?.id !== 'PPT') throw new Error('selftest failed: $PPT did not route to presentation pipeline');
@@ -3314,14 +3349,14 @@ async function selftest() {
3314
3349
  const pptSchema = buildQuestionSchema('$PPT 투자자용 피치덱 만들어줘');
3315
3350
  const pptSlotIds = pptSchema.slots.map((s) => s.id);
3316
3351
  for (const id of ['PRESENTATION_DELIVERY_CONTEXT', 'PRESENTATION_AUDIENCE_PROFILE', 'PRESENTATION_STP_STRATEGY', 'PRESENTATION_PAINPOINT_SOLUTION_MAP', 'PRESENTATION_DECISION_CONTEXT']) {
3317
- if (!pptSlotIds.includes(id)) throw new Error(`selftest failed: PPT schema missing ${id}`);
3352
+ if (pptSlotIds.includes(id) || pptSchema.inferred_answers?.[id] === undefined) throw new Error(`selftest failed: PPT schema did not infer ${id}`);
3318
3353
  }
3319
3354
  const pptSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'ppt', 'SKILL.md'));
3320
- if (!pptSkillText.includes('STP') || !pptSkillText.includes('target audience profile') || !pptSkillText.includes('decision context') || !pptSkillText.includes('3+ pain-point to solution mappings')) throw new Error('selftest failed: generated PPT skill missing STP/audience/pain-point guidance');
3355
+ if (!pptSkillText.includes('STP') || !pptSkillText.includes('target audience profile') || !pptSkillText.includes('decision context') || !pptSkillText.includes('3+ pain-point to solution mappings') || !pptSkillText.includes('Do not surface a prequestion sheet')) throw new Error('selftest failed: generated PPT skill missing inferred STP/audience/pain-point guidance');
3321
3356
  if (!pptSkillText.includes('simple, restrained, and information-first') || !pptSkillText.includes('over-designed decoration') || !pptSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !pptSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY) || !pptSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !pptSkillText.includes('only design decision SSOT') || !pptSkillText.includes('instead of treating references as parallel authorities')) throw new Error('selftest failed: generated PPT skill missing restrained design/imagegen/fused-SSOT guidance');
3322
3357
  if (!pptSkillText.includes('PPT pipeline allowlist') || !pptSkillText.includes('ignore installed skills and MCPs') || !pptSkillText.includes('prevent AI-like generic presentation design') || !pptSkillText.includes('Do not use generic design skills such as design-artifact-expert')) throw new Error('selftest failed: generated PPT skill missing pipeline allowlist enforcement');
3323
3358
  if (!pptSkillText.includes('source-html/') || !pptSkillText.includes('temporary build files') || !pptSkillText.includes('ppt-parallel-report.json')) throw new Error('selftest failed: generated PPT skill missing source preservation/temp cleanup/parallel guidance');
3324
- if (!pptSkillText.includes('ppt-fact-ledger.json') || !pptSkillText.includes('ppt-image-asset-ledger.json') || !pptSkillText.includes('OpenAI Image API') || !pptSkillText.includes('ppt-review-ledger.json') || !pptSkillText.includes('ppt-iteration-report.json') || !pptSkillText.includes('never simulate missing gpt-image-2 output')) throw new Error('selftest failed: generated PPT skill missing fact/image/review loop anti-fake guidance');
3359
+ if (!pptSkillText.includes('ppt-fact-ledger.json') || !pptSkillText.includes('ppt-image-asset-ledger.json') || !pptSkillText.includes('direct API fallback') || !pptSkillText.includes('ppt-review-ledger.json') || !pptSkillText.includes('ppt-iteration-report.json') || !pptSkillText.includes('never simulate missing gpt-image-2 output')) throw new Error('selftest failed: generated PPT skill missing fact/image/review loop anti-fake guidance');
3325
3360
  if (routeRequiresSubagents(pptRoute, '$PPT 투자자용 피치덱 만들어줘')) throw new Error('selftest failed: PPT route should not require subagents by default');
3326
3361
  if (!reflectionRequiredForRoute(pptRoute)) throw new Error('selftest failed: PPT route should require reflection');
3327
3362
  const pptMission = await createMission(tmp, { mode: 'ppt', prompt: '$PPT 투자자용 피치덱 만들어줘' });
@@ -3408,11 +3443,11 @@ async function selftest() {
3408
3443
  unsupported_critical_claims_count: 0,
3409
3444
  passed: true
3410
3445
  });
3411
- const requiredImageBuildResult = await runProcess(process.execPath, [hookBin, 'ppt', 'build', requiredImagePptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1', OPENAI_API_KEY: '' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
3446
+ const requiredImageBuildResult = await runProcess(process.execPath, [hookBin, 'ppt', 'build', requiredImagePptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1', SKS_FAKE_IMAGE_GATE_TOKEN: 'ignored-by-sks-route-gate' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
3412
3447
  if (requiredImageBuildResult.code !== 0) throw new Error(`selftest failed: required-image PPT build command failed: ${requiredImageBuildResult.stderr || requiredImageBuildResult.stdout}`);
3413
3448
  const requiredImageBuild = JSON.parse(requiredImageBuildResult.stdout);
3414
3449
  const requiredImageLedger = await readJson(path.join(requiredImagePptMission.dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT));
3415
- if (requiredImageBuild.ok || requiredImageBuild.gate?.passed || !requiredImageBuild.gate?.image_asset_ledger_created || requiredImageBuild.gate?.image_asset_policy_satisfied !== false || !requiredImageLedger.required || requiredImageLedger.passed || !requiredImageLedger.blockers?.includes('missing_OPENAI_API_KEY_for_required_gpt_image_2_assets') || requiredImageLedger.generated_count !== 0) throw new Error('selftest failed: required PPT image assets were not blocked without real gpt-image-2 credentials');
3450
+ if (requiredImageBuild.ok || requiredImageBuild.gate?.passed || !requiredImageBuild.gate?.image_asset_ledger_created || requiredImageBuild.gate?.image_asset_policy_satisfied !== false || !requiredImageLedger.required || requiredImageLedger.passed || !requiredImageLedger.blockers?.includes('missing_codex_app_imagegen_gpt_image_2_asset_evidence') || requiredImageLedger.generated_count !== 0) throw new Error('selftest failed: required PPT image assets were not blocked without Codex App imagegen evidence');
3416
3451
  const installUxSchema = buildQuestionSchema('SKS first install/bootstrap UX and Context7 MCP setup improvement');
3417
3452
  const installUxSlotIds = installUxSchema.slots.map((s) => s.id);
3418
3453
  if (installUxSchema.domain_hints.includes('uiux') || installUxSlotIds.includes('VISUAL_REGRESSION_REQUIRED')) throw new Error('selftest failed: CLI UX install prompt should not ask visual UI questions');
@@ -3424,9 +3459,8 @@ async function selftest() {
3424
3459
  const { id, dir, mission } = await createMission(tmp, { mode: 'goal', prompt: '발표자료 만들어줘' });
3425
3460
  const schema = buildQuestionSchema(mission.prompt);
3426
3461
  await writeQuestions(dir, schema);
3427
- if (validateAnswers(schema, {}).ok) throw new Error('selftest failed: empty answers valid');
3428
- const answers = {};
3429
- for (const s of schema.slots) answers[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'));
3462
+ if (!validateAnswers(schema, {}).ok || schema.slots.length !== 0) throw new Error('selftest failed: inferred empty answer set should be valid after prequestion removal');
3463
+ const answers = { ...(schema.inferred_answers || {}) };
3430
3464
  await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
3431
3465
  const sealed = await sealContract(dir, mission);
3432
3466
  if (!sealed.ok) throw new Error('selftest failed: answers rejected');
@@ -15,7 +15,7 @@ import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge }
15
15
  import { DEFAULT_EVAL_THRESHOLDS, compareEvaluationReports, runEvaluationBenchmark } from '../core/evaluation.mjs';
16
16
  import { contextCapsule } from '../core/triwiki-attention.mjs';
17
17
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
18
- import { ALLOWED_REASONING_EFFORTS, CODEX_COMPUTER_USE_ONLY_POLICY, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, RECOMMENDED_SKILLS, ROUTES, hasFromChatImgSignal, routePrompt, routeReasoning, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
18
+ import { ALLOWED_REASONING_EFFORTS, CODEX_COMPUTER_USE_ONLY_POLICY, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, RECOMMENDED_SKILLS, ROUTES, hasFromChatImgSignal, routePrompt, routeReasoning, stackCurrentDocsPolicy, stripVisibleDecisionAnswerBlocks, triwikiContextTracking } from '../core/routes.mjs';
19
19
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
20
20
  import { appendTeamEvent, formatAgentReasoning, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, readTeamControl, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamCleanupSummary, renderTeamWatch, requestTeamSessionCleanup, teamCleanupRequested, teamReasoningPolicy } from '../core/team-live.mjs';
21
21
  import { evaluateTeamReviewPolicyGate, MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT, teamReviewPolicy } from '../core/team-review-policy.mjs';
@@ -281,21 +281,38 @@ function qaRoute() {
281
281
  async function qaLoopPrepare(args) {
282
282
  const root = await sksRoot();
283
283
  if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
284
- const prompt = promptOf(args);
284
+ const prompt = stripVisibleDecisionAnswerBlocks(promptOf(args));
285
285
  if (!prompt) throw new Error('Missing QA target prompt.');
286
286
  const { id, dir } = await createMission(root, { mode: 'qaloop', prompt });
287
287
  const schema = buildQaLoopQuestionSchema(prompt);
288
288
  const route = qaRoute();
289
289
  await writeQuestions(dir, schema);
290
290
  await writeJsonAtomic(path.join(dir, 'route-context.json'), { route: 'QALoop', command: '$QA-LOOP', mode: 'QALOOP', task: prompt, required_skills: route?.requiredSkills || [], context7_required: false, original_stop_gate: 'qa-gate.json', clarification_gate: true });
291
+ if (schema.slots.length === 0) {
292
+ await writeJsonAtomic(path.join(dir, 'answers.json'), schema.inferred_answers || {});
293
+ const result = await sealContract(dir, { id, prompt, mode: 'qaloop' });
294
+ if (!result.ok) {
295
+ console.error('Inferred QA-LOOP answers failed validation.');
296
+ console.error(JSON.stringify(result.validation, null, 2));
297
+ process.exitCode = 2;
298
+ return;
299
+ }
300
+ const artifactResult = await writeQaLoopArtifacts(dir, { id, prompt, mode: 'qaloop' }, result.contract);
301
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.prepare.auto_sealed', slots: 0, hash: result.contract.sealed_hash, checklist_count: artifactResult.checklist_count });
302
+ await setCurrent(root, { mission_id: id, route: 'QALoop', route_command: '$QA-LOOP', mode: 'QALOOP', phase: 'QALOOP_CLARIFICATION_CONTRACT_SEALED', questions_allowed: false, implementation_allowed: true, clarification_required: false, clarification_passed: true, ambiguity_gate_required: true, ambiguity_gate_passed: true, stop_gate: 'qa-gate.json', qa_loop_artifacts_ready: true, qa_report_file: artifactResult.report_file, qa_checklist_count: artifactResult.checklist_count, reasoning_effort: 'high', reasoning_profile: 'sks-logic-high', reasoning_temporary: true });
303
+ console.log(`QA-LOOP mission created: ${id}`);
304
+ console.log('QA-LOOP contract auto-sealed from prompt, TriWiki/current-code defaults, and conservative safety policy.');
305
+ console.log(`Checklist: ${artifactResult.checklist_count} cases`);
306
+ console.log(`Report: ${path.relative(root, path.join(dir, artifactResult.report_file))}`);
307
+ console.log(`Run: sks qa-loop run ${id} --max-cycles ${schema.inferred_answers?.MAX_QA_CYCLES || 1}`);
308
+ return;
309
+ }
291
310
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.prepare.questions_created', slots: schema.slots.length });
292
311
  await setCurrent(root, { mission_id: id, route: 'QALoop', route_command: '$QA-LOOP', mode: 'QALOOP', phase: 'QALOOP_CLARIFICATION_AWAITING_ANSWERS', questions_allowed: true, implementation_allowed: false, clarification_required: true, ambiguity_gate_required: true, stop_gate: 'clarification-gate', reasoning_effort: 'high', reasoning_profile: 'sks-logic-high', reasoning_temporary: true });
293
312
  console.log(`QA-LOOP mission created: ${id}`);
294
- console.log('QA-LOOP is locked until all required answers are supplied.');
295
- console.log(`Questions: ${path.relative(root, path.join(dir, 'questions.md'))}`);
313
+ console.log('QA-LOOP could not auto-seal because required safe defaults were unavailable.');
296
314
  console.log(`Answer schema: ${path.relative(root, path.join(dir, 'required-answers.schema.json'))}`);
297
- console.log('\nRequired questions:');
298
- console.log(formatQuestionsForCli(schema));
315
+ console.log('Inspect the schema and provide answers with: sks qa-loop answer <mission-id> answers.json');
299
316
  }
300
317
 
301
318
  async function qaLoopAnswer(args) {
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.57';
8
+ export const PACKAGE_VERSION = '0.7.59';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11