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 +8 -8
- package/package.json +1 -1
- package/src/cli/main.mjs +74 -2
- package/src/core/db-safety.mjs +2 -2
- package/src/core/harness-guard.mjs +2 -2
- package/src/core/hooks-runtime.mjs +73 -16
- package/src/core/init.mjs +6 -5
- package/src/core/pipeline.mjs +7 -0
- package/src/core/routes.mjs +18 -3
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**:
|
|
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
|
|
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
|
|
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`.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 (
|
|
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');
|
package/src/core/db-safety.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export async function ensureDbSafetyPolicy(root) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export async function loadDbSafetyPolicy(root) {
|
|
36
|
-
const p =
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
|
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 =
|
|
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}
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -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 || '');
|
package/src/core/routes.mjs
CHANGED
|
@@ -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 =
|
|
180
|
-
|
|
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) =>
|
|
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) {
|