sneakoscope 0.7.21 → 0.7.25

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
@@ -46,7 +46,7 @@ sks selftest --mock
46
46
  | CLI runtime | `sks tmux open` and `sks --mad` explicitly launch Codex CLI with tmux; bare `sks` only prints help/readiness surfaces. |
47
47
  | Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
48
48
  | Pipeline plans | Writes `pipeline-plan.json` for stateful routes so the runtime lane, kept stages, skipped stages, verification commands, and no-unrequested-fallback invariant are visible with `sks pipeline plan`. |
49
- | Team orchestration | Runs substantial work through ambiguity handling, scouts, TriWiki refresh, debate, runtime task graphs, worker inboxes, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence to skip unrelated pipeline work instead of expanding Team. |
49
+ | Team orchestration | Runs substantial work through score-based ambiguity handling, scouts, TriWiki refresh, debate, runtime task graphs, worker inboxes, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence to skip unrelated pipeline work instead of expanding Team. |
50
50
  | Skill dreaming | Records cheap generated-skill usage counters in JSON and only periodically scans `.agents/skills` for keep, merge, prune, and improvement candidates. Reports are recommendation-only and never delete skills automatically. |
51
51
  | From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
52
52
  | QA loop | Dogfoods UI/API behavior with safety gates, Codex Computer Use-only UI evidence, safe fixes, and rechecks. |
@@ -95,7 +95,7 @@ sks bootstrap
95
95
 
96
96
  `sks` commands work even when no project root is present. Project-aware commands use the nearest `.sneakoscope`, `.dcodex`, or `.git` root; if none exists, SKS uses a per-user global runtime root. `sks bootstrap` still initializes the current project when you want project-local hooks, skills, and TriWiki state.
97
97
 
98
- Project setup writes shared `.gitignore` entries for generated SKS files: `.sneakoscope/`, `.codex/`, `.agents/`, and managed `AGENTS.md`. Use `sks setup --local-only` when you want those excludes kept only in `.git/info/exclude`.
98
+ Project setup writes shared `.gitignore` entries for generated SKS files: `.sneakoscope/`, `.codex/`, `.agents/`, and managed `AGENTS.md`. Setup, doctor repair, and npm postinstall refreshes also compare the previous SKS generated-file manifest with the current package templates and prune stale SKS-generated legacy skills or agent files while preserving user-owned custom skills. Use `sks setup --local-only` when you want those excludes kept only in `.git/info/exclude`.
99
99
 
100
100
  During npm postinstall, SKS also installs generated Codex App skills and tries the official getdesign Codex skill command, `skills add MohtashamMurshid/getdesign`, when the `skills` CLI is available. If that CLI is missing, setup still installs the generated `getdesign-reference` skill. Design work still flows through one authority: `design.md`. When `design.md` is missing, `docs/Design-Sys-Prompt.md` is the builder prompt and getdesign plus curated DESIGN.md examples such as [VoltAgent/awesome-design-md](https://github.com/VoltAgent/awesome-design-md) are inputs to fuse into that SSOT or into route-local `$PPT` style tokens.
101
101
 
@@ -236,6 +236,12 @@ sks code-structure scan --json
236
236
 
237
237
  `sks proof-field scan` is SKS's lightweight outcome rubric: it maps the goal to proof cones, records unrelated work that can be skipped with evidence, reports a simplicity score, and names escalation triggers for when the route must return to the full Team/Honest proof path. The rubric embeds Hyperplan-style adversarial pressure as compact lenses instead of a new command: challenge framing, subtract surface, demand evidence, test integration risk, and consider one simpler alternative. When `execution_lane.lane` is `proof_field_fast_lane`, SKS can keep the parent-owned minimal patch plus listed verification and skip Team debate, fresh executor teams, broad route rework, and unrelated checks. Database, security, visual-forensic, unknown, broad, failed, or unsupported-claim signals fail closed to the normal Team/Honest path. Use `sks pipeline plan --proof-field` after changed files are known to bind that Proof Field decision to the mission plan.
238
238
 
239
+ ### Ambiguity Questions
240
+
241
+ SKS no longer starts from a fixed checklist such as `GOAL_PRECISE` and `ACCEPTANCE_CRITERIA`. The clarification gate first scores goal clarity, constraint clarity, success-criteria clarity, and codebase-context clarity, then asks only the lowest-clarity item that can change execution. Predictable UI defaults, DB safety defaults, test scope, fallback policy, and ordinary implementation acceptance criteria are inferred and sealed automatically.
242
+
243
+ The design borrows two useful ideas from external planning systems without copying their route weight: Ouroboros-style ambiguity thresholds decide whether the prompt is clear enough to proceed, while Prometheus/Hyperplan-style adversarial lenses challenge framing, remove unnecessary surface, demand evidence, test integration risk, and consider a simpler alternative before Team work starts.
244
+
239
245
  `sks skill-dream` keeps generated skill complexity bounded without doing a heavy evaluation on every prompt. Route use writes compact counters to `.sneakoscope/skills/dream-state.json`; after the configured count/cooldown threshold, or when you run `sks skill-dream run`, SKS scans `.agents/skills` and writes `.sneakoscope/reports/skill-dream-latest.json` with keep, merge, prune, and improvement candidates. The report is intentionally advisory: deleting or merging skills requires explicit approval.
240
246
 
241
247
  `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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.21",
4
+ "version": "0.7.25",
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",
@@ -186,7 +186,14 @@ export async function ensureGlobalCodexSkillsDuringInstall(opts = {}) {
186
186
  try {
187
187
  const install = await installSkills(home);
188
188
  const skills = await checkRequiredSkills(home, root);
189
- return { status: skills.ok ? 'installed' : 'partial', root, installed_count: install.installed_skills.length, removed_aliases: install.removed_agent_skill_aliases, missing_skills: skills.missing };
189
+ return {
190
+ status: skills.ok ? 'installed' : 'partial',
191
+ root,
192
+ installed_count: install.installed_skills.length,
193
+ removed_aliases: install.removed_agent_skill_aliases,
194
+ removed_stale_generated_skills: install.removed_stale_generated_skills,
195
+ missing_skills: skills.missing
196
+ };
190
197
  } catch (err) {
191
198
  return { status: 'failed', root, error: err.message };
192
199
  }
package/src/cli/main.mjs CHANGED
@@ -83,54 +83,17 @@ export async function main(args) {
83
83
  if (!cmd) return help();
84
84
  if (cmd === '--help' || cmd === '-h') return help();
85
85
  if (cmd === '--version' || cmd === '-v' || cmd === 'version') return version();
86
- if (cmd === 'postinstall') return postinstall({ bootstrap });
87
- if (cmd === 'wizard' || cmd === 'ui') return wizard(tail);
88
86
  if (cmd === 'tmux') return !sub || String(sub).startsWith('--') ? tmuxCommand('check', tail) : tmuxCommand(sub, rest);
89
87
  if (cmd === 'auto-review' || cmd === 'autoreview') return autoReviewCommand(sub, rest);
90
- if (cmd === 'update-check') return updateCheck(tail);
91
- if (cmd === 'help') return help(tail);
92
- if (cmd === 'commands') return commands(tail);
93
- if (cmd === 'usage') return usage(tail);
94
- if (cmd === 'root') return rootCommand(tail);
95
- if (cmd === 'quickstart') return quickstartCommand();
96
- if (cmd === 'codex-app') return codexAppHelp(tail);
97
- if (cmd === 'bootstrap') return bootstrap(tail);
98
- if (cmd === 'deps') return deps(sub, rest);
99
88
  if (cmd === 'dollar-commands' || cmd === 'dollars' || cmd === '$') return dollarCommands(tail);
100
89
  if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
101
- if (cmd === 'qa-loop') return qaLoopCommand(sub, rest);
102
- if (cmd === 'ppt') return pptCommand(sub, rest);
103
- if (cmd === 'context7') return context7Command(sub, rest);
104
- if (cmd === 'pipeline') return pipeline(sub, rest);
105
- if (cmd === 'guard') return guard(sub, rest);
106
- if (cmd === 'conflicts') return conflicts(sub, rest);
107
- if (cmd === 'versioning') return versioning(sub, rest);
108
- if (cmd === 'reasoning') return reasoningCommand(tail);
109
- if (cmd === 'aliases') return aliases();
110
- if (cmd === 'setup') return setup(tail);
111
- if (cmd === 'fix-path') return fixPath(tail);
112
- if (cmd === 'doctor') return doctor(tail);
113
- if (cmd === 'init') return init(tail);
114
- if (cmd === 'selftest') return selftest(tail);
115
- if (cmd === 'goal') return goalCommand(sub, rest);
116
- if (cmd === 'research') return researchCommand(sub, rest);
117
- if (cmd === 'hook') return emitHook(sub);
118
- if (cmd === 'profile') return profileCommand(sub, rest);
119
- if (cmd === 'hproof') return hproofCommand(sub, rest);
120
- if (cmd === 'validate-artifacts') return validateArtifactsCommand(tail);
121
- if (cmd === 'perf') return perfCommand(sub, rest);
122
- if (cmd === 'proof-field') return proofFieldCommand(sub, rest);
123
- if (cmd === 'skill-dream') return skillDreamCommand(sub, rest);
124
- if (cmd === 'code-structure') return codeStructureCommand(sub, rest);
125
- if (cmd === 'memory') return memoryCommand(sub, rest);
126
- if (cmd === 'gx') return gxCommand(sub, rest);
127
- if (cmd === 'team') return team(tail);
128
- if (cmd === 'db') return dbCommand(sub, rest);
129
- if (cmd === 'eval') return evalCommand(sub, rest);
130
- if (cmd === 'harness') return harnessCommand(sub, rest);
131
- if (cmd === 'wiki') return wikiCommand(sub, rest);
132
- if (cmd === 'gc') return gcCommand(tail);
133
- if (cmd === 'stats') return statsCommand(tail);
90
+ const handlers = {
91
+ postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
92
+ 'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), context7: () => context7Command(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
93
+ goal: () => goalCommand(sub, rest), research: () => researchCommand(sub, rest), hook: () => emitHook(sub), profile: () => profileCommand(sub, rest), hproof: () => hproofCommand(sub, rest), 'validate-artifacts': () => validateArtifactsCommand(tail), perf: () => perfCommand(sub, rest), 'proof-field': () => proofFieldCommand(sub, rest), 'skill-dream': () => skillDreamCommand(sub, rest), 'code-structure': () => codeStructureCommand(sub, rest), memory: () => memoryCommand(sub, rest), gx: () => gxCommand(sub, rest),
94
+ team: () => team(tail), db: () => dbCommand(sub, rest), eval: () => evalCommand(sub, rest), harness: () => harnessCommand(sub, rest), wiki: () => wikiCommand(sub, rest), gc: () => gcCommand(tail), stats: () => statsCommand(tail)
95
+ };
96
+ if (handlers[cmd]) return handlers[cmd]();
134
97
  console.error(`Unknown command: ${cmd}`);
135
98
  process.exitCode = 1;
136
99
  }
@@ -1433,11 +1396,12 @@ async function doctor(args) {
1433
1396
  let conflictScan = await scanHarnessConflicts(root);
1434
1397
  let repairApplied = false;
1435
1398
  let globalSkillsRepair = null;
1399
+ let projectRepair = null;
1436
1400
  const globalCommand = await globalSksCommand();
1437
1401
  if (flag(args, '--fix') && !conflictScan.hard_block) {
1438
1402
  const existingManifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
1439
1403
  const fixScope = requestedScope || normalizeInstallScope(existingManifest?.installation?.scope || 'global');
1440
- await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1404
+ projectRepair = await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1441
1405
  if (!flag(args, '--local-only')) globalSkillsRepair = await ensureGlobalCodexSkillsDuringInstall({ force: true });
1442
1406
  repairApplied = true;
1443
1407
  conflictScan = await scanHarnessConflicts(root);
@@ -1464,7 +1428,7 @@ async function doctor(args) {
1464
1428
  const result = {
1465
1429
  node: { ok: nodeOk, version: process.version }, root, codex, rust,
1466
1430
  install,
1467
- repair: { applied: repairApplied, global_skills: globalSkillsRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
1431
+ repair: { applied: repairApplied, project: projectRepair, global_skills: globalSkillsRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
1468
1432
  harness_conflicts: {
1469
1433
  ok: conflictScan.ok,
1470
1434
  hard_block: conflictScan.hard_block,
@@ -1780,6 +1744,26 @@ async function selftest() {
1780
1744
  await initProject(repairTmp, { installScope: 'project', localOnly: true });
1781
1745
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'), 'tampered\n');
1782
1746
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
1747
+ await ensureDir(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated'));
1748
+ 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');
1749
+ await writeJsonAtomic(path.join(repairTmp, '.agents', 'skills', '.sks-generated.json'), {
1750
+ schema_version: 1,
1751
+ generated_by: 'sneakoscope',
1752
+ version: '0.0.1',
1753
+ skills: ['team', 'stale-sks-generated'],
1754
+ files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md']
1755
+ });
1756
+ const staleCodexAgentRel = '.codex/agents/stale-generated.toml';
1757
+ await writeTextAtomic(path.join(repairTmp, staleCodexAgentRel), 'name = "stale_generated"\n');
1758
+ const staleManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
1759
+ staleManifest.version = '0.0.1';
1760
+ staleManifest.generated_files = {
1761
+ schema_version: 1,
1762
+ generated_by: 'sneakoscope',
1763
+ prune_policy: 'remove_previous_sks_generated_paths_absent_from_current_manifest',
1764
+ files: [...(staleManifest.generated_files?.files || []), '.agents/skills/stale-sks-generated/SKILL.md', staleCodexAgentRel]
1765
+ };
1766
+ await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'manifest.json'), staleManifest);
1783
1767
  await ensureDir(path.join(repairTmp, '.agents', 'skills', 'custom-keep'));
1784
1768
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md'), '---\nname: custom-keep\ndescription: User custom skill, not generated by SKS.\n---\n');
1785
1769
  await writeTextAtomic(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'), 'legacy mirror\n');
@@ -1802,6 +1786,11 @@ async function selftest() {
1802
1786
  const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
1803
1787
  if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest failed: doctor repair did not regenerate team skill');
1804
1788
  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');
1789
+ 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');
1790
+ 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');
1791
+ 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');
1792
+ const generatedCleanupReport = doctorRepairJson.repair?.project?.generated_cleanup || {};
1793
+ if (![...(generatedCleanupReport.pruned || []), ...(generatedCleanupReport.already_absent || [])].includes(staleCodexAgentRel)) throw new Error('selftest failed: doctor repair did not report stale generated file pruning');
1805
1794
  if (!(await exists(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md')))) throw new Error('selftest failed: doctor repair removed a user-owned custom skill');
1806
1795
  if (await exists(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove legacy .codex/skills');
1807
1796
  const repairedQuickReference = await safeReadText(path.join(repairTmp, '.codex', 'SNEAKOSCOPE.md'));
@@ -2792,8 +2781,13 @@ async function selftest() {
2792
2781
  const buttonUxSchema = buildQuestionSchema('$Team 버튼 UX 수정');
2793
2782
  const buttonUxSlotIds = buttonUxSchema.slots.map((s) => s.id);
2794
2783
  if (buttonUxSlotIds.includes('UI_STATE_BEHAVIOR') || buttonUxSlotIds.includes('VISUAL_REGRESSION_REQUIRED')) throw new Error('selftest failed: predictable UI defaults should be inferred, not asked');
2784
+ if (buttonUxSlotIds.length) throw new Error(`selftest failed: clear small UI work should auto-seal, got ${buttonUxSlotIds.join(',')}`);
2795
2785
  if (buttonUxSchema.inferred_answers.UI_STATE_BEHAVIOR !== 'infer_from_task_context_and_existing_design_system; preserve existing loading/error/empty/retry behavior unless explicitly requested; add only standard states required by the touched surface') throw new Error('selftest failed: UI state default inference missing');
2796
2786
  if (buttonUxSchema.inferred_answers.VISUAL_REGRESSION_REQUIRED !== 'yes_if_available') throw new Error('selftest failed: visual regression default inference missing');
2787
+ const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
2788
+ const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
2789
+ 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(',')}`);
2790
+ 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');
2797
2791
  const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
2798
2792
  if (pptRoute?.id !== 'PPT') throw new Error('selftest failed: $PPT did not route to presentation pipeline');
2799
2793
  if (JSON.stringify(pptRoute.requiredSkills) !== JSON.stringify(PPT_PIPELINE_SKILL_ALLOWLIST)) throw new Error(`selftest failed: PPT route required skills are not allowlisted: ${pptRoute.requiredSkills.join(',')}`);
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.21';
8
+ export const PACKAGE_VERSION = '0.7.25';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -11,6 +11,8 @@ import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.mjs';
11
11
 
12
12
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
13
13
  const SKS_GENERATED_GIT_PATTERNS = ['.sneakoscope/', '.codex/', '.agents/', 'AGENTS.md'];
14
+ const SKS_SKILL_MANIFEST_FILE = '.sks-generated.json';
15
+ const GENERATED_PRUNE_POLICY = 'remove_previous_sks_generated_paths_absent_from_current_manifest';
14
16
 
15
17
  function reflectionInstructionText(commandPrefix = 'sks') {
16
18
  return `Post-route reflection: full routes load \`reflection\` after work/tests and before final; DFix/Answer/Help/Wiki/SKS discovery are exempt. Write reflection.md; record only real misses/gaps, or no_issue_acknowledged. For lessons, append TriWiki claim rows to ${REFLECTION_MEMORY_PATH}. Run "${commandPrefix} wiki refresh" or pack, validate, then pass reflection-gate.json.`;
@@ -101,6 +103,8 @@ export async function initProject(root, opts = {}) {
101
103
  const requestedHookCommandPrefix = opts.hookCommandPrefix || sksCommandPrefix(installScope, { globalCommand: opts.globalCommand });
102
104
  const hookCommandPrefix = sourceProject ? 'node ./bin/sks.mjs' : requestedHookCommandPrefix;
103
105
  const sine = path.join(root, '.sneakoscope');
106
+ const manifestPath = path.join(sine, 'manifest.json');
107
+ const previousManifest = await readJson(manifestPath, null);
104
108
  if (opts.repair) {
105
109
  const repair = await repairSksGeneratedArtifacts(root, { resetState: Boolean(opts.resetState) });
106
110
  if (repair.removed.length) created.push(`repaired generated SKS files (${repair.removed.length})`);
@@ -114,7 +118,7 @@ export async function initProject(root, opts = {}) {
114
118
  if (localExclude?.path) created.push(`${path.relative(root, localExclude.path)} local-only excludes`);
115
119
  if (sharedIgnore?.changed) created.push(`${path.relative(root, sharedIgnore.path)} SKS generated files ignore`);
116
120
 
117
- await writeJsonAtomic(path.join(sine, 'manifest.json'), {
121
+ const manifest = {
118
122
  package: 'sneakoscope',
119
123
  version: PACKAGE_VERSION,
120
124
  initialized_at: nowIso(),
@@ -197,7 +201,8 @@ export async function initProject(root, opts = {}) {
197
201
  },
198
202
  database_safety: 'destructive_db_operations_denied_always',
199
203
  gx_renderer: 'deterministic_svg_html'
200
- });
204
+ };
205
+ await writeJsonAtomic(manifestPath, manifest);
201
206
  created.push('.sneakoscope/manifest.json');
202
207
 
203
208
  const dbSafetyPath = path.join(sine, 'db-safety.json');
@@ -467,16 +472,36 @@ policy = "Deny destructive database operations, credential exfiltration, persist
467
472
 
468
473
  const skillInstall = await installSkills(root);
469
474
  created.push('.agents/skills/*');
475
+ if (skillInstall.removed_stale_generated_skills.length) created.push(`stale generated skills removed (${skillInstall.removed_stale_generated_skills.length})`);
476
+ if (skillInstall.removed_agent_skill_aliases.length) created.push(`deprecated generated skill aliases removed (${skillInstall.removed_agent_skill_aliases.length})`);
470
477
  if (skillInstall.removed_codex_skill_mirrors.length) created.push(`.codex/skills generated mirrors removed (${skillInstall.removed_codex_skill_mirrors.length})`);
471
- await installCodexAgents(root);
478
+ const agentInstall = await installCodexAgents(root);
472
479
  created.push('.codex/agents/*');
480
+ const generatedFiles = currentGeneratedFileInventory(skillInstall, agentInstall);
481
+ const generatedCleanup = await pruneStaleGeneratedFiles(root, previousManifest, generatedFiles);
482
+ if (generatedCleanup.pruned.length) created.push(`stale generated files pruned (${generatedCleanup.pruned.length})`);
483
+ manifest.generated_files = {
484
+ schema_version: 1,
485
+ generated_by: 'sneakoscope',
486
+ prune_policy: GENERATED_PRUNE_POLICY,
487
+ files: generatedFiles
488
+ };
489
+ manifest.generated_cleanup = {
490
+ schema_version: 1,
491
+ last_run_at: nowIso(),
492
+ previous_version: previousManifest?.version || null,
493
+ current_version: PACKAGE_VERSION,
494
+ pruned: generatedCleanup.pruned,
495
+ already_absent: generatedCleanup.already_absent || []
496
+ };
497
+ await writeJsonAtomic(manifestPath, manifest);
473
498
  await writeHarnessGuardPolicy(root);
474
499
  created.push('.sneakoscope/harness-guard.json');
475
500
  const versionHookCommand = sourceProject ? 'node ./bin/sks.mjs' : hookCommandPrefix;
476
501
  const versionHook = await installVersionGitHook(root, versionHookCommand);
477
502
  if (versionHook.installed) created.push('.git/hooks/pre-commit SKS version guard');
478
503
  else created.push(`version guard skipped (${versionHook.reason})`);
479
- return { created };
504
+ return { created, generated_cleanup: generatedCleanup, skill_install: skillInstall };
480
505
  }
481
506
 
482
507
  async function ensureSharedGitIgnore(root) {
@@ -556,7 +581,7 @@ export async function installSkills(root) {
556
581
  'answer': `---\nname: answer\ndescription: Answer-only research route for ordinary questions that should not start implementation.\n---\n\nUse for explanations, comparisons, status, facts, source-backed research, or docs guidance. Use repo/TriWiki first for project-local facts; hydrate low-trust claims from source. Browse or use Context7 for current external package/API/framework/MCP docs. End with a concise answer summary plus Honest Mode; do not create missions, subagents, or file edits.\n`,
557
582
  'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse local SKS commands: bootstrap, deps, commands, quickstart, codex-app, context7, guard, conflicts, reasoning, wiki, pipeline status, pipeline plan, skill-dream. Promote code-changing work to Team unless Answer/DFix/Help/Wiki/safety route fits. Surface route/guard/scope, use TriWiki, do not edit installed harness files outside this engine repo, and require human-approved conflict cleanup. ${skillDreamPolicyText()}\n`,
558
583
  'wiki': `---\nname: wiki\ndescription: Dollar-command route for $Wiki TriWiki refresh, pack, validate, and prune commands.\n---\n\nUse for $Wiki or Korean wiki-refresh requests. Refresh/update/갱신: run sks wiki refresh, then validate .sneakoscope/wiki/context-pack.json. Pack: run sks wiki pack, then validate. Prune/clean/정리: use sks wiki refresh --prune, or sks wiki prune --dry-run for inspection. Report claims, anchors, trust, attention.use_first/hydrate_first, validation, and blockers. Do not start ambiguity-gated implementation, subagents, or unrelated work.\n`,
559
- 'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
584
+ 'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first, but score goal, constraints, success criteria, and codebase context before asking; ask only the lowest-clarity scope/safety/behavior/acceptance question(s), otherwise auto-seal inferred answers. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
560
585
  'from-chat-img': `---\nname: from-chat-img\ndescription: Explicit $From-Chat-IMG Team alias for chat screenshot plus attachment analysis.\n---\n\nUse only for From-Chat-IMG/$From-Chat-IMG. It enters the normal Team pipeline. Treat uploads as chat screenshot plus originals. Use Codex Computer Use visual inspection when available, list requirements first, match regions to attachments with confidence, write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}, then continue Team gates, review, reflection, and Honest Mode. ${CODEX_COMPUTER_USE_ONLY_POLICY} The ledger must account for every visible customer request, screenshot image region, and separate attachment; ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} must have a checked item for each request, image-region/attachment match, work item, scoped QA-LOOP, and verification step; ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} stores temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}. ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} must prove QA-LOOP ran over the exact customer-request work-order range after implementation, with every work item covered, post-fix verification complete, and zero unresolved findings. team-gate.json cannot pass From-Chat-IMG completion until unresolved_items is empty, every checklist box is checked, and scoped_qa_loop_completed=true.\n`,
561
586
  'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates, Codex Computer Use-only UI evidence, safe fixes, rechecks, and a QA report.\n---\n\nUse only $QA-LOOP. Ask scope, target, mutation, login. Credentials are runtime-only; never save secrets. UI-level E2E needs Codex Computer Use evidence or must be marked unverified; Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, and other browser automation do not satisfy UI/browser verification. Deployed targets are read-only; destructive removal is forbidden. After answer/run, dogfood real flows, apply safe contract-allowed code/test/docs fixes, recheck, and do not pass qa-gate.json with unresolved findings or without post_fix_verification_complete. Finish qa-ledger, date/version report, gate, completion summary, and Honest Mode.\n`,
562
587
  'ppt': `---\nname: ppt\ndescription: $PPT information-first HTML/PDF presentation pipeline with STP, audience, pain-point, format, research, design-system, and verification questions.\n---\n\nUse only when the user invokes $PPT or asks to create a presentation, deck, slides, pitch deck, proposal deck, HTML presentation, or PDF presentation artifact. Before artifact work, seal presentation-specific ambiguity answers: delivery context, target audience profile including role/average age/job/industry/topic familiarity/decision power, STP strategy, decision context and objections, and 3+ pain-point to solution mappings with expected aha moments. Presentation design must be simple, restrained, and information-first: avoid over-designed decoration, ornamental gradients, nested cards, and effects that compete with the message. Design detail should be embedded through typography hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents. ${pptPipelineAllowlistPolicyText()} Use design.md as the only design decision SSOT. If design.md is missing, use docs/Design-Sys-Prompt.md plus getdesign-reference and curated DESIGN.md examples from ${AWESOME_DESIGN_MD_REFERENCE.url} only as source inputs, then fuse them into route-local PPT style tokens with a recorded design_ssot instead of treating references as parallel authorities. If generated image assets are needed, use imagegen only when that asset need is explicitly sealed in the $PPT contract and prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) before API generation. Use web or Context7 evidence only when external facts/libraries/current docs are required by the PPT contract, then create the PDF plus editable source HTML under source-html/, keep independent strategy/render/file-write phases parallel where inputs allow, record ppt-parallel-report.json, and verify readability, overlap, format fit, source coverage, export state, and temporary build files cleanup. Finish with reflection and Honest Mode; do not skip STP/audience questions for presentation artifacts.\n`,
@@ -570,7 +595,7 @@ export async function installSkills(root) {
570
595
  'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\n---\n\nUse only when the user explicitly invokes $MAD-SKS. It can be combined with another route, such as $MAD-SKS $Team or $DB ... $MAD-SKS; in that case the other command remains the primary workflow and MAD-SKS is only the temporary permission grant. The widened DB permission applies only while the active mission gate is open, must be deactivated when the task ends, and opens Supabase MCP column/schema cleanup, direct execute SQL, and normal DB write permissions. Keep only catastrophic database-wipe safeguards: whole database/table removal, all-row delete/update, reset, and dangerous project/branch management remain blocked. Do not carry MAD-SKS permission into later prompts or routes.\n`,
571
596
  'gx': `---\nname: gx\ndescription: Dollar-command route for $GX or $gx deterministic GX visual context cartridges.\n---\n\nUse when the user invokes $GX/$gx or asks for architecture/context visualization through SKS. Prefer sks gx init, render, validate, drift, and snapshot. vgraph.json remains the source of truth.\n`,
572
597
  'help': `---\nname: help\ndescription: Dollar-command route for $Help or $help explaining installed SKS commands and workflows.\n---\n\nUse when the user invokes $Help/$help or asks what commands exist. Prefer concise output from sks commands, sks usage <topic>, sks quickstart, sks aliases, and sks codex-app.\n`,
573
- 'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Ask only scope/safety/behavior/acceptance-changing questions; otherwise seal inferred answers. Materialize pipeline-plan.json for the runtime lane, kept/skipped stages, no-fallback invariant, and verification; inspect with sks pipeline plan, adding --proof-field when changed files are known. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()}\n\n${chatCaptureIntakeText()}\n\nDesign: non-PPT UI/UX reads design.md; if missing use design-system-builder; use imagegen for image/logo/raster, and imagegen must prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) before API generation. For $PPT, ${pptPipelineAllowlistPolicyText()} ${getdesignReferencePolicyText()} TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
598
+ 'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Score ambiguity first using goal, constraints, success criteria, and codebase context; ask only the lowest-clarity scope/safety/behavior/acceptance-changing questions within a small question budget, otherwise seal inferred answers. Materialize pipeline-plan.json for the runtime lane, kept/skipped stages, no-fallback invariant, and verification; inspect with sks pipeline plan, adding --proof-field when changed files are known. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()}\n\n${chatCaptureIntakeText()}\n\nDesign: non-PPT UI/UX reads design.md; if missing use design-system-builder; use imagegen for image/logo/raster, and imagegen must prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) before API generation. For $PPT, ${pptPipelineAllowlistPolicyText()} ${getdesignReferencePolicyText()} TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
574
599
  'reasoning-router': `---\nname: reasoning-router\ndescription: Temporary SKS reasoning-effort routing for every command and pipeline route.\n---\n\nmedium: simple copy/color/discovery/setup/mechanical edits. high: logic, safety, architecture, DB, orchestration, refactor, multi-file work. xhigh: research, AutoResearch, falsification, benchmarks, SEO/GEO, open-ended discovery, and From-Chat-IMG image work-order analysis. Routing is temporary; return to default after the gate. Inspect with sks reasoning and sks pipeline status.\n`,
575
600
  'pipeline-runner': `---\nname: pipeline-runner\ndescription: Execute SKS dollar-command routes as stateful pipelines with mission artifacts, route gates, Context7 evidence, temporary reasoning routing, reflection, and Honest Mode.\n---\n\nEvery $ command is a route. Use current.json, mission artifacts, and pipeline-plan.json as the execution plan: it records the lane, skipped stages, kept stages, verification, and no-unrequested-fallback invariant. Use temporary reasoning, TriWiki before stages, source hydration, Context7 when required, Team cleanup before reflection, reflection for full routes, and completion summary plus Honest Mode before final. Surface guard/scopes, record evidence, refresh/pack/validate TriWiki, and check sks pipeline status/resume/plan. ${speedLanePolicyText()} ${skillDreamPolicyText()}\n`,
576
601
  'context7-docs': `---\nname: context7-docs\ndescription: Enforce Context7 MCP documentation evidence for SKS routes that depend on external libraries, frameworks, APIs, MCPs, package managers, DB SDKs, or generated docs.\n---\n\nWhen required, resolve-library-id, then query-docs for the resolved id. Legacy get-library-docs evidence is accepted. Prefer sks context7 tools/resolve/docs/evidence and finish only after both evidence stages exist. Check setup with sks context7 check.\n`,
@@ -601,13 +626,57 @@ export async function installSkills(root) {
601
626
  await writeSkillMetadata(dir, name);
602
627
  }
603
628
  const skillNames = Object.keys(skills);
629
+ const removedStaleGeneratedSkills = await removeStaleGeneratedSkillsFromManifest(root, skillNames);
630
+ await writeGeneratedSkillManifest(root, skillNames);
604
631
  return {
605
632
  installed_skills: skillNames,
633
+ generated_files: generatedSkillFiles(skillNames),
634
+ removed_stale_generated_skills: removedStaleGeneratedSkills,
606
635
  removed_agent_skill_aliases: await removeGeneratedAgentSkillAliases(root, skillNames),
607
636
  removed_codex_skill_mirrors: await removeGeneratedCodexSkillMirrors(root, skillNames)
608
637
  };
609
638
  }
610
639
 
640
+ function generatedSkillFiles(skillNames) {
641
+ return skillNames.flatMap((name) => [
642
+ `.agents/skills/${name}/SKILL.md`,
643
+ `.agents/skills/${name}/agents/openai.yaml`
644
+ ]).sort();
645
+ }
646
+
647
+ function generatedSkillManifestPath(root) {
648
+ return path.join(root, '.agents', 'skills', SKS_SKILL_MANIFEST_FILE);
649
+ }
650
+
651
+ async function writeGeneratedSkillManifest(root, skillNames) {
652
+ const manifestPath = generatedSkillManifestPath(root);
653
+ await writeJsonAtomic(manifestPath, {
654
+ schema_version: 1,
655
+ generated_by: 'sneakoscope',
656
+ version: PACKAGE_VERSION,
657
+ prune_policy: GENERATED_PRUNE_POLICY,
658
+ skills: [...skillNames].sort(),
659
+ files: generatedSkillFiles(skillNames)
660
+ });
661
+ }
662
+
663
+ async function removeStaleGeneratedSkillsFromManifest(root, skillNames) {
664
+ const previous = await readJson(generatedSkillManifestPath(root), null);
665
+ const previousSkills = Array.isArray(previous?.skills) ? previous.skills : [];
666
+ if (!previousSkills.length) return [];
667
+ const current = new Set(skillNames);
668
+ const removed = [];
669
+ for (const name of previousSkills) {
670
+ const skillName = String(name || '').trim();
671
+ if (!skillName || current.has(skillName) || !/^[a-z0-9-]+$/.test(skillName)) continue;
672
+ const dir = path.join(root, '.agents', 'skills', skillName);
673
+ if (!(await exists(dir))) continue;
674
+ await fsp.rm(dir, { recursive: true, force: true });
675
+ removed.push(path.relative(root, dir));
676
+ }
677
+ return removed.sort();
678
+ }
679
+
611
680
  function enrichSkillContent(name, content) {
612
681
  if (!['sks', 'answer', 'wiki', 'team', 'qa-loop', 'ppt', 'computer-use', 'computer-use-fast', 'cu', 'goal', 'research', 'autoresearch', 'db', 'gx', 'reflection', 'prompt-pipeline', 'pipeline-runner', 'context7-docs', 'turbo-context-pack', 'hproof-evidence-bind'].includes(name)) return content;
613
682
  const text = String(content || '').trimEnd();
@@ -704,4 +773,89 @@ async function installCodexAgents(root) {
704
773
  for (const [file, content] of Object.entries(agents)) {
705
774
  await writeTextAtomic(path.join(dir, file), content);
706
775
  }
776
+ return {
777
+ installed_agents: Object.keys(agents),
778
+ generated_files: Object.keys(agents).map((file) => `.codex/agents/${file}`).sort()
779
+ };
780
+ }
781
+
782
+ function currentGeneratedFileInventory(skillInstall = {}, agentInstall = {}) {
783
+ return Array.from(new Set([
784
+ '.codex/config.toml',
785
+ '.codex/SNEAKOSCOPE.md',
786
+ '.codex/hooks.json',
787
+ '.sneakoscope/harness-guard.json',
788
+ '.sneakoscope/db-safety.json',
789
+ '.sneakoscope/policy.json',
790
+ '.agents/skills/.sks-generated.json',
791
+ ...(Array.isArray(skillInstall.generated_files) ? skillInstall.generated_files : []),
792
+ ...(Array.isArray(agentInstall.generated_files) ? agentInstall.generated_files : [])
793
+ ])).sort();
794
+ }
795
+
796
+ async function pruneStaleGeneratedFiles(root, previousManifest, currentFiles) {
797
+ const previousFiles = Array.isArray(previousManifest?.generated_files?.files) ? previousManifest.generated_files.files : [];
798
+ if (!previousFiles.length) return { pruned: [] };
799
+ const current = new Set(currentFiles);
800
+ const pruned = [];
801
+ const already_absent = [];
802
+ for (const rel of previousFiles) {
803
+ const relPath = normalizeGeneratedRelPath(rel);
804
+ if (!relPath || current.has(relPath) || !isPrunableGeneratedPath(relPath)) continue;
805
+ const removed = await removeGeneratedRelPath(root, relPath);
806
+ if (removed) pruned.push(removed);
807
+ else already_absent.push(relPath);
808
+ }
809
+ return { pruned: pruned.sort(), already_absent: already_absent.sort() };
810
+ }
811
+
812
+ function normalizeGeneratedRelPath(value) {
813
+ const rel = String(value || '').trim().replaceAll('\\', '/');
814
+ if (!rel || rel.startsWith('/') || rel.includes('\0')) return null;
815
+ if (rel.split('/').some((part) => part === '..')) return null;
816
+ return rel;
817
+ }
818
+
819
+ function isPrunableGeneratedPath(rel) {
820
+ if (rel.startsWith('.agents/skills/')) return true;
821
+ if (rel.startsWith('.codex/agents/')) return true;
822
+ if (rel.startsWith('.codex/skills/')) return true;
823
+ return new Set([
824
+ '.codex/config.toml',
825
+ '.codex/SNEAKOSCOPE.md',
826
+ '.codex/hooks.json',
827
+ '.sneakoscope/harness-guard.json',
828
+ '.sneakoscope/db-safety.json',
829
+ '.sneakoscope/policy.json'
830
+ ]).has(rel);
831
+ }
832
+
833
+ async function removeGeneratedRelPath(root, rel) {
834
+ const absRoot = path.resolve(root);
835
+ const abs = path.resolve(absRoot, rel);
836
+ if (abs !== absRoot && !abs.startsWith(`${absRoot}${path.sep}`)) return null;
837
+ if (!(await exists(abs))) return null;
838
+ await fsp.rm(abs, { recursive: true, force: true });
839
+ await removeEmptyGeneratedParents(root, rel);
840
+ return rel;
841
+ }
842
+
843
+ async function removeEmptyGeneratedParents(root, rel) {
844
+ const parts = rel.split('/');
845
+ if (parts.length <= 1) return;
846
+ const stopDirs = new Set([
847
+ path.resolve(root, '.agents', 'skills'),
848
+ path.resolve(root, '.codex', 'agents'),
849
+ path.resolve(root, '.codex', 'skills'),
850
+ path.resolve(root, '.codex'),
851
+ path.resolve(root, '.sneakoscope')
852
+ ]);
853
+ let dir = path.resolve(root, ...parts.slice(0, -1));
854
+ while (!stopDirs.has(dir) && dir.startsWith(path.resolve(root))) {
855
+ await removeDirIfEmpty(dir);
856
+ const parent = path.dirname(dir);
857
+ if (parent === dir) break;
858
+ dir = parent;
859
+ }
860
+ if (rel.startsWith('.codex/skills/')) await removeDirIfEmpty(path.join(root, '.codex', 'skills'));
707
861
  }
@@ -60,6 +60,134 @@ function hasAnswer(value) {
60
60
  return true;
61
61
  }
62
62
 
63
+ const AMBIGUITY_READY_THRESHOLD = 0.2;
64
+ const CLARITY_FLOORS = {
65
+ goal: 0.75,
66
+ constraints: 0.65,
67
+ success: 0.7,
68
+ context: 0.6
69
+ };
70
+ const CLARITY_WEIGHTS = {
71
+ goal: 0.35,
72
+ constraints: 0.25,
73
+ success: 0.25,
74
+ context: 0.15
75
+ };
76
+
77
+ function clamp01(n) {
78
+ if (!Number.isFinite(n)) return 0;
79
+ return Math.max(0, Math.min(1, n));
80
+ }
81
+
82
+ function hasAny(re, text) {
83
+ return re.test(text);
84
+ }
85
+
86
+ function scoreComponent(name, clarity, weight, justification) {
87
+ return {
88
+ name,
89
+ clarity_score: Number(clamp01(clarity).toFixed(2)),
90
+ weight,
91
+ ambiguity_contribution: Number(((1 - clamp01(clarity)) * weight).toFixed(3)),
92
+ justification
93
+ };
94
+ }
95
+
96
+ function summarizeAnswer(value) {
97
+ if (Array.isArray(value)) return value.map((v) => String(v || '').trim()).filter(Boolean).join('; ');
98
+ return String(value || '').trim();
99
+ }
100
+
101
+ function promptedGoalFromAnswers(explicitAnswers = {}) {
102
+ const target = summarizeAnswer(explicitAnswers.INTENT_TARGET);
103
+ const outcome = summarizeAnswer(explicitAnswers.REQUIRED_OUTCOME || explicitAnswers.SUCCESS_CRITERIA_OR_ACCEPTANCE);
104
+ if (target && outcome) return `${target}: ${outcome}`;
105
+ return target || outcome || '';
106
+ }
107
+
108
+ function promptHasExplicitAcceptance(lower) {
109
+ return /완료\s*기준|성공\s*기준|acceptance|criteria|definition of done|검증|테스트|pass|green|확인|완성도|완전히|처음부터|바로\s*보이|노출|표시|end[- ]?to[- ]?end/.test(lower);
110
+ }
111
+
112
+ function promptHasTarget(text, lower) {
113
+ return /[`'"][^`'"]+[`'"]/.test(text)
114
+ || /(?:^|\s)(?:src|bin|scripts|docs|README|CHANGELOG|package\.json|\.sneakoscope|\.agents|\.codex|[A-Za-z0-9_.-]+\/)[^\s,)]*/.test(text)
115
+ || /\$[A-Za-z0-9_-]+/.test(text)
116
+ || /(모호성|질문|파이프라인|게이트|라우트|화면|버튼|모달|디자인|레이아웃|컴포넌트|프론트|리드미|코덱스|결제|로그인|인증|세션|codex|route|pipeline|ambiguity|clarification|question|decision[- ]?contract|hyperplan|prometheus|ouroboros|openagent|payment|billing|auth|session|팀|team|qa|ppt|db|ui|ux|설치|버전|readme|changelog)/.test(lower);
117
+ }
118
+
119
+ function promptHasAction(lower) {
120
+ return /(구현|수정|개선|고쳐|만들|추가|삭제|정리|리팩터|바꿔|교체|재설계|처음부터|알려|보이게|보여|노출|표시|rebuild|rewrite|implement|fix|improve|add|remove|refactor|change|replace|redesign|reverse engineer)/.test(lower);
121
+ }
122
+
123
+ function promptIsUnderspecified(lower) {
124
+ const trimmed = lower.trim();
125
+ return trimmed.length < 12
126
+ || /^(이거|저거|그거|뭔가|문제|고쳐줘|수정해줘|개선해줘|해줘|fix this|improve this|do it)\s*[.!?。]*$/.test(trimmed)
127
+ || /^(이거|저거|그거)\s+(고쳐|수정|개선|해줘)/.test(trimmed);
128
+ }
129
+
130
+ function promptHasRisk(lower) {
131
+ return /(운영|production|prod|live|배포|publish|release|결제|payment|billing|auth|인증|보안|security|db|database|supabase|postgres|sql|schema|migration|마이그레이션|삭제|delete|drop|truncate|reset|권한|permission|credential|secret)/.test(lower);
132
+ }
133
+
134
+ function promptHasContextTarget(text, lower) {
135
+ return promptHasTarget(text, lower)
136
+ || /https?:\/\/\S+/.test(text)
137
+ || /(프로젝트|repo|repository|codebase|코드베이스|현재 코드|current code|기존|existing|local|로컬)/.test(lower);
138
+ }
139
+
140
+ export function buildAmbiguityAssessment(prompt, explicitAnswers = {}) {
141
+ const text = String(prompt || '');
142
+ const lower = text.toLowerCase();
143
+ const target = promptHasTarget(text, lower) || hasAnswer(explicitAnswers.INTENT_TARGET) || hasAnswer(explicitAnswers.GOAL_PRECISE);
144
+ const action = promptHasAction(lower) || hasAnswer(explicitAnswers.REQUIRED_OUTCOME) || hasAnswer(explicitAnswers.GOAL_PRECISE);
145
+ const underspecified = promptIsUnderspecified(lower);
146
+ const acceptance = promptHasExplicitAcceptance(lower) || hasAnswer(explicitAnswers.ACCEPTANCE_CRITERIA) || hasAnswer(explicitAnswers.SUCCESS_CRITERIA_OR_ACCEPTANCE);
147
+ const risk = promptHasRisk(lower);
148
+ const contextTarget = promptHasContextTarget(text, lower) || hasAnswer(explicitAnswers.CODEBASE_CONTEXT_TARGET);
149
+ const predictableSafetyDefault = /(재시도|retry|세션\s*만료|session\s*expired|session\s*expiry|token\s*expired)/.test(lower);
150
+ const hasPolicy = hasAnswer(explicitAnswers.RISK_BOUNDARY) || hasAnswer(explicitAnswers.RISK_AND_BOUNDARY) || predictableSafetyDefault || /(하지\s*마|금지|no\s+|never|묻지|보존|preserve|safe|안전|검증|approval|승인|알아서|판단|추론|infer|default|기본)/.test(lower);
151
+ const hasMultipleChoiceRisk = /(\bor\b|또는|아니면|선택|둘 중|여러|multiple|대안)/.test(lower) && !/(알아서|판단|infer|추론|default|기본)/.test(lower);
152
+
153
+ const goalClarity = underspecified ? (target || action ? 0.45 : 0.2) : (target && action ? 0.9 : target || action ? 0.62 : 0.25);
154
+ const constraintClarity = risk ? (hasPolicy ? 0.78 : 0.64) : 0.82;
155
+ const successClarity = acceptance ? 0.86 : (target && action && !risk ? 0.73 : target && action ? 0.66 : 0.38);
156
+ const contextClarity = contextTarget ? 0.82 : (underspecified ? 0.35 : 0.62);
157
+ const components = {
158
+ goal: scoreComponent('goal_clarity', goalClarity, CLARITY_WEIGHTS.goal, target && action ? 'target and action are present' : 'target or action is missing'),
159
+ constraints: scoreComponent('constraint_clarity', constraintClarity, CLARITY_WEIGHTS.constraints, risk ? (hasPolicy ? 'risk cues include a policy boundary' : 'risk cues need a boundary') : 'no high-risk cue detected'),
160
+ success: scoreComponent('success_criteria_clarity', successClarity, CLARITY_WEIGHTS.success, acceptance ? 'success or verification language is explicit' : 'success criteria can be inferred only if goal/risk are clear enough'),
161
+ context: scoreComponent('context_clarity', contextClarity, CLARITY_WEIGHTS.context, contextTarget ? 'target context is named or discoverable' : 'target context is not discoverable from prompt')
162
+ };
163
+ const overall = Object.values(components).reduce((sum, item) => sum + item.ambiguity_contribution, 0);
164
+ const floorFailures = [];
165
+ if (components.goal.clarity_score < CLARITY_FLOORS.goal) floorFailures.push('goal_clarity');
166
+ if (components.constraints.clarity_score < CLARITY_FLOORS.constraints) floorFailures.push('constraint_clarity');
167
+ if (components.success.clarity_score < CLARITY_FLOORS.success) floorFailures.push('success_criteria_clarity');
168
+ if (components.context.clarity_score < CLARITY_FLOORS.context) floorFailures.push('context_clarity');
169
+ const unresolved = [];
170
+ if (components.goal.clarity_score < CLARITY_FLOORS.goal) unresolved.push('intent_target_or_required_outcome');
171
+ if (components.success.clarity_score < CLARITY_FLOORS.success && (!target || !action || risk)) unresolved.push('success_criteria_or_acceptance');
172
+ if (components.constraints.clarity_score < CLARITY_FLOORS.constraints || hasMultipleChoiceRisk) unresolved.push('risk_boundary_or_choice');
173
+ if (components.context.clarity_score < CLARITY_FLOORS.context) unresolved.push('codebase_context_target');
174
+ const uniqueUnresolved = [...new Set(unresolved)];
175
+ return {
176
+ schema_version: 1,
177
+ method: 'weighted_clarity_interview',
178
+ inspired_by: ['ouroboros_ambiguity_threshold', 'prometheus_interview_plan_first', 'hyperplan_adversarial_lenses'],
179
+ threshold: AMBIGUITY_READY_THRESHOLD,
180
+ overall_score: Number(overall.toFixed(3)),
181
+ ready_for_contract: overall <= AMBIGUITY_READY_THRESHOLD && floorFailures.length === 0,
182
+ component_floors_passed: floorFailures.length === 0,
183
+ floor_failures: floorFailures,
184
+ components,
185
+ unresolved_dimensions: uniqueUnresolved,
186
+ question_budget: risk ? 3 : 2,
187
+ adversarial_lenses: ['challenge_framing', 'subtract_unneeded_surface', 'demand_evidence', 'test_integration_risk', 'consider_simpler_alternative']
188
+ };
189
+ }
190
+
63
191
  function addInferred(out, notes, id, value, note) {
64
192
  if (!hasAnswer(value) && !(Array.isArray(value) && value.length === 0)) return;
65
193
  out[id] = value;
@@ -78,6 +206,7 @@ function looksLikePresentationArtifactPrompt(lower) {
78
206
  export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
79
207
  const text = `${prompt || ''}\n${explicitAnswers.GOAL_PRECISE || ''}`;
80
208
  const lower = text.toLowerCase();
209
+ const ambiguity = buildAmbiguityAssessment(prompt, explicitAnswers);
81
210
  const inferred = {};
82
211
  const notes = {};
83
212
  const normalizedPrompt = String(prompt || '')
@@ -122,26 +251,30 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
122
251
  presentation: ['audience profile and STP strategy are explicit before artifact creation', 'target pain points map to proposed solution moments', 'decision context and likely objections are sealed before storyboarding', 'presentation format, device, and delivery context are fixed before design work'],
123
252
  install: ['bootstrap/deps initialize readiness', 'missing runtime deps show repair actions', 'readiness output is concrete']
124
253
  };
125
- if (!hasAnswer(explicitAnswers.GOAL_PRECISE)) {
254
+ const explicitPromptedGoal = promptedGoalFromAnswers(explicitAnswers);
255
+ const canInferCoreGoal = explicitPromptedGoal || !ambiguity.unresolved_dimensions.includes('intent_target_or_required_outcome');
256
+ if (!hasAnswer(explicitAnswers.GOAL_PRECISE) && canInferCoreGoal) {
126
257
  addInferred(
127
258
  inferred,
128
259
  notes,
129
260
  'GOAL_PRECISE',
130
- presentationWork ? goals.presentation : (kind ? goals[kind] : (normalizedPrompt ? `사용자 요청을 현재 코드 기준으로 구현한다: ${normalizedPrompt}` : '사용자 요청을 현재 코드 기준으로 구현한다')),
131
- presentationWork ? 'presentation' : (kind || 'prompt-derived-goal')
261
+ explicitPromptedGoal || (presentationWork ? goals.presentation : (kind ? goals[kind] : (normalizedPrompt ? `사용자 요청을 현재 코드 기준으로 구현한다: ${normalizedPrompt}` : '사용자 요청을 현재 코드 기준으로 구현한다'))),
262
+ explicitPromptedGoal ? 'user-answered-dynamic-intent' : (presentationWork ? 'presentation' : (kind || 'prompt-derived-goal'))
132
263
  );
133
264
  }
134
- if (!hasAnswer(explicitAnswers.ACCEPTANCE_CRITERIA)) {
265
+ const explicitAcceptance = explicitAnswers.SUCCESS_CRITERIA_OR_ACCEPTANCE;
266
+ const canInferAcceptance = hasAnswer(explicitAcceptance) || Boolean(kind || presentationWork || paymentWork || authWork) || !ambiguity.unresolved_dimensions.includes('success_criteria_or_acceptance');
267
+ if (!hasAnswer(explicitAnswers.ACCEPTANCE_CRITERIA) && canInferAcceptance) {
135
268
  addInferred(
136
269
  inferred,
137
270
  notes,
138
271
  'ACCEPTANCE_CRITERIA',
139
- presentationWork ? criteria.presentation : (kind ? criteria[kind] : [
272
+ hasAnswer(explicitAcceptance) ? explicitAcceptance : (presentationWork ? criteria.presentation : (kind ? criteria[kind] : [
140
273
  'requested behavior is implemented in the relevant code path',
141
274
  'relevant tests/checks pass or any unavailable check is explicitly justified',
142
275
  'final response states what was changed, verified, and left unverified'
143
- ]),
144
- presentationWork ? 'presentation' : (kind || 'default-implementation-criteria')
276
+ ])),
277
+ hasAnswer(explicitAcceptance) ? 'user-answered-dynamic-acceptance' : (presentationWork ? 'presentation' : (kind || 'default-implementation-criteria'))
145
278
  );
146
279
  }
147
280
 
@@ -157,11 +290,12 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
157
290
  }
158
291
  if (!hasAnswer(explicitAnswers.RISK_BOUNDARY)) {
159
292
  addInferred(inferred, notes, 'RISK_BOUNDARY', [
293
+ ...(hasAnswer(explicitAnswers.RISK_AND_BOUNDARY) ? [summarizeAnswer(explicitAnswers.RISK_AND_BOUNDARY)] : []),
160
294
  'no npm publish unless explicitly requested',
161
295
  'do not revert unrelated changes',
162
296
  'no destructive commands or live data writes',
163
297
  'no unrequested fallback implementation code'
164
- ], 'safety');
298
+ ], hasAnswer(explicitAnswers.RISK_AND_BOUNDARY) ? 'user-answered-dynamic-risk-boundary' : 'safety');
165
299
  }
166
300
  if (uiuxWork) {
167
301
  if (!hasAnswer(explicitAnswers.UI_STATE_BEHAVIOR)) {
@@ -257,36 +391,47 @@ export function buildQuestionSchema(prompt) {
257
391
  if (/\b(ui|modal|screen|button|visual|design|layout|component|prototype|frontend)\b|화면|버튼|모달|디자인|레이아웃|컴포넌트|프론트|시각|발표자료|디자인\s*시스템/.test(lower)) domainHints.push('uiux');
258
392
  if (looksLikePresentationArtifactPrompt(lower)) domainHints.push('presentation');
259
393
  if (/db|database|schema|migration|테이블|마이그레이션|supabase|postgres|sql/.test(lower)) domainHints.push('db');
260
- const slots = [
261
- { id: 'GOAL_PRECISE', question: '이번 작업의 최종 목표를 한 문장으로 정확히 정의해주세요.', required: true, type: 'string' },
262
- { id: 'ACCEPTANCE_CRITERIA', question: '완료 기준을 항목으로 적어주세요. 최소 2개 이상 권장합니다.', required: true, type: 'array_or_string' },
263
- { id: 'NON_GOALS', question: '이번 작업에서 제외할 범위가 있나요? 없으면 빈 배열로 답해주세요.', required: true, type: 'array_or_string', allow_empty: true },
264
- { id: 'PUBLIC_API_CHANGE_ALLOWED', question: 'public API 또는 외부 계약 변경을 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_needed', 'yes'] },
265
- { id: 'DB_SCHEMA_CHANGE_ALLOWED', question: 'DB schema 또는 migration 변경을 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_needed', 'yes_with_migration'] },
266
- { id: 'DEPENDENCY_CHANGE_ALLOWED', question: '새 dependency 추가를 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_already_approved', 'yes'] },
267
- { id: 'TEST_SCOPE', question: '완료 전 실행 또는 정당화해야 할 테스트 범위를 지정해주세요.', required: true, type: 'array_or_string', examples: ['unit', 'integration', 'e2e', 'lint', 'typecheck'] },
268
- { id: 'MID_RUN_UNKNOWN_POLICY', question: '실행 중 새 모호성이 생기면 사용자에게 묻지 않고 어떤 해결 순서로 판단할까요? 이 항목은 대체 구현 또는 fallback 코드를 새로 만드는 허가가 아닙니다.', required: true, type: 'array', options: ['preserve_existing_behavior', 'smallest_reversible_change', 'defer_optional_scope', 'block_only_if_no_safe_path'] },
269
- { id: 'RISK_BOUNDARY', question: '보안, 결제, 데이터 손상, 권한, 인증 등 절대 넘으면 안 되는 위험 경계를 적어주세요.', required: true, type: 'array_or_string' },
270
-
271
- { id: 'DATABASE_TARGET_ENVIRONMENT', question: 'DB 관련 작업의 대상 환경을 지정해주세요. production write는 Sneakoscope Codex가 허용하지 않습니다.', required: true, type: 'enum', options: ['no_database', 'local_dev', 'preview_branch', 'supabase_branch', 'production_read_only'] },
272
- { id: 'DATABASE_WRITE_MODE', question: 'DB 쓰기 정책을 선택해주세요. Supabase/Postgres MCP live write는 기본 차단됩니다.', required: true, type: 'enum', options: ['read_only_only', 'migration_files_only', 'non_destructive_writes_to_local_or_branch_only'] },
273
- { id: 'SUPABASE_MCP_POLICY', question: 'Supabase MCP를 사용한다면 어떤 안전 정책을 적용할까요?', required: true, type: 'enum', options: ['not_used', 'read_only_project_scoped_only', 'branch_only_no_live_writes'] },
274
- { id: 'DESTRUCTIVE_DB_OPERATIONS_ALLOWED', question: 'DROP/TRUNCATE/DB reset/mass DELETE/branch reset/project delete 같은 파괴적 DB 작업을 허용하나요? Sneakoscope Codex는 never만 허용합니다.', required: true, type: 'enum', options: ['never'] },
275
- { id: 'DB_BACKUP_OR_BRANCH_REQUIRED', question: 'DB 쓰기가 필요한 경우 local/preview branch 또는 백업이 있어야만 진행하도록 할까요?', required: true, type: 'enum', options: ['yes_for_any_write'] },
276
- { id: 'DB_MAX_BLAST_RADIUS', question: 'DML이 꼭 필요한 경우 허용 가능한 최대 영향 범위를 적어주세요. 기본 권장값은 no_live_dml입니다.', required: true, type: 'string' }
277
- ];
278
- if (domainHints.includes('payment')) {
394
+ const ambiguity = buildAmbiguityAssessment(prompt);
395
+ const slots = [];
396
+ const presentationSpecific = domainHints.includes('presentation');
397
+ if (!presentationSpecific && ambiguity.unresolved_dimensions.includes('intent_target_or_required_outcome')) {
279
398
  slots.push(
280
- { id: 'PAYMENT_SUCCESS_INVARIANT', question: '이미 성공 처리된 결제에 대해서는 어떤 invariant를 보존해야 하나요?', required: true, type: 'string' },
281
- { id: 'PAYMENT_RETRY_POLICY', question: '재시도 횟수, backoff, 실패 최종 상태 정책을 지정해주세요.', required: true, type: 'string' }
399
+ { id: 'INTENT_TARGET', question: '실제로 바꿀 대상과 원하는 결과를 문장으로만 적어주세요. 파일/화면/기능명이 있으면 같이 적어주세요.', required: true, type: 'string' }
282
400
  );
283
401
  }
284
- if (domainHints.includes('auth')) {
402
+ if (!presentationSpecific && ambiguity.unresolved_dimensions.includes('success_criteria_or_acceptance')) {
285
403
  slots.push(
286
- { id: 'AUTH_SESSION_EXPIRED_BEHAVIOR', question: '세션/토큰 만료 사용자가 보게 UX 또는 API 동작을 지정해주세요.', required: true, type: 'string' },
287
- { id: 'AUTH_PROTOCOL_CHANGE_ALLOWED', question: '인증 프로토콜 변경을 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_needed', 'yes'] }
404
+ { id: 'SUCCESS_CRITERIA_OR_ACCEPTANCE', question: '완료라고 판단할 있는 관찰 가능한 기준을 1~3개만 적어주세요. 모르면 “현재 코드 기준으로 판단”이라고 적어도 됩니다.', required: true, type: 'array_or_string' }
288
405
  );
289
406
  }
407
+ if (ambiguity.unresolved_dimensions.includes('risk_boundary_or_choice')) {
408
+ slots.push(
409
+ { id: 'RISK_AND_BOUNDARY', question: '여러 선택지가 있거나 위험한 변경이 있다면 반드시 지켜야 할 경계만 적어주세요. 없으면 “기존 동작 보존, 파괴적 작업 금지”라고 답해주세요.', required: true, type: 'string' }
410
+ );
411
+ }
412
+ if (ambiguity.unresolved_dimensions.includes('codebase_context_target')) {
413
+ slots.push(
414
+ { id: 'CODEBASE_CONTEXT_TARGET', question: '이 요청이 가리키는 repo/브랜치/화면/파일/최근 오류 맥락을 알려주세요.', required: true, type: 'string' }
415
+ );
416
+ }
417
+ if (domainHints.includes('payment')) {
418
+ const inferred = inferAnswersForPrompt(prompt);
419
+ if (!hasAnswer(inferred.answers.PAYMENT_SUCCESS_INVARIANT)) {
420
+ slots.push({ id: 'PAYMENT_SUCCESS_INVARIANT', question: '이미 성공 처리된 결제에 대해서는 어떤 invariant를 보존해야 하나요?', required: true, type: 'string' });
421
+ }
422
+ if (!hasAnswer(inferred.answers.PAYMENT_RETRY_POLICY)) {
423
+ slots.push({ id: 'PAYMENT_RETRY_POLICY', question: '재시도 횟수, backoff, 실패 최종 상태 정책을 지정해주세요.', required: true, type: 'string' });
424
+ }
425
+ }
426
+ if (domainHints.includes('auth')) {
427
+ const inferred = inferAnswersForPrompt(prompt);
428
+ if (!hasAnswer(inferred.answers.AUTH_SESSION_EXPIRED_BEHAVIOR)) {
429
+ slots.push({ id: 'AUTH_SESSION_EXPIRED_BEHAVIOR', question: '세션/토큰 만료 시 사용자가 보게 될 UX 또는 API 동작을 지정해주세요.', required: true, type: 'string' });
430
+ }
431
+ if (!hasAnswer(inferred.answers.AUTH_PROTOCOL_CHANGE_ALLOWED)) {
432
+ slots.push({ id: 'AUTH_PROTOCOL_CHANGE_ALLOWED', question: '인증 프로토콜 변경을 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_needed', 'yes'] });
433
+ }
434
+ }
290
435
  if (domainHints.includes('uiux')) {
291
436
  slots.push(
292
437
  { id: 'UI_STATE_BEHAVIOR', question: '로딩, 에러, 빈 상태, 재시도 등 UI 상태별 기대 동작을 지정해주세요.', required: true, type: 'string' },
@@ -308,15 +453,22 @@ export function buildQuestionSchema(prompt) {
308
453
  { id: 'DB_READ_ONLY_QUERY_LIMIT', question: 'MCP/SQL read-only 조회 시 기본 LIMIT를 몇으로 둘까요?', required: true, type: 'string' }
309
454
  );
310
455
  }
311
- const skippedByDefault = new RegExp('^(D' + 'B_|D' + 'ATABASE_|D' + 'ESTRUCTIVE_D' + 'B_|SUPA' + 'BASE_)');
312
456
  const inferred = inferAnswersForPrompt(prompt);
313
- const inferredSlots = new Set(['MID_RUN_UNKNOWN_POLICY', ...Object.keys(inferred.answers)]);
314
- const askedSlots = slots.filter((s) => !inferredSlots.has(s.id) && (domainHints.includes('db') || !skippedByDefault.test(s.id)));
457
+ const inferredSlots = new Set(Object.keys(inferred.answers));
458
+ const askedSlots = slots
459
+ .filter((s) => {
460
+ if (inferredSlots.has(s.id)) return false;
461
+ if (s.id === 'INTENT_TARGET' && hasAnswer(inferred.answers.GOAL_PRECISE)) return false;
462
+ if (s.id === 'SUCCESS_CRITERIA_OR_ACCEPTANCE' && hasAnswer(inferred.answers.ACCEPTANCE_CRITERIA)) return false;
463
+ return true;
464
+ })
465
+ .slice(0, domainHints.includes('presentation') ? slots.length : ambiguity.question_budget);
315
466
  return {
316
- schema_version: 1,
317
- description: 'Only slots that can change scope, safety, behavior, or acceptance are asked. The rest is inferred from TriWiki/current code defaults. After the contract is sealed, SKS resolves with the decision ladder instead of asking mid-run questions.',
467
+ schema_version: 2,
468
+ description: 'SKS scores goal, constraints, success criteria, and codebase context first, then asks only the lowest-clarity questions that can change execution. The rest is inferred from the prompt, TriWiki/current-code defaults, and conservative SKS safety policy. After the contract is sealed, SKS resolves with the decision ladder instead of asking mid-run questions.',
318
469
  prompt,
319
470
  domain_hints: domainHints,
471
+ ambiguity_assessment: ambiguity,
320
472
  inferred_answers: inferred.answers,
321
473
  inference_notes: inferred.notes,
322
474
  slots: askedSlots
@@ -339,6 +491,15 @@ export function questionsMarkdown(schema) {
339
491
  lines.push('사용자 의도가 실제로 모호한 항목만 묻고, 나머지는 TriWiki/current-code 기본값으로 추론합니다.');
340
492
  }
341
493
  if (schema.description) lines.push(schema.description);
494
+ if (schema.ambiguity_assessment) {
495
+ lines.push('');
496
+ lines.push('## Ambiguity Assessment');
497
+ lines.push('');
498
+ lines.push(`- method: ${schema.ambiguity_assessment.method}`);
499
+ lines.push(`- score: ${schema.ambiguity_assessment.overall_score} (ready threshold <= ${schema.ambiguity_assessment.threshold})`);
500
+ lines.push(`- unresolved dimensions: ${(schema.ambiguity_assessment.unresolved_dimensions || []).join(', ') || 'none'}`);
501
+ lines.push(`- question budget: ${schema.ambiguity_assessment.question_budget}`);
502
+ }
342
503
  if (schema.inferred_answers && Object.keys(schema.inferred_answers).length) {
343
504
  lines.push('');
344
505
  lines.push('## Inferred Answers');