sneakoscope 0.6.25 → 0.6.26

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
@@ -30,7 +30,7 @@ Sneakoscope Codex is for developers who want Codex CLI to keep working until a g
30
30
  - **Database-safe autonomous coding**: destructive SQL, unsafe Supabase MCP writes, production DB mutation, and risky migration flows are blocked or surfaced early.
31
31
  - **Harness self-protection**: after setup, installed SKS control files are locked against LLM tool edits, with a source-repo-only exception for Sneakoscope engine development.
32
32
  - **Other-harness conflict gate**: OMX/DCodex-style Codex harness traces block npm install and setup until a human-approved cleanup is performed.
33
- - **Automatic project versioning**: every commit can carry a unique patch version bump, with package-lock sync and a Git common-dir lock for parallel workers.
33
+ - **Automatic project versioning**: commits can carry a unique patch bump with lockfile sync.
34
34
  - **Honest completion gates**: H-Proof and Honest Mode require evidence before the agent claims the work is complete.
35
35
  - **TriWiki context-tracking SSOT**: structured wiki packs, visual coordinate anchors, and bounded memory help long-running work survive context pressure without relying on lossy summaries.
36
36
 
@@ -43,7 +43,7 @@ npm i -g sneakoscope
43
43
  sks
44
44
  ```
45
45
 
46
- `npm i -g sneakoscope` prints setup guidance without making npm output look like a crash. If OMX, DCodex, or their global/repo-level traces are detected during postinstall, npm installation can still finish, but SKS clearly reports that `sks setup` and `sks doctor --fix` are blocked until a human-approved cleanup happens. In an interactive terminal, postinstall asks whether to show the GPT-5.5 high cleanup prompt now; in CI or agent installs, it prints `sks conflicts prompt` for later. If no conflicting harness exists, SKS checks whether the `sks` command is available, best-effort creates a command shim in a writable PATH directory when needed, and best-effort installs the Context7 MCP globally when Codex CLI is available. Run `sks` in a real terminal to open the setup UI. The UI asks whether this project should use the global install or a project-only install, then offers to run setup, doctor, and selftest.
46
+ `npm i -g sneakoscope` prints setup guidance without making npm output look like a crash. If OMX, DCodex, or their global/repo-level traces are detected, npm can finish but SKS reports that `sks setup` and `sks doctor --fix` are blocked until human-approved cleanup. Otherwise postinstall best-effort creates an `sks` shim, configures Context7 when Codex CLI is available, and initializes the current project when `INIT_CWD` looks like one. Project setup writes hooks, skills, agents, `$team`, and the `$agent-team` fallback picker alias. Run `sks` for the setup UI.
47
47
 
48
48
  Default non-interactive setup:
49
49
 
@@ -159,7 +159,7 @@ AGENTS.md repository rules loaded by Codex agents
159
159
  .sneakoscope/ mission state, gates, logs, policy, GX cartridges, and reports
160
160
  ```
161
161
 
162
- Codex App discovers repo-local skills from `.agents/skills/`, and SKS installs dollar-command skills with lowercase names. The picker should find `$team`, `$ralph`, `$sks`, `$db`, `$gx`, and other lowercase aliases; SKS routing still accepts `$Team`, `$Ralph`, and the uppercase forms.
162
+ Codex App discovers repo-local skills from `.agents/skills/`. The picker should find `$team`, `$ralph`, `$sks`, `$db`, `$gx`, and other lowercase aliases; SKS still accepts `$Team`, `$Ralph`, and uppercase forms. SKS also installs `$agent-team` as a Team fallback alias when the app hides the plain `team` skill name.
163
163
 
164
164
  SKS uses the official Codex hook behavior: `UserPromptSubmit` can inject additional developer context or block a prompt, `Stop` with `decision: "block"` continues the turn by creating a new continuation prompt, and hook `statusMessage` text makes active SKS routing, guard, permission, and done-gate checks visible in Codex App.
165
165
 
@@ -167,9 +167,9 @@ After setup, SKS writes `.sneakoscope/harness-guard.json`. Hooks block LLM tool
167
167
 
168
168
  ## Project Versioning
169
169
 
170
- SKS setup installs a managed Git `pre-commit` hook for projects with `package.json`. On every commit, SKS bumps the project patch version before Git writes the commit, syncs `package-lock.json` and `npm-shrinkwrap.json` when present, and stages those version files into the same commit.
170
+ SKS setup installs a managed Git `pre-commit` hook for projects with `package.json`. It bumps the patch version, syncs lockfiles, and stages those files into the same commit.
171
171
 
172
- Multiple workers and worktrees share a version lock in the Git common directory. If another worker already used a version, the next commit automatically bumps above the last seen version instead of reusing it.
172
+ Workers and worktrees share a Git common-dir lock so versions are not reused.
173
173
 
174
174
  ```bash
175
175
  sks versioning status
@@ -241,7 +241,7 @@ If your shell cannot find the global command yet, run through npm without relyin
241
241
  npx -y -p sneakoscope sks setup
242
242
  ```
243
243
 
244
- The global postinstall also tries to create a local `sks` shim automatically. If the only writable fallback is `~/.local/bin` or `~/bin`, add that directory to your shell PATH once.
244
+ The global postinstall also tries to create a local `sks` shim automatically. If the install runs from a project directory, it performs the same Codex App setup as `sks setup` unless `SKS_SKIP_POSTINSTALL_SETUP=1` or CI is active.
245
245
 
246
246
  Create a Ralph mission:
247
247
 
@@ -284,7 +284,7 @@ sks research run latest --max-cycles 3
284
284
  - **Forced subagent execution policy**: code-changing work first surfaces SKS status context, then defaults to parallel worker subagents when independent write scopes exist; the parent orchestrator owns integration and verification.
285
285
  - **AutoResearch loop**: open-ended improvement tasks use a small experiment cycle: program, hypothesis, experiment, metric, keep/discard, falsification, and honest conclusion.
286
286
  - **Update-aware hooks**: before work, SKS checks for a newer published package and asks whether to update now or skip for the current conversation only.
287
- - **Automatic project versioning**: setup installs a pre-commit guard that bumps `package.json` patch versions, syncs lockfiles, stages them, and avoids duplicate versions across parallel workers.
287
+ - **Automatic project versioning**: setup installs a pre-commit patch bump and lockfile sync guard.
288
288
  - **Honest Mode finish**: final answers must include an evidence-aware verification pass before claiming the goal is complete.
289
289
  - **Fast DF mode**: `$DF` handles small design/content edits like color, copy, labels, spacing, and translation without unnecessary Ralph, Research, or evaluation loops.
290
290
  - **Database guard**: destructive DB operations, production writes, unsafe Supabase MCP configuration, and direct live SQL mutations are blocked or warned on.
@@ -504,7 +504,7 @@ Context tracking uses TriWiki as the SSOT. When a route spans turns, subagent ha
504
504
 
505
505
  Installed projects treat the SKS harness as immutable to LLM tool edits. The `PreToolUse` and `PermissionRequest` hooks block direct writes to generated control files, generated skills/agents, policy files, `AGENTS.md`, and the installed `node_modules/sneakoscope` package. They also block LLM-issued maintenance commands such as `sks setup`, `sks init`, `sks doctor --fix`, `sks context7 setup`, and package-manager removal of `sneakoscope`.
506
506
 
507
- `sks doctor --fix` repairs broken SKS-generated settings by deleting and regenerating the current installed package templates for Codex hooks, config, app skills, local agents, manifest, policy, DB guard, and harness guard. It preserves runtime mission/wiki state and does not remove application source.
507
+ `sks doctor --fix` repairs broken SKS-generated hooks, config, app skills, local agents, manifest, policy, DB guard, and harness guard. It also restores picker fallback aliases such as `$agent-team` when `$team` can be hidden by the app. Runtime mission/wiki state and application source are preserved.
508
508
 
509
509
  The guard writes fingerprints to `.sneakoscope/harness-guard.json`, and `sks doctor` includes the guard in readiness. Check it directly with:
510
510
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "Sneakoscope Codex",
4
- "version": "0.6.25",
4
+ "version": "0.6.26",
5
5
  "description": "Sneakoscope Codex: update-aware, database-safe Codex CLI harness with multi-agent Team orchestration, Ralph no-question execution, autoresearch-style loops, and H-Proof gates.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -180,6 +180,11 @@ async function postinstall() {
180
180
  else if (context7Install.status === 'codex_missing') console.log('Context7 MCP: Codex CLI missing. Install @openai/codex or set SKS_CODEX_BIN, then run `sks context7 setup --scope global` or `sks setup` in a project.');
181
181
  else if (context7Install.status === 'skipped') console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
182
182
  else if (context7Install.status === 'failed') console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
183
+ const appSetup = await ensureCodexAppProjectDuringInstall(installRoot, { shim });
184
+ if (appSetup.status === 'installed') console.log(`Codex App project setup: installed in ${appSetup.root} (${appSetup.install_scope}; picker aliases include ${appSetup.aliases.join(', ')}).`);
185
+ else if (appSetup.status === 'partial') console.log(`Codex App project setup: repaired with missing skill warning (${appSetup.missing_skills.join(', ')}). Run \`sks doctor --fix\`.`);
186
+ else if (appSetup.status === 'skipped') console.log(`Codex App project setup: skipped (${appSetup.reason}).`);
187
+ else if (appSetup.status === 'failed') console.log(`Codex App project setup: auto setup failed. Run \`sks doctor --fix\`. ${appSetup.error || ''}`.trim());
183
188
  console.log('Run `sks` to open the interactive setup UI, or run `sks setup` for the default global setup.');
184
189
  console.log('Project-only setup: `sks wizard` -> choose project, or `npx sks setup --install-scope project`.\n');
185
190
  }
@@ -288,6 +293,47 @@ async function ensureGlobalContext7DuringInstall() {
288
293
  return { status: 'failed', error: `${add.stderr || add.stdout || 'codex mcp add failed'}`.trim() };
289
294
  }
290
295
 
296
+ async function ensureCodexAppProjectDuringInstall(installRoot, opts = {}) {
297
+ if (process.env.SKS_SKIP_POSTINSTALL_SETUP === '1') return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SETUP=1' };
298
+ if (process.env.CI === 'true') return { status: 'skipped', reason: 'CI=true' };
299
+ const root = path.resolve(installRoot || process.cwd());
300
+ if (!(await isProjectSetupCandidate(root))) return { status: 'skipped', reason: 'no package.json, .git, .codex, .agents, or AGENTS.md in INIT_CWD' };
301
+ try {
302
+ const installScope = await isProjectPackageInstall(root) ? 'project' : 'global';
303
+ const globalCommand = opts.shim?.command && opts.shim.status !== 'created_not_on_path'
304
+ ? opts.shim.command
305
+ : await globalSksCommand();
306
+ await initProject(root, { installScope, globalCommand, localOnly: false });
307
+ const skills = await checkRequiredSkills(root);
308
+ return {
309
+ status: skills.ok ? 'installed' : 'partial',
310
+ root,
311
+ install_scope: installScope,
312
+ aliases: DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill),
313
+ missing_skills: skills.missing
314
+ };
315
+ } catch (err) {
316
+ return { status: 'failed', root, error: err.message };
317
+ }
318
+ }
319
+
320
+ async function isProjectSetupCandidate(root) {
321
+ for (const marker of ['package.json', '.git', '.codex', '.agents', 'AGENTS.md']) {
322
+ if (await exists(path.join(root, marker))) return true;
323
+ }
324
+ return false;
325
+ }
326
+
327
+ async function isProjectPackageInstall(root) {
328
+ const installedPackage = path.join(root, 'node_modules', 'sneakoscope');
329
+ if (!(await exists(path.join(installedPackage, 'package.json')))) return false;
330
+ const [installedReal, packageReal] = await Promise.all([
331
+ fsp.realpath(installedPackage).catch(() => installedPackage),
332
+ fsp.realpath(packageRoot()).catch(() => packageRoot())
333
+ ]);
334
+ return installedReal === packageReal;
335
+ }
336
+
291
337
  async function wizard(args = []) {
292
338
  if (!shouldShowWizard() && !flag(args, '--force')) return help();
293
339
  const rl = readline.createInterface({ input, output });
@@ -1716,10 +1762,13 @@ async function selftest() {
1716
1762
  const repairTmp = tmpdir();
1717
1763
  await initProject(repairTmp, {});
1718
1764
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'), 'tampered\n');
1765
+ await fsp.rm(path.join(repairTmp, '.agents', 'skills', 'agent-team'), { recursive: true, force: true });
1719
1766
  await writeTextAtomic(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'), 'legacy mirror\n');
1720
1767
  await initProject(repairTmp, { force: true, repair: true });
1721
1768
  const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
1722
1769
  if (!repairedTeamSkill.includes('SKS Team multi-agent orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest failed: doctor repair did not regenerate team skill');
1770
+ const repairedTeamAliasSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'));
1771
+ if (!repairedTeamAliasSkill.includes('Fallback Codex App picker alias')) throw new Error('selftest failed: doctor repair did not regenerate agent-team fallback skill');
1723
1772
  if (await exists(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove legacy .codex/skills');
1724
1773
  const conflictTmp = tmpdir();
1725
1774
  await ensureDir(path.join(conflictTmp, '.omx'));
@@ -1731,6 +1780,12 @@ async function selftest() {
1731
1780
  if (!postinstallConflictOutput.includes('SKS setup is blocked') || postinstallConflictOutput.includes('Cleanup prompt:')) throw new Error('selftest failed: postinstall conflict notice did not stay informational');
1732
1781
  const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
1733
1782
  if (postinstallConflictPrompt.code !== 0 || !String(postinstallConflictPrompt.stdout || '').includes('Goal: completely remove the conflicting Codex harnesses')) throw new Error('selftest failed: interactive postinstall prompt did not print cleanup prompt');
1783
+ const postinstallSetupTmp = tmpdir();
1784
+ await writeJsonAtomic(path.join(postinstallSetupTmp, 'package.json'), { name: 'postinstall-setup-smoke', version: '0.0.0' });
1785
+ const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
1786
+ if (postinstallSetup.code !== 0) throw new Error(`selftest failed: postinstall setup exited ${postinstallSetup.code}: ${postinstallSetup.stderr}`);
1787
+ if (!(await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md')))) throw new Error('selftest failed: postinstall did not install agent-team fallback skill');
1788
+ if (!String(postinstallSetup.stdout || '').includes('Codex App project setup: installed')) throw new Error('selftest failed: postinstall did not report automatic Codex App setup');
1734
1789
  const guardBlocked = await checkHarnessModification(tmp, { tool_name: 'apply_patch', command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' });
1735
1790
  if (guardBlocked.action !== 'block') throw new Error('selftest failed: harness guard allowed skill tampering');
1736
1791
  const setupBlocked = await checkHarnessModification(tmp, { command: 'sks setup --force' });
@@ -1767,6 +1822,16 @@ async function selftest() {
1767
1822
  await initProject(projectScopeTmp, { installScope: 'project' });
1768
1823
  const projectHooks = await readJson(path.join(projectScopeTmp, '.codex', 'hooks.json'));
1769
1824
  if (projectHooks.hooks.PreToolUse[0].hooks[0].command !== 'node ./node_modules/sneakoscope/bin/sks.mjs hook pre-tool') throw new Error('selftest failed: project install hook command missing');
1825
+ const sourceHookTmp = tmpdir();
1826
+ await writeJsonAtomic(path.join(sourceHookTmp, 'package.json'), { name: 'sneakoscope', version: '0.0.0' });
1827
+ await ensureDir(path.join(sourceHookTmp, 'bin'));
1828
+ await ensureDir(path.join(sourceHookTmp, 'src', 'core'));
1829
+ await writeTextAtomic(path.join(sourceHookTmp, 'bin', 'sks.mjs'), '#!/usr/bin/env node\n');
1830
+ await writeTextAtomic(path.join(sourceHookTmp, 'src', 'core', 'init.mjs'), '');
1831
+ await writeTextAtomic(path.join(sourceHookTmp, 'src', 'core', 'hooks-runtime.mjs'), '');
1832
+ await initProject(sourceHookTmp, { installScope: 'global', globalCommand: '/usr/local/bin/sks' });
1833
+ const sourceHooks = await readJson(path.join(sourceHookTmp, '.codex', 'hooks.json'));
1834
+ if (sourceHooks.hooks.PreToolUse[0].hooks[0].command !== 'node ./bin/sks.mjs hook pre-tool') throw new Error('selftest failed: source repo hook command should use local bin');
1770
1835
  const versionTmp = tmpdir();
1771
1836
  await runProcess('git', ['init'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1772
1837
  await runProcess('git', ['config', 'user.email', 'sks-selftest@example.invalid'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
@@ -1857,7 +1922,13 @@ async function selftest() {
1857
1922
  const hookGuardResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'pre-tool'], { cwd: tmp, input: hookGuardPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1858
1923
  const hookGuardJson = JSON.parse(hookGuardResult.stdout);
1859
1924
  if (hookGuardJson.decision !== 'block' || !String(hookGuardJson.reason || '').includes('harness guard')) throw new Error('selftest failed: hook did not block harness tampering');
1925
+ const camelHookGuardPayload = JSON.stringify({ cwd: tmp, toolName: 'apply_patch', toolInput: { command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' } });
1926
+ const camelHookGuardResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'pre-tool'], { cwd: tmp, input: camelHookGuardPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1927
+ const camelHookGuardJson = JSON.parse(camelHookGuardResult.stdout);
1928
+ if (camelHookGuardJson.decision !== 'block') throw new Error('selftest failed: hook did not block camelCase Codex tool payload');
1860
1929
  if (new Set(DOLLAR_COMMANDS.map((c) => c.command)).size !== DOLLAR_COMMANDS.length) throw new Error('selftest failed: duplicate dollar commands');
1930
+ if (!DOLLAR_COMMAND_ALIASES.some((alias) => alias.canonical === '$Team' && alias.app_skill === '$agent-team')) throw new Error('selftest failed: $Team fallback picker alias missing');
1931
+ if (routePrompt('$agent-team run specialists')?.id !== 'Team') throw new Error('selftest failed: $agent-team did not route to Team');
1861
1932
  if (!COMMAND_CATALOG.some((c) => c.name === 'context7') || !COMMAND_CATALOG.some((c) => c.name === 'pipeline')) throw new Error('selftest failed: context7/pipeline commands missing from catalog');
1862
1933
  const registryDollarCommands = DOLLAR_COMMANDS.map((c) => c.command);
1863
1934
  const manifest = await readJson(path.join(tmp, '.sneakoscope', 'manifest.json'));
@@ -1883,10 +1954,11 @@ async function selftest() {
1883
1954
  const hookResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookRalphTmp, input: hookPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
1884
1955
  if (hookResult.code !== 0) throw new Error(`selftest failed: $Ralph hook exited ${hookResult.code}: ${hookResult.stderr}`);
1885
1956
  const hookJson = JSON.parse(hookResult.stdout);
1886
- if (!hookJson.additionalContext?.includes('MANDATORY $Ralph route activated')) throw new Error('selftest failed: $Ralph hook did not activate Ralph prepare pipeline');
1957
+ if ('statusMessage' in hookJson || 'additionalContext' in hookJson) throw new Error('selftest failed: hook emitted Codex schema-invalid top-level fields');
1958
+ if (!hookJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY $Ralph route activated')) throw new Error('selftest failed: $Ralph hook did not activate Ralph prepare pipeline');
1887
1959
  if (hookJson.hookSpecificOutput?.hookEventName !== 'UserPromptSubmit' || !hookJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY $Ralph route activated')) throw new Error('selftest failed: $Ralph hook did not emit official UserPromptSubmit additionalContext');
1888
1960
  if (!String(hookJson.systemMessage || '').includes('Ralph clarification gate')) throw new Error('selftest failed: $Ralph hook missing visible status message');
1889
- if (!hookJson.additionalContext?.includes('GOAL_PRECISE')) throw new Error('selftest failed: $Ralph hook did not provide clarification questions');
1961
+ if (!hookJson.hookSpecificOutput?.additionalContext?.includes('GOAL_PRECISE')) throw new Error('selftest failed: $Ralph hook did not provide clarification questions');
1890
1962
  const hookState = await readJson(stateFile(hookRalphTmp), {});
1891
1963
  if (hookState.phase !== 'RALPH_AWAITING_ANSWERS') throw new Error('selftest failed: $Ralph hook did not set awaiting-answers state');
1892
1964
  if (!(await exists(path.join(missionDir(hookRalphTmp, hookState.mission_id), 'questions.md')))) throw new Error('selftest failed: $Ralph hook did not write questions.md');
@@ -33,7 +33,7 @@ export async function ensureDbSafetyPolicy(root) {
33
33
  }
34
34
 
35
35
  export async function loadDbSafetyPolicy(root) {
36
- const p = await ensureDbSafetyPolicy(root);
36
+ const p = path.join(root, '.sneakoscope', 'db-safety.json');
37
37
  const data = await readJson(p, {});
38
38
  return { ...DEFAULT_DB_SAFETY_POLICY, ...(data || {}) };
39
39
  }
@@ -189,7 +189,7 @@ function looksLikeSqlText(text = '') {
189
189
 
190
190
  export function classifyToolPayload(payload = {}) {
191
191
  const strings = recursivelyCollectStrings(payload).slice(0, 200);
192
- const toolName = [payload.tool_name, payload.name, payload.tool?.name, payload.server, payload.mcp_tool, payload.tool, payload.type].filter(Boolean).join(' ').toLowerCase();
192
+ const toolName = [payload.tool_name, payload.toolName, payload.name, payload.tool?.name, payload.server, payload.mcp_tool, payload.tool, payload.type].filter(Boolean).join(' ').toLowerCase();
193
193
  const combined = strings.filter(looksLikeSqlText).join('\n');
194
194
  const sqlClass = classifySql(combined);
195
195
  const commandClass = classifyCommand(strings.find((s) => /\b(supabase|psql|prisma|drizzle|knex|sequelize)\b/i.test(s)) || '');
@@ -130,7 +130,7 @@ export function harnessGuardBlockReason(decision = {}) {
130
130
  export function classifyHarnessPayload(root, payload = {}, policy = {}) {
131
131
  const strings = collectPayloadStrings(payload).slice(0, 300);
132
132
  const hay = strings.join('\n');
133
- const toolName = [payload.tool_name, payload.name, payload.tool?.name, payload.server, payload.mcp_tool, payload.tool, payload.type].filter(Boolean).join(' ').toLowerCase();
133
+ const toolName = [payload.tool_name, payload.toolName, payload.name, payload.tool?.name, payload.server, payload.mcp_tool, payload.tool, payload.type].filter(Boolean).join(' ').toLowerCase();
134
134
  const command = extractCommand(payload);
135
135
  const writeIntent = hasWriteIntent(toolName, command, hay);
136
136
  const maintenance = classifyMaintenanceCommand(command || hay);
@@ -175,7 +175,7 @@ async function listFiles(dir) {
175
175
  }
176
176
 
177
177
  function extractCommand(payload = {}) {
178
- return payload.command || payload.tool_input?.command || payload.input?.command || payload.tool?.input?.command || '';
178
+ return payload.command || payload.tool_input?.command || payload.toolInput?.command || payload.input?.command || payload.tool?.input?.command || '';
179
179
  }
180
180
 
181
181
  function collectPayloadStrings(obj, out = [], depth = 0) {
@@ -40,7 +40,26 @@ function conversationId(payload) {
40
40
  }
41
41
 
42
42
  function extractCommand(payload) {
43
- return payload.command || payload.tool_input?.command || payload.input?.command || payload.tool?.input?.command || '';
43
+ return payload.command || payload.tool_input?.command || payload.toolInput?.command || payload.input?.command || payload.tool?.input?.command || '';
44
+ }
45
+
46
+ function toolFailed(payload = {}) {
47
+ const candidates = [
48
+ payload.exit_code,
49
+ payload.exitCode,
50
+ payload.tool_response?.exit_code,
51
+ payload.toolResponse?.exitCode,
52
+ payload.result?.exit_code,
53
+ payload.result?.exitCode
54
+ ];
55
+ for (const candidate of candidates) {
56
+ if (candidate === undefined || candidate === null || candidate === '') continue;
57
+ const n = Number(candidate);
58
+ if (Number.isFinite(n)) return n !== 0;
59
+ }
60
+ if (payload.success === false || payload.tool_response?.success === false || payload.toolResponse?.success === false || payload.result?.success === false) return true;
61
+ if (payload.executed === false) return true;
62
+ return false;
44
63
  }
45
64
 
46
65
  function dollarCommand(prompt) {
@@ -111,8 +130,7 @@ async function hookPostTool(root, state, payload, noQuestion) {
111
130
  await recordContext7Evidence(root, state, payload).catch(() => null);
112
131
  await recordSubagentEvidence(root, state, payload).catch(() => null);
113
132
  if (!noQuestion) return { continue: true };
114
- const failed = payload.exit_code && payload.exit_code !== 0;
115
- if (failed) {
133
+ if (toolFailed(payload)) {
116
134
  return {
117
135
  additionalContext: 'SKS no-question mode is active. Do not ask the user about this tool failure. Apply the active plan fallback ladder, create a fix task, and continue.'
118
136
  };
@@ -231,22 +249,61 @@ export async function emitHook(name) {
231
249
  }
232
250
 
233
251
  function normalizeHookResult(name, result = {}) {
234
- const out = { ...result };
235
252
  const eventName = codexHookEventName(name);
236
- const specific = { ...(out.hookSpecificOutput || {}) };
237
- if (out.additionalContext && (eventName === 'UserPromptSubmit' || eventName === 'PostToolUse')) {
238
- specific.hookEventName = eventName;
239
- specific.additionalContext = out.additionalContext;
253
+ const out = { ...result };
254
+ const systemMessage = out.systemMessage || visibleHookMessage(name, out.reason || out.additionalContext || '');
255
+ const normalized = { continue: out.continue !== false, systemMessage };
256
+ const reason = out.reason || 'SKS guard denied this action.';
257
+
258
+ if (eventName === 'UserPromptSubmit' || eventName === 'PostToolUse') {
259
+ if (out.decision === 'block') {
260
+ normalized.decision = 'block';
261
+ normalized.reason = reason;
262
+ }
263
+ if (out.additionalContext) {
264
+ normalized.hookSpecificOutput = {
265
+ hookEventName: eventName,
266
+ additionalContext: out.additionalContext
267
+ };
268
+ }
269
+ return normalized;
270
+ }
271
+
272
+ if (eventName === 'PreToolUse') {
273
+ if (out.decision === 'block' || out.permissionDecision === 'deny' || out.decision === 'deny') {
274
+ normalized.decision = 'block';
275
+ normalized.reason = reason;
276
+ }
277
+ return normalized;
278
+ }
279
+
280
+ if (eventName === 'PermissionRequest') {
281
+ if (out.decision === 'deny' || out.permissionDecision === 'deny') {
282
+ normalized.hookSpecificOutput = {
283
+ hookEventName: 'PermissionRequest',
284
+ decision: {
285
+ behavior: 'deny',
286
+ message: reason
287
+ }
288
+ };
289
+ } else if (out.decision === 'allow' || out.permissionDecision === 'allow') {
290
+ normalized.hookSpecificOutput = {
291
+ hookEventName: 'PermissionRequest',
292
+ decision: { behavior: 'allow' }
293
+ };
294
+ }
295
+ return normalized;
240
296
  }
241
- if ((eventName === 'PreToolUse' || eventName === 'PermissionRequest') && (out.decision === 'block' || out.permissionDecision === 'deny' || out.decision === 'deny')) {
242
- specific.hookEventName = eventName;
243
- specific.permissionDecision = out.permissionDecision || 'deny';
244
- specific.reason = out.reason || 'SKS guard denied this action.';
297
+
298
+ if (eventName === 'Stop') {
299
+ if (out.decision === 'block') {
300
+ normalized.decision = 'block';
301
+ normalized.reason = reason;
302
+ }
303
+ return normalized;
245
304
  }
246
- if (Object.keys(specific).length) out.hookSpecificOutput = { hookEventName: eventName, ...specific };
247
- if (!out.systemMessage) out.systemMessage = visibleHookMessage(name, out.reason || out.additionalContext || '');
248
- if (!out.statusMessage) out.statusMessage = out.systemMessage;
249
- return out;
305
+
306
+ return normalized;
250
307
  }
251
308
 
252
309
  function codexHookEventName(name) {
package/src/core/init.mjs CHANGED
@@ -191,7 +191,9 @@ export async function initProject(root, opts = {}) {
191
191
  const created = [];
192
192
  const installScope = normalizeInstallScope(opts.installScope || 'global');
193
193
  const localOnly = Boolean(opts.localOnly);
194
- const hookCommandPrefix = opts.hookCommandPrefix || sksCommandPrefix(installScope, { globalCommand: opts.globalCommand });
194
+ const sourceProject = await isHarnessSourceProject(root).catch(() => false);
195
+ const requestedHookCommandPrefix = opts.hookCommandPrefix || sksCommandPrefix(installScope, { globalCommand: opts.globalCommand });
196
+ const hookCommandPrefix = sourceProject ? 'node ./bin/sks.mjs' : requestedHookCommandPrefix;
195
197
  const sine = path.join(root, '.sneakoscope');
196
198
  if (opts.repair) {
197
199
  const repair = await repairSksGeneratedArtifacts(root, { resetState: Boolean(opts.resetState) });
@@ -506,9 +508,7 @@ export async function initProject(root, opts = {}) {
506
508
  created.push('.codex/agents/*');
507
509
  await writeHarnessGuardPolicy(root);
508
510
  created.push('.sneakoscope/harness-guard.json');
509
- const versionHookCommand = await isHarnessSourceProject(root).catch(() => false)
510
- ? 'node ./bin/sks.mjs'
511
- : hookCommandPrefix;
511
+ const versionHookCommand = sourceProject ? 'node ./bin/sks.mjs' : hookCommandPrefix;
512
512
  const versionHook = await installVersionGitHook(root, versionHookCommand);
513
513
  if (versionHook.installed) created.push('.git/hooks/pre-commit SKS version guard');
514
514
  else created.push(`version guard skipped (${versionHook.reason})`);
@@ -687,6 +687,7 @@ async function installSkills(root) {
687
687
  'df': `---\nname: df\ndescription: Fast design/content fix mode for $DF or $df requests and inferred simple edits such as text color, copy, labels, spacing, or translation.\n---\n\nYou are running SKS DF mode.\n\nPurpose:\n- Quickly convert a small design/content request into the exact implementation change.\n- Use for requests like 글자 색 바꿔줘, 내용을 영어로 바꿔줘, button label 수정, spacing 조정, copy replacement, simple style tweaks.\n\nRules:\n- Do not start Ralph, Research, eval, or broad redesign unless the user explicitly asks.\n- Do not ask for more requirements when the target can be inferred from local context.\n- Inspect only the files needed to locate the target.\n- Make the smallest scoped edit that satisfies the request.\n- Preserve the existing design system and component patterns.\n- Run only cheap verification when useful, such as syntax check, focused test, or local render check for visual risk.\n- Final response should be short: what changed and any verification.\n`,
688
688
  'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse the local SKS control surface. Prefer these discovery commands when the user asks what is available: sks commands, sks usage <topic>, sks quickstart, sks codex-app, sks context7 check, sks guard check, sks conflicts check, sks reasoning, sks wiki pack, and sks pipeline status. If implementation is requested, route to the lightest matching SKS path and keep reasoning-profile changes temporary. For code-changing execution, first surface route/guard/write-scope status, then use worker subagents by default when scopes are independent; the parent integrates and verifies, while urgent blocking work stays local. Context tracking uses TriWiki as the SSOT for long-running or cross-turn work. Do not edit installed harness control files; the harness guard blocks LLM writes after setup except in the Sneakoscope engine source repo. If OMX/DCodex or another explicit Codex harness is detected, do not install SKS; use sks conflicts prompt and require human approval before cleanup.\n`,
689
689
  'team': `---\nname: team\ndescription: Dollar-command route for $Team or $team SKS Team multi-agent orchestration: parallel analysis scouts, TriWiki refresh, role-counted debate, fresh executor development team, live transcript, and final integration.\n---\n\nUse when the user invokes $Team/$team, asks for a team of agents, or asks for parallel specialist implementation.\n\nWorkflow:\n1. Create or inspect the Team mission with sks team \"task\" when useful. Role counts use executor:5 reviewer:2 user:1 planner:1. executor:N means exactly N analysis_scout_N agents first, exactly N debate participants next, and then a separate N-person executor development team. --agents N, --sessions N, and --team-size N remain aliases for executor/session budget; --max-agents uses the configured default maximum of 6 sessions/agents; default is executor:3 reviewer:1 user:1 planner:1.\n2. Parallel analysis scouts: spawn the concrete analysis_scout_N roster read-only. Split repo, docs, tests, API, DB-risk, UX-friction, and implementation-surface investigation into independent slices. Each scout returns source-backed findings for team-analysis.md.\n3. TriWiki refresh: parent turns scout findings into TriWiki-ready claims, runs sks wiki pack, then runs sks wiki validate .sneakoscope/wiki/context-pack.json. Do not move to debate or implementation until the pack is refreshed and validated.\n4. Debate bundle: spawn the concrete debate_team roster using the refreshed TriWiki context. Users are intentionally low-context, self-interested, stubborn, and inconvenience-averse. Executor voices are capable developers. Reviewers are strict. Planners force one coherent objective and required ambiguity-removal questions.\n5. Live visibility phase: after every useful scout finding, subagent status/result/handoff, record it with sks team event <mission-id|latest> --agent <name> --phase <phase> --message \"...\" so the user can see the team conversation without tmux.\n6. Consensus phase: synthesize debate into one objective, explicit constraints, acceptance criteria, and disjoint implementation slices.\n7. Close or stop the debate team after their results are captured.\n8. Development bundle: form a fresh development_team where exactly executor_N developers implement slices in parallel with non-overlapping ownership. Tell workers they are not alone in the codebase and must not revert others' edits.\n9. Review phase: validation_team reviewers check correctness, DB safety, missing tests, and evidence; user personas reject outcomes that create practical friction.\n10. Verification phase: run focused tests or justify gaps, update mission artifacts when present, and produce final evidence.\n\nLive files:\n- .sneakoscope/missions/<id>/team-analysis.md stores source-backed scout findings and TriWiki-ready claims.\n- .sneakoscope/missions/<id>/team-live.md is the user-readable live transcript inside Codex App.\n- .sneakoscope/missions/<id>/team-transcript.jsonl is the machine-readable event stream.\n- .sneakoscope/missions/<id>/team-dashboard.json is the current dashboard.\n\nRules:\n- The parent agent remains orchestrator and owns final integration.\n- Before spawning development workers, surface visible SKS route, guard, write-scope, TriWiki, and verification status.\n- Do not delegate the immediate blocking task when the parent can do it faster.\n- Use high reasoning only while the Team route is active, then return to the default/user-selected profile.\n- Never let subagents bypass SKS hooks, DB safety, no-question Ralph rules, or H-Proof completion gates.\n- Destructive database actions remain forbidden.\n`,
690
+ 'agent-team': `---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team/$team when the app hides or reserves the plain team skill name.\n---\n\nUse exactly like $Team. This skill exists so npm install, sks setup, and sks doctor --fix can repair Codex App discovery when the plain \`team\` skill file exists but does not appear in the picker.\n\nRoute:\n- Treat $agent-team as $Team.\n- Create or inspect the Team mission with sks team \"task\" when useful.\n- Follow the same scout-first Team orchestration protocol: parallel analysis scouts, TriWiki refresh and validation, read-only debate, one sealed objective, fresh executor_N implementation team, strict review, and final evidence.\n- Record live progress with sks team event <mission-id|latest> --agent <name> --phase <phase> --message \"...\".\n\nRules:\n- The parent agent remains orchestrator and owns final integration.\n- Never let subagents bypass SKS hooks, DB safety, no-question Ralph rules, Context7 gates, or H-Proof/Honest Mode.\n- Destructive database actions remain forbidden.\n`,
690
691
  'ralph': `---\nname: ralph\ndescription: Dollar-command route for $Ralph or $ralph mandatory clarification and no-question mission workflows.\n---\n\nUse when the user invokes $Ralph/$ralph or requests a clarification-gated autonomous implementation mission. Prepare with sks ralph prepare, answer/seal required slots when answers are provided, then run only after decision-contract.json exists.\n`,
691
692
  'research': `---\nname: research\ndescription: Dollar-command route for $Research or $research frontier discovery workflows.\n---\n\nUse when the user invokes $Research/$research or asks for research, hypotheses, new mechanisms, falsification, or testable predictions. Prefer sks research prepare and sks research run. Do not use for ordinary code edits.\n`,
692
693
  'autoresearch': `---\nname: autoresearch\ndescription: Dollar-command route for $AutoResearch or $autoresearch iterative experiment loops.\n---\n\nUse when the user invokes $AutoResearch/$autoresearch or asks for iterative improvement, SEO/GEO, ranking, prompt/workflow improvement, benchmark gains, or open-ended experimentation. Follow the autoresearch-loop skill and load seo-geo-optimizer for README, npm, GitHub stars, schema, keyword, AI-search, or discoverability work. Define program, hypothesis, experiment, metric, keep/discard decision, falsification, next experiment, and Honest Mode conclusion.\n`,
@@ -724,7 +725,7 @@ async function installSkills(root) {
724
725
  }
725
726
 
726
727
  function enrichSkillContent(name, content) {
727
- if (!['sks', 'team', 'ralph', 'research', 'autoresearch', 'db', 'gx', 'prompt-pipeline', 'pipeline-runner', 'turbo-context-pack', 'hproof-evidence-bind'].includes(name)) return content;
728
+ if (!['sks', 'team', 'agent-team', 'ralph', 'research', 'autoresearch', 'db', 'gx', 'prompt-pipeline', 'pipeline-runner', 'turbo-context-pack', 'hproof-evidence-bind'].includes(name)) return content;
728
729
  const text = String(content || '').trimEnd();
729
730
  if (text.includes('TriWiki context-tracking SSOT')) return text;
730
731
  return `${text}
@@ -229,6 +229,7 @@ function formatRalphQuestions(schema) {
229
229
  export async function recordContext7Evidence(root, state, payload) {
230
230
  const stage = context7Stage(payload);
231
231
  if (!stage) return null;
232
+ if (!await shouldWritePipelineEvidence(root, state)) return null;
232
233
  const record = { ts: nowIso(), stage, tool: context7ToolName(payload), payload_keys: Object.keys(payload || {}).sort() };
233
234
  const id = state?.mission_id;
234
235
  const file = id ? path.join(missionDir(root, id), 'context7-evidence.jsonl') : path.join(root, '.sneakoscope', 'state', 'context7-evidence.jsonl');
@@ -243,6 +244,7 @@ export async function recordContext7Evidence(root, state, payload) {
243
244
  export async function recordSubagentEvidence(root, state, payload) {
244
245
  const stage = subagentStage(payload);
245
246
  if (!stage) return null;
247
+ if (!await shouldWritePipelineEvidence(root, state)) return null;
246
248
  const record = { ts: nowIso(), stage, tool: subagentToolName(payload), payload_keys: Object.keys(payload || {}).sort() };
247
249
  const id = state?.mission_id;
248
250
  const file = id ? path.join(missionDir(root, id), 'subagent-evidence.jsonl') : path.join(root, '.sneakoscope', 'state', 'subagent-evidence.jsonl');
@@ -254,6 +256,11 @@ export async function recordSubagentEvidence(root, state, payload) {
254
256
  return record;
255
257
  }
256
258
 
259
+ async function shouldWritePipelineEvidence(root, state = {}) {
260
+ if (state?.mission_id) return exists(missionDir(root, state.mission_id));
261
+ return exists(path.join(root, '.sneakoscope', 'state', 'current.json'));
262
+ }
263
+
257
264
  function subagentToolName(payload) {
258
265
  const obj = payload || {};
259
266
  return String(obj.tool_name || obj.name || obj.tool?.name || obj.mcp_tool || obj.command || obj.type || '');
@@ -81,6 +81,7 @@ export const ROUTES = [
81
81
  mode: 'TEAM',
82
82
  route: 'multi-agent team orchestration',
83
83
  description: 'Run parallel analysis scouts, refresh TriWiki, debate with role personas, agree on an objective, close debate agents, then form a fresh executor development team for parallel work.',
84
+ appSkillAliases: ['agent-team'],
84
85
  requiredSkills: ['team', 'pipeline-runner', 'context7-docs', 'prompt-pipeline', 'honest-mode'],
85
86
  lifecycle: ['parallel_analysis_scouting', 'triwiki_refresh', 'planning_debate', 'live_transcript', 'consensus_artifact', 'fresh_implementation_team', 'review_artifact', 'integration_evidence', 'honest_mode'],
86
87
  context7Policy: 'required',
@@ -176,8 +177,14 @@ export const ROUTES = [
176
177
  ];
177
178
 
178
179
  export const DOLLAR_COMMANDS = ROUTES.map(({ command, route, description }) => ({ command, route, description }));
179
- export const DOLLAR_SKILL_NAMES = DOLLAR_COMMANDS.map((c) => dollarSkillName(c.command));
180
- export const DOLLAR_COMMAND_ALIASES = DOLLAR_COMMANDS.map((c) => ({ canonical: c.command, app_skill: `$${dollarSkillName(c.command)}` }));
180
+ export const DOLLAR_SKILL_NAMES = ROUTES.flatMap((route) => [
181
+ dollarSkillName(route.command),
182
+ ...(route.appSkillAliases || [])
183
+ ]);
184
+ export const DOLLAR_COMMAND_ALIASES = ROUTES.flatMap((route) => [
185
+ { canonical: route.command, app_skill: `$${dollarSkillName(route.command)}` },
186
+ ...(route.appSkillAliases || []).map((alias) => ({ canonical: route.command, app_skill: `$${alias}` }))
187
+ ]);
181
188
 
182
189
  export const COMMAND_CATALOG = [
183
190
  { name: 'help', usage: 'sks help [topic]', description: 'Show CLI help or focused help for a topic.' },
@@ -218,7 +225,15 @@ export const COMMAND_CATALOG = [
218
225
 
219
226
  export function routeById(id) {
220
227
  const key = String(id || '').replace(/^\$/, '').toLowerCase();
221
- return ROUTES.find((route) => route.id.toLowerCase() === key || route.mode.toLowerCase() === key) || null;
228
+ return ROUTES.find((route) => {
229
+ const aliases = [
230
+ route.id,
231
+ route.mode,
232
+ dollarSkillName(route.command),
233
+ ...(route.appSkillAliases || [])
234
+ ].map((x) => String(x || '').toLowerCase());
235
+ return aliases.includes(key);
236
+ }) || null;
222
237
  }
223
238
 
224
239
  export function dollarCommand(prompt) {