sneakoscope 0.6.87 → 0.6.90

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
@@ -90,6 +90,8 @@ sks bootstrap
90
90
 
91
91
  `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.
92
92
 
93
+ 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`.
94
+
93
95
  ### One-Shot Install
94
96
 
95
97
  Use this when you do not want to keep a global install:
@@ -256,6 +258,8 @@ Generated app files include:
256
258
  | `.codex/config.toml` | Codex profiles, agents, and MCP configuration. |
257
259
  | `.sneakoscope/` | Runtime state, missions, wiki packs, policies, and artifacts. |
258
260
 
261
+ Default setup adds these generated SKS paths to the project `.gitignore`; `--local-only` uses `.git/info/exclude` instead.
262
+
259
263
  Use `sks dollar-commands` to confirm that terminal discovery and Codex App prompt commands agree.
260
264
 
261
265
  TriWiki is intentionally sparse: `sks wiki sweep` records demote, soft-forget, archive, delete, promote-to-skill, and promote-to-rule candidates instead of injecting every old claim into future prompts. `sks harness fixture` validates the broader Harness Growth Factory contract: deliberate forgetting fixtures, skill card metadata, experiment schema, tool-error taxonomy, permission profiles, MultiAgentV2 defaults, and Warp cockpit view coverage. `sks code-structure scan` flags handwritten files above 1000/2000/3000-line thresholds so new logic can be extracted before command files become harder to maintain.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.6.87",
4
+ "version": "0.6.90",
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",
package/src/cli/main.mjs CHANGED
@@ -1385,6 +1385,7 @@ async function codexAppHelp(args = []) {
1385
1385
  `Skills: project=${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} global=${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`}`, '',
1386
1386
  'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks warp check', '',
1387
1387
  'Generated files:', ' .codex/config.toml', ' .codex/hooks.json', ' .agents/skills/', ' .codex/agents/', ' .codex/SNEAKOSCOPE.md', ' AGENTS.md', '',
1388
+ 'Git ignore:', ' default setup writes .gitignore entries for .sneakoscope/, .codex/, .agents/, AGENTS.md', ' --local-only writes those patterns to .git/info/exclude instead', '',
1388
1389
  'Prompt routes:', formatDollarCommandsCompact(' ')
1389
1390
  ].join('\n'));
1390
1391
  }
@@ -1564,6 +1565,7 @@ async function setup(args) {
1564
1565
  console.log(`Hooks: ${path.relative(root, hooksPath)}`);
1565
1566
  console.log(`Version: ${versioningInfo.enabled ? (versioningInfo.hook_installed ? 'auto-bump enabled' : 'auto-bump hook missing') : 'not enabled'}${versioningInfo.package_version ? ` (${versioningInfo.package_version})` : ''}`);
1566
1567
  if (localOnly) console.log('Git: local-only (.git/info/exclude; user AGENTS preserved, SKS managed block refreshed)');
1568
+ else console.log('Git: .gitignore ignores SKS generated files');
1567
1569
  console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
1568
1570
  console.log(`Global $: ${globalSkills.status === 'installed' ? 'ok' : globalSkills.status} ${globalSkills.root || ''}`.trimEnd());
1569
1571
  console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser Use=${appRuntime.mcp.has_browser_use ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'}`);
@@ -1759,6 +1761,7 @@ async function init(args) {
1759
1761
  console.log(`Initialized ㅅㅋㅅ in ${root}`);
1760
1762
  console.log(`Install scope: ${installScope} (${sksCommandPrefix(installScope, { globalCommand })})`);
1761
1763
  if (localOnly) console.log('Git mode: local-only (.git/info/exclude)');
1764
+ else console.log('Git mode: shared .gitignore');
1762
1765
  for (const x of res.created) console.log(`- ${x}`);
1763
1766
  }
1764
1767
 
@@ -1921,6 +1924,24 @@ async function selftest() {
1921
1924
  if (trippedStop) throw new Error('selftest failed: compliance loop guard did not terminally trip');
1922
1925
  const loopBlocker = await readJson(path.join(loopMission.dir, 'hard-blocker.json'), null);
1923
1926
  if (loopBlocker?.reason !== 'compliance_loop_guard_tripped') throw new Error('selftest failed: compliance loop guard did not write hard blocker');
1927
+ await setCurrent(tmp, loopState);
1928
+ const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
1929
+ cwd: tmp,
1930
+ input: JSON.stringify({ cwd: tmp, prompt: '$DFix Change the CTA label only' }),
1931
+ timeoutMs: 15000,
1932
+ maxOutputBytes: 64 * 1024
1933
+ });
1934
+ if (dfixPromptHook.code !== 0) throw new Error(`selftest failed: DFix prompt hook exited ${dfixPromptHook.code}: ${dfixPromptHook.stderr}`);
1935
+ const dfixStopHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'stop'], {
1936
+ cwd: tmp,
1937
+ input: JSON.stringify({ cwd: tmp, last_assistant_message: 'DFix 완료 요약: CTA 라벨만 변경했습니다. 검증: 대상 파일 확인 통과. 남은 문제: 없음.' }),
1938
+ timeoutMs: 15000,
1939
+ maxOutputBytes: 64 * 1024
1940
+ });
1941
+ if (dfixStopHook.code !== 0) throw new Error(`selftest failed: DFix stop hook exited ${dfixStopHook.code}: ${dfixStopHook.stderr}`);
1942
+ const dfixStop = JSON.parse(dfixStopHook.stdout || '{}');
1943
+ if (dfixStop.decision === 'block' || dfixStop.continue === false) throw new Error(`selftest failed: DFix stop hook was blocked: ${dfixStopHook.stdout}`);
1944
+ if (!String(dfixStop.systemMessage || '').includes('DFix ultralight finalization accepted')) throw new Error('selftest failed: DFix stop hook did not use the ultralight finalization bypass');
1924
1945
  await writeJsonAtomic(path.join(loopMission.dir, 'team-roster.json'), { schema_version: 1, mission_id: loopMission.id, confirmed: true });
1925
1946
  await writeJsonAtomic(path.join(loopMission.dir, 'team-session-cleanup.json'), { schema_version: 1, passed: true, all_sessions_closed: true, outstanding_sessions: 0, live_transcript_finalized: true });
1926
1947
  await writeJsonAtomic(path.join(loopMission.dir, 'team-gate.json'), { passed: true, team_roster_confirmed: true, analysis_artifact: true, triwiki_refreshed: true, triwiki_validated: true, consensus_artifact: true, implementation_team_fresh: true, review_artifact: true, integration_evidence: true, session_cleanup: true });
@@ -1967,9 +1988,11 @@ async function selftest() {
1967
1988
  await writeJsonAtomic(path.join(postinstallBootstrapTmp, 'package.json'), { name: 'postinstall-bootstrap-smoke', version: '0.0.0' });
1968
1989
  const postinstallBootstrap = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallBootstrapTmp, input: 'y\n', env: { INIT_CWD: postinstallBootstrapTmp, HOME: path.join(postinstallBootstrapTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1', SKS_SKIP_CLI_TOOLS: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
1969
1990
  if (postinstallBootstrap.code !== 0 || !String(postinstallBootstrap.stdout || '').includes('SKS Ready')) throw new Error(`selftest failed: approved postinstall bootstrap did not run: ${postinstallBootstrap.stderr}`);
1970
- for (const rel of ['.agents/skills/team/SKILL.md', '.codex/config.toml', '.codex/hooks.json', '.sneakoscope/harness-guard.json', '.codex/SNEAKOSCOPE.md', 'AGENTS.md']) {
1991
+ for (const rel of ['.agents/skills/team/SKILL.md', '.codex/config.toml', '.codex/hooks.json', '.sneakoscope/harness-guard.json', '.codex/SNEAKOSCOPE.md', 'AGENTS.md', '.gitignore']) {
1971
1992
  if (!(await exists(path.join(postinstallBootstrapTmp, rel)))) throw new Error(`selftest failed: bootstrap did not create ${rel}`);
1972
1993
  }
1994
+ const postinstallBootstrapGitignore = await safeReadText(path.join(postinstallBootstrapTmp, '.gitignore'));
1995
+ if (!postinstallBootstrapGitignore.includes('.sneakoscope/') || !postinstallBootstrapGitignore.includes('.codex/') || !postinstallBootstrapGitignore.includes('.agents/') || !postinstallBootstrapGitignore.includes('AGENTS.md')) throw new Error('selftest failed: bootstrap did not ignore SKS generated files');
1973
1996
  const bootstrapJsonTmp = tmpdir();
1974
1997
  await writeJsonAtomic(path.join(bootstrapJsonTmp, 'package.json'), { name: 'bootstrap-json-smoke', version: '0.0.0' });
1975
1998
  const bootstrapJson = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'bootstrap', '--json'], { cwd: bootstrapJsonTmp, env: { HOME: path.join(bootstrapJsonTmp, 'home'), SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
@@ -2090,10 +2113,19 @@ async function selftest() {
2090
2113
  await initProject(localOnlyTmp, { localOnly: true });
2091
2114
  const localExclude = await safeReadText(path.join(localOnlyTmp, '.git', 'info', 'exclude'));
2092
2115
  if (!localExclude.includes('.codex/') || !localExclude.includes('AGENTS.md')) throw new Error('selftest failed: local-only git excludes missing');
2116
+ if (await exists(path.join(localOnlyTmp, '.gitignore'))) throw new Error('selftest failed: local-only wrote shared .gitignore');
2093
2117
  const localAgents = await safeReadText(path.join(localOnlyTmp, 'AGENTS.md'));
2094
2118
  if (localAgents.trim() !== 'existing local rules') throw new Error('selftest failed: local-only modified existing AGENTS.md');
2095
2119
  const localManifest = await readJson(path.join(localOnlyTmp, '.sneakoscope', 'manifest.json'));
2096
2120
  if (!localManifest.git?.local_only) throw new Error('selftest failed: local-only manifest missing');
2121
+ const gitignoreTmp = tmpdir();
2122
+ await writeTextAtomic(path.join(gitignoreTmp, '.gitignore'), 'node_modules/\n.sneakoscope/\n');
2123
+ await initProject(gitignoreTmp, {});
2124
+ const gitignoreText = await safeReadText(path.join(gitignoreTmp, '.gitignore'));
2125
+ if (!gitignoreText.includes('node_modules/') || !gitignoreText.includes('# BEGIN Sneakoscope Codex generated files') || !gitignoreText.includes('.codex/') || !gitignoreText.includes('.agents/') || !gitignoreText.includes('AGENTS.md')) throw new Error('selftest failed: shared .gitignore did not preserve existing entries and add SKS patterns');
2126
+ await initProject(gitignoreTmp, {});
2127
+ const gitignoreTextSecond = await safeReadText(path.join(gitignoreTmp, '.gitignore'));
2128
+ if ((gitignoreTextSecond.match(/BEGIN Sneakoscope Codex generated files/g) || []).length !== 1) throw new Error('selftest failed: shared .gitignore managed block duplicated');
2097
2129
  const managedAgentsTmp = tmpdir();
2098
2130
  await ensureDir(path.join(managedAgentsTmp, '.git'));
2099
2131
  await writeTextAtomic(path.join(managedAgentsTmp, 'AGENTS.md'), '<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->\nold managed rules\n<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n');
@@ -89,6 +89,8 @@ Local-only install artifacts:
89
89
  # writes generated SKS files but excludes .sneakoscope/, .codex/, .agents/, AGENTS.md through .git/info/exclude
90
90
  # user-owned AGENTS.md is preserved; an existing SKS managed block is refreshed
91
91
 
92
+ Default project setup writes the same SKS generated-file patterns into the project .gitignore.
93
+
92
94
  GitHub install for unreleased commits:
93
95
  npm i -g git+${REPOSITORY_URL}
94
96
  sks bootstrap
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.6.87';
8
+ export const PACKAGE_VERSION = '0.6.90';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -12,9 +12,11 @@ const TEAM_DIGEST_MESSAGE_CHARS = 180;
12
12
  const TEAM_DIGEST_CONTEXT_CHARS = 1600;
13
13
  const TEAM_DIGEST_SYSTEM_CHARS = 260;
14
14
  const STOP_REPEAT_GUARD_ARTIFACT = 'stop-hook-repeat-guard.json';
15
+ const LIGHT_ROUTE_STOP_ARTIFACT = 'light-route-stop.json';
15
16
  const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
16
17
  const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
17
18
  const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
19
+ const LIGHT_ROUTE_STOP_WINDOW_MS = 10 * 60 * 1000;
18
20
 
19
21
  async function loadHookPayload() {
20
22
  const raw = await readStdin();
@@ -111,6 +113,7 @@ async function hookUserPrompt(root, state, payload, noQuestion) {
111
113
  const command = dollarCommand(prompt);
112
114
  const route = routePrompt(prompt);
113
115
  const bypassActiveRoute = route?.id === 'DFix' || route?.id === 'Answer';
116
+ if (route?.id === 'DFix') await recordLightRouteStop(root, route, payload, prompt);
114
117
  if (isClarificationAwaiting(state) && !looksLikeClarificationCancel(prompt)) {
115
118
  const activeContext = await activeRouteContext(root, state);
116
119
  const teamDigest = await teamLiveDigest(root, state);
@@ -222,6 +225,12 @@ async function hookPermission(root, state, payload, noQuestion) {
222
225
  }
223
226
 
224
227
  async function hookStop(root, state, payload, noQuestion) {
228
+ if (!noQuestion && await consumeLightRouteStop(root, payload)) {
229
+ return {
230
+ continue: true,
231
+ systemMessage: 'SKS: DFix ultralight finalization accepted; full-route Honest Mode loopback is not required.'
232
+ };
233
+ }
225
234
  const routeDecision = await evaluateStop(root, state, payload, { noQuestion });
226
235
  if (routeDecision) return routeDecision;
227
236
  if (!noQuestion) {
@@ -258,6 +267,46 @@ async function hookStop(root, state, payload, noQuestion) {
258
267
  };
259
268
  }
260
269
 
270
+ async function recordLightRouteStop(root, route = {}, payload = {}, prompt = '') {
271
+ const now = nowIso();
272
+ const expires = new Date(Date.parse(now) + LIGHT_ROUTE_STOP_WINDOW_MS).toISOString();
273
+ const file = path.join(root, '.sneakoscope', 'state', LIGHT_ROUTE_STOP_ARTIFACT);
274
+ await writeJsonAtomic(file, {
275
+ schema_version: 1,
276
+ route: route.id || null,
277
+ route_command: route.command || null,
278
+ mode: route.mode || null,
279
+ conversation_id: conversationId(payload),
280
+ prompt_hash: sha256(String(prompt || '')).slice(0, 16),
281
+ created_at: now,
282
+ expires_at: expires,
283
+ pending_stop_bypass: true,
284
+ stop_policy: 'dfix_ultralight_bypasses_full_route_honest_mode'
285
+ }).catch(() => null);
286
+ }
287
+
288
+ async function consumeLightRouteStop(root, payload = {}) {
289
+ const file = path.join(root, '.sneakoscope', 'state', LIGHT_ROUTE_STOP_ARTIFACT);
290
+ const record = await readJson(file, null).catch(() => null);
291
+ if (!record?.pending_stop_bypass) return false;
292
+ if (record.route !== 'DFix') return false;
293
+ const nowMs = Date.now();
294
+ const expiresMs = Date.parse(record.expires_at || '');
295
+ if (!Number.isFinite(expiresMs) || expiresMs < nowMs) return false;
296
+ const currentConversation = conversationId(payload);
297
+ if (record.conversation_id && explicitConversationId(payload) && record.conversation_id !== currentConversation) return false;
298
+ await writeJsonAtomic(file, {
299
+ ...record,
300
+ pending_stop_bypass: false,
301
+ consumed_at: nowIso()
302
+ }).catch(() => null);
303
+ return true;
304
+ }
305
+
306
+ function explicitConversationId(payload = {}) {
307
+ return payload.conversation_id || payload.thread_id || payload.session_id || payload.chat_id || null;
308
+ }
309
+
261
310
  async function finalizationRepeatDecision(root, state = {}, payload = {}, reason = '', kind = 'finalization') {
262
311
  const now = nowIso();
263
312
  const guardPath = path.join(root, '.sneakoscope', 'state', STOP_REPEAT_GUARD_ARTIFACT);
package/src/core/init.mjs CHANGED
@@ -9,6 +9,8 @@ import { installVersionGitHook } from './version-manager.mjs';
9
9
  import { CODEX_COMPUTER_USE_ONLY_POLICY, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, chatCaptureIntakeText, context7ConfigToml, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
10
10
 
11
11
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
12
+ const SKS_GENERATED_GIT_PATTERNS = ['.sneakoscope/', '.codex/', '.agents/', 'AGENTS.md'];
13
+
12
14
  function reflectionInstructionText(commandPrefix = 'sks') {
13
15
  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.`;
14
16
  }
@@ -107,7 +109,9 @@ export async function initProject(root, opts = {}) {
107
109
  ];
108
110
  for (const d of dirs) await ensureDir(path.join(root, d));
109
111
  const localExclude = localOnly ? await ensureLocalOnlyGitExclude(root) : null;
112
+ const sharedIgnore = localOnly ? null : await ensureSharedGitIgnore(root);
110
113
  if (localExclude?.path) created.push(`${path.relative(root, localExclude.path)} local-only excludes`);
114
+ if (sharedIgnore?.changed) created.push(`${path.relative(root, sharedIgnore.path)} SKS generated files ignore`);
111
115
 
112
116
  await writeJsonAtomic(path.join(sine, 'manifest.json'), {
113
117
  package: 'sneakoscope',
@@ -165,6 +169,8 @@ export async function initProject(root, opts = {}) {
165
169
  },
166
170
  git: {
167
171
  local_only: localOnly,
172
+ ignore_path: sharedIgnore?.path ? path.relative(root, sharedIgnore.path) : null,
173
+ ignored_patterns: sharedIgnore?.patterns || [],
168
174
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : null,
169
175
  excluded_patterns: localExclude?.patterns || [],
170
176
  versioning: {
@@ -198,6 +204,8 @@ export async function initProject(root, opts = {}) {
198
204
  git: {
199
205
  ...(policy.git || {}),
200
206
  local_only: localOnly || Boolean(policy.git?.local_only),
207
+ ignore_path: sharedIgnore?.path ? path.relative(root, sharedIgnore.path) : policy.git?.ignore_path || null,
208
+ ignored_patterns: sharedIgnore?.patterns || policy.git?.ignored_patterns || [],
201
209
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : policy.git?.exclude_path || null,
202
210
  excluded_patterns: localExclude?.patterns || policy.git?.excluded_patterns || [],
203
211
  versioning: {
@@ -267,6 +275,8 @@ export async function initProject(root, opts = {}) {
267
275
  installation: installPolicy(scope, commandPrefix),
268
276
  git: {
269
277
  local_only: localOnly,
278
+ ignore_path: sharedIgnore?.path ? path.relative(root, sharedIgnore.path) : null,
279
+ ignored_patterns: sharedIgnore?.patterns || [],
270
280
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : null,
271
281
  excluded_patterns: localExclude?.patterns || [],
272
282
  versioning: {
@@ -445,10 +455,33 @@ policy = "Deny destructive database operations, credential exfiltration, persist
445
455
  return { created };
446
456
  }
447
457
 
458
+ async function ensureSharedGitIgnore(root) {
459
+ const patterns = SKS_GENERATED_GIT_PATTERNS;
460
+ const ignorePath = path.join(root, '.gitignore');
461
+ const markerStart = '# BEGIN Sneakoscope Codex generated files';
462
+ const markerEnd = '# END Sneakoscope Codex generated files';
463
+ const managedBlock = `${markerStart}\n${patterns.join('\n')}\n${markerEnd}\n`;
464
+ const current = await readText(ignorePath, '');
465
+ if (current.includes(markerStart)) {
466
+ const re = new RegExp(`${escapeRegExp(markerStart)}[\\s\\S]*?${escapeRegExp(markerEnd)}\\n?`);
467
+ const next = current.replace(re, managedBlock);
468
+ if (next !== current) await writeTextAtomic(ignorePath, next.endsWith('\n') ? next : `${next}\n`);
469
+ return { path: ignorePath, patterns, changed: next !== current };
470
+ }
471
+ const existing = new Set(current.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
472
+ const missing = patterns.filter((pattern) => !existing.has(pattern));
473
+ if (!missing.length) return { path: ignorePath, patterns, changed: false };
474
+ const block = missing.length === patterns.length
475
+ ? managedBlock
476
+ : `${markerStart}\n${missing.join('\n')}\n${markerEnd}\n`;
477
+ await writeTextAtomic(ignorePath, `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}`);
478
+ return { path: ignorePath, patterns, changed: true };
479
+ }
480
+
448
481
  async function ensureLocalOnlyGitExclude(root) {
449
482
  const gitDir = await resolveGitDir(root);
450
483
  if (!gitDir) return { path: null, patterns: [] };
451
- const patterns = ['.sneakoscope/', '.codex/', '.agents/', 'AGENTS.md'];
484
+ const patterns = SKS_GENERATED_GIT_PATTERNS;
452
485
  const excludePath = path.join(gitDir, 'info', 'exclude');
453
486
  await ensureDir(path.dirname(excludePath));
454
487
  const markerStart = '# Sneakoscope Codex local-only generated files';
@@ -494,7 +527,7 @@ function codexAppQuickReference(scope, commandPrefix) {
494
527
 
495
528
  export async function installSkills(root) {
496
529
  const skills = {
497
- 'dfix': `---\nname: dfix\ndescription: Ultralight fast design/content fix mode for $DFix or $dfix requests and inferred simple edits such as text color, copy, labels, spacing, or translation.\n---\n\nUse for tiny copy/color/label/spacing/translation edits. List exact micro-edits, inspect only needed files, apply only those edits, and run cheap verification. Bypass broad SKS routing, Goal, Research, eval, and redesign. Read \`design.md\` for UI work when present; use imagegen for image/logo/raster assets.\n`,
530
+ 'dfix': `---\nname: dfix\ndescription: Ultralight fast design/content fix mode for $DFix or $dfix requests and inferred simple edits such as text color, copy, labels, spacing, or translation.\n---\n\nUse for tiny copy/color/label/spacing/translation edits. List exact micro-edits, inspect only needed files, apply only those edits, and run cheap verification. Bypass broad SKS routing, Goal, Research, eval, redesign, and repeated full-route Honest Mode loops. Read \`design.md\` for UI work when present; use imagegen for image/logo/raster assets.\n`,
498
531
  '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`,
499
532
  '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. 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.\n`,
500
533
  '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`,
@@ -81,7 +81,7 @@ export function dfixQuickContext(prompt, route = routePrompt(prompt)) {
81
81
  '2. Inspect only the files needed to locate that target.',
82
82
  '3. Apply only the listed design/content edit; for UI/UX micro-edits read design.md when present, and use imagegen for any image/logo/raster asset.',
83
83
  '4. Run only cheap verification when useful, such as syntax check, focused test, or local render smoke.',
84
- '5. Final response: one short completion summary explaining what changed, plus verification or the exact blocker.'
84
+ '5. Final response: one short DFix completion summary explaining what changed, plus cheap verification or the exact blocker. Do not enter repeated full-route Honest Mode loops.'
85
85
  ].join('\n');
86
86
  }
87
87