sneakoscope 0.9.7 → 0.9.8

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
@@ -491,6 +491,8 @@ Run local checks:
491
491
  npm run repo-audit
492
492
  npm run changelog:check
493
493
  npm run packcheck
494
+ npm run feature:check
495
+ npm run all-features:selftest
494
496
  npm run selftest
495
497
  npm run sizecheck
496
498
  npm run registry:check
@@ -498,7 +500,7 @@ npm run release:check
498
500
  npm run publish:dry
499
501
  ```
500
502
 
501
- `release:check` runs audit, changelog, syntax, selftest, size, and registry checks. `publish:dry` runs that same gate and then performs an npm dry-run publish against the public registry.
503
+ `release:check` runs audit, changelog, syntax, feature-registry coverage, all-features contract selftest, selftest, size, and registry checks. Generate the human-readable registry with `sks features inventory --write-docs`. `publish:dry` runs that same gate and then performs an npm dry-run publish against the public registry.
502
504
 
503
505
  Version bumps are manual. Run `sks versioning bump` only when preparing release metadata; SKS will not create `.git/hooks/pre-commit` or auto-bump during ordinary commits.
504
506
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.9.7",
4
+ "version": "0.9.8",
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",
@@ -38,7 +38,9 @@
38
38
  "changelog:check": "node ./scripts/changelog-check.mjs",
39
39
  "sizecheck": "node ./scripts/sizecheck.mjs",
40
40
  "registry:check": "node ./scripts/release-registry-check.mjs",
41
- "release:check": "npm run repo-audit && npm run changelog:check && npm run packcheck && npm run selftest && npm run sizecheck && npm run registry:check",
41
+ "feature:check": "node ./bin/sks.mjs features check --json",
42
+ "all-features:selftest": "node ./bin/sks.mjs all-features selftest --mock --json",
43
+ "release:check": "npm run repo-audit && npm run changelog:check && npm run packcheck && npm run feature:check && npm run all-features:selftest && npm run selftest && npm run sizecheck && npm run registry:check",
42
44
  "publish:dry": "npm run release:check && npm --cache /tmp/sks-npm-cache publish --dry-run --registry https://registry.npmjs.org/ --access public",
43
45
  "publish:npm": "npm --cache /tmp/sks-npm-cache publish --registry https://registry.npmjs.org/ --access public",
44
46
  "prepublishOnly": "npm run release:check && node ./scripts/release-registry-check.mjs --require-unpublished"
@@ -0,0 +1,131 @@
1
+ import path from 'node:path';
2
+ import { projectRoot } from '../core/fsx.mjs';
3
+ import { CODEX_ACCESS_TOKENS_DOCS_URL } from '../core/codex-app.mjs';
4
+ import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry, writeFeatureInventoryDocs } from '../core/feature-registry.mjs';
5
+
6
+ const flag = (args, name) => args.includes(name);
7
+
8
+ export async function featuresCommand(sub = 'list', args = []) {
9
+ const action = sub || 'list';
10
+ const root = await projectRoot();
11
+ if (action === 'list' || action === 'status' || action === 'registry') {
12
+ const registry = await buildFeatureRegistry({ root });
13
+ if (flag(args, '--json')) return console.log(JSON.stringify(registry, null, 2));
14
+ printFeatureRegistrySummary(registry);
15
+ if (!registry.coverage.ok) process.exitCode = 1;
16
+ return;
17
+ }
18
+ if (action === 'check') {
19
+ const registry = await buildFeatureRegistry({ root });
20
+ const coverage = validateFeatureRegistry(registry);
21
+ const result = { schema: 'sks.feature-registry-check.v1', generated_at: registry.generated_at, ok: coverage.ok, coverage };
22
+ if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
23
+ else printFeatureCoverage(coverage);
24
+ if (!coverage.ok) process.exitCode = 1;
25
+ return;
26
+ }
27
+ if (action === 'inventory') {
28
+ const writeDocs = flag(args, '--write-docs');
29
+ const result = writeDocs
30
+ ? await writeFeatureInventoryDocs({ root })
31
+ : { ok: true, registry: await buildFeatureRegistry({ root }), path: path.join(root, 'docs', 'feature-inventory.md') };
32
+ if (flag(args, '--json')) return console.log(JSON.stringify({ ok: result.ok, path: result.path, coverage: result.registry.coverage }, null, 2));
33
+ if (writeDocs) console.log(`Feature inventory written: ${path.relative(root, result.path)}`);
34
+ printFeatureRegistrySummary(result.registry);
35
+ if (!result.registry.coverage.ok) process.exitCode = 1;
36
+ return;
37
+ }
38
+ console.error('Usage: sks features list|check|inventory [--json] [--write-docs]');
39
+ process.exitCode = 1;
40
+ }
41
+
42
+ export async function allFeaturesCommand(sub = 'selftest', args = []) {
43
+ const action = sub || 'selftest';
44
+ if (action !== 'selftest') {
45
+ console.error('Usage: sks all-features selftest --mock [--json]');
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const root = await projectRoot();
50
+ const registry = await buildFeatureRegistry({ root });
51
+ const result = buildAllFeaturesSelftest(registry);
52
+ if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
53
+ else {
54
+ console.log('SKS all-features selftest');
55
+ console.log(`Status: ${result.status}`);
56
+ for (const check of result.checks) console.log(`- ${check.ok ? 'ok' : 'blocked'} ${check.id}${check.blockers.length ? `: ${check.blockers.join(', ')}` : ''}`);
57
+ if (result.note) console.log(`\n${result.note}`);
58
+ }
59
+ if (!result.ok) process.exitCode = 1;
60
+ }
61
+
62
+ export function hooksCommand(sub = 'explain', args = []) {
63
+ const action = sub || 'explain';
64
+ if (action !== 'explain' && action !== 'status') {
65
+ console.error('Usage: sks hooks explain [--json]');
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ const report = hooksExplainReport();
70
+ if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
71
+ console.log('SKS hooks explain\n');
72
+ console.log(`Status: ${report.status}`);
73
+ console.log(`Feature key: ${report.feature_key}`);
74
+ console.log(`Config paths: ${report.config_paths.join(', ')}`);
75
+ console.log(`Events: ${report.events.join(', ')}`);
76
+ console.log(`Handlers: ${report.handlers.supported.join(', ')} supported; ${report.handlers.parsed_but_skipped.join(', ')} parsed but skipped`);
77
+ console.log('\nPolicies:');
78
+ for (const policy of report.sks_policies) console.log(`- ${policy}`);
79
+ console.log('\nSources:');
80
+ for (const source of report.sources) console.log(`- ${source.title}: ${source.url}`);
81
+ }
82
+
83
+ export function hooksExplainReport() {
84
+ return {
85
+ schema: 'sks.hooks-explain.v1',
86
+ status: 'supported_by_official_docs_and_local_config',
87
+ feature_key: 'features.hooks',
88
+ deprecated_feature_alias: 'features.codex_hooks',
89
+ config_paths: ['~/.codex/hooks.json', '~/.codex/config.toml', '<repo>/.codex/hooks.json', '<repo>/.codex/config.toml'],
90
+ events: ['SessionStart', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'UserPromptSubmit', 'Stop'],
91
+ handlers: {
92
+ supported: ['command'],
93
+ parsed_but_skipped: ['prompt', 'agent'],
94
+ async_command_hooks: 'parsed_but_not_supported'
95
+ },
96
+ runtime_notes: [
97
+ 'Matching hooks from multiple files all run.',
98
+ 'Multiple matching command hooks for the same event are launched concurrently.',
99
+ 'Project-local hooks require a trusted project .codex layer.',
100
+ 'Repo-local hook commands should resolve from git root instead of assuming the session cwd.'
101
+ ],
102
+ sks_policies: [
103
+ 'secret_scan_policy',
104
+ 'directory_rule_policy',
105
+ 'db_safety_policy',
106
+ 'visual_claim_source_policy',
107
+ 'proof_required_policy',
108
+ 'codex_lb_health_policy'
109
+ ],
110
+ sources: [
111
+ { title: 'OpenAI Codex Hooks', url: 'https://developers.openai.com/codex/hooks' },
112
+ { title: 'OpenAI Codex Configuration Reference', url: 'https://developers.openai.com/codex/config-reference' },
113
+ { title: 'OpenAI Codex Access Tokens', url: CODEX_ACCESS_TOKENS_DOCS_URL }
114
+ ]
115
+ };
116
+ }
117
+
118
+ function printFeatureRegistrySummary(registry) {
119
+ console.log('SKS feature registry\n');
120
+ console.log(`Schema: ${registry.schema}`);
121
+ console.log(`Features: ${registry.features.length}`);
122
+ printFeatureCoverage(registry.coverage);
123
+ }
124
+
125
+ function printFeatureCoverage(coverage = {}) {
126
+ console.log(`Coverage: ${coverage.ok ? 'ok' : 'blocked'} (${coverage.status || 'unknown'})`);
127
+ for (const [kind, values] of Object.entries(coverage.unmapped || {})) {
128
+ console.log(`- ${kind}: ${values.length ? values.join(', ') : 'none'}`);
129
+ }
130
+ if (coverage.blockers?.length) console.log(`Blockers: ${coverage.blockers.join(', ')}`);
131
+ }
package/src/cli/main.mjs CHANGED
@@ -73,8 +73,10 @@ import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/
73
73
  import { buildPromptContext } from '../core/prompt-context-builder.mjs';
74
74
  import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
75
75
  import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
76
- import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
76
+ import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
77
+ import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry } from '../core/feature-registry.mjs';
77
78
  import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
79
+ import { allFeaturesCommand, featuresCommand, hooksCommand, hooksExplainReport } from './feature-commands.mjs';
78
80
  import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
79
81
  import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, sweepCodexLbTmuxSessions, sweepTmuxTeamSurfaces, teamLaneStyle } from '../core/tmux-ui.mjs';
80
82
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
@@ -111,7 +113,7 @@ export async function main(args) {
111
113
  if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
112
114
  const handlers = {
113
115
  postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), auth: () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
114
- 'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), 'image-ux-review': () => imageUxReviewCommand(sub, rest), 'ux-review': () => imageUxReviewCommand(sub, rest), 'visual-review': () => imageUxReviewCommand(sub, rest), 'ui-ux-review': () => imageUxReviewCommand(sub, rest), context7: () => context7Command(sub, rest), recallpulse: () => recallPulseCommand(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
116
+ 'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), 'image-ux-review': () => imageUxReviewCommand(sub, rest), 'ux-review': () => imageUxReviewCommand(sub, rest), 'visual-review': () => imageUxReviewCommand(sub, rest), 'ui-ux-review': () => imageUxReviewCommand(sub, rest), context7: () => context7Command(sub, rest), recallpulse: () => recallPulseCommand(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), features: () => featuresCommand(sub, rest), 'all-features': () => allFeaturesCommand(sub, rest), hooks: () => hooksCommand(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
115
117
  goal: () => goalCommand(sub, rest), research: () => researchCommand(sub, rest), hook: () => emitHook(sub), profile: () => profileCommand(sub, rest), hproof: () => hproofCommand(sub, rest), 'validate-artifacts': () => validateArtifactsCommand(tail), perf: () => perfCommand(sub, rest), 'proof-field': () => proofFieldCommand(sub, rest), 'skill-dream': () => skillDreamCommand(sub, rest), 'code-structure': () => codeStructureCommand(sub, rest), memory: () => memoryCommand(sub, rest), gx: () => gxCommand(sub, rest),
116
118
  team: () => team(tail), db: () => dbCommand(sub, rest), eval: () => evalCommand(sub, rest), harness: () => harnessCommand(sub, rest), wiki: () => wikiCommand(sub, rest), gc: () => gcCommand(tail), stats: () => statsCommand(tail)
117
119
  };
@@ -188,6 +190,8 @@ Usage:
188
190
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
189
191
  sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
190
192
  sks codex-app
193
+ sks codex-app pat status [--json]
194
+ sks hooks explain [--json]
191
195
  sks codex-lb status|health|repair|release|unselect|setup --host <domain> --api-key <key>
192
196
  sks auth status|health|repair|release|unselect|setup --host <domain> --api-key <key>
193
197
  sks openclaw install|path|print [--dir path] [--force] [--json]
@@ -218,6 +222,8 @@ Usage:
218
222
  sks doctor [--fix] [--local-only] [--json] [--install-scope global|project]
219
223
  sks init [--install-scope global|project] [--local-only]
220
224
  sks selftest [--mock]
225
+ sks features list|check|inventory [--json] [--write-docs]
226
+ sks all-features selftest --mock [--json]
221
227
  sks goal create "task"
222
228
  sks goal pause|resume|clear <mission-id|latest>
223
229
  sks goal status <mission-id|latest>
@@ -1499,6 +1505,23 @@ async function autoReviewCommand(sub = 'status', args = []) {
1499
1505
  async function codexAppHelp(args = []) {
1500
1506
  const action = args[0] || 'help';
1501
1507
  if (action === 'remote-control' || action === 'remote') return codexAppRemoteControlCommand(args.slice(1));
1508
+ if (action === 'pat') {
1509
+ const patAction = args[1] || 'status';
1510
+ if (patAction !== 'status' && patAction !== 'check') {
1511
+ console.error('Usage: sks codex-app pat status [--json]');
1512
+ process.exitCode = 1;
1513
+ return;
1514
+ }
1515
+ const status = codexAccessTokenStatus();
1516
+ if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
1517
+ console.log('Codex App PAT status\n');
1518
+ console.log(`Status: ${status.status}`);
1519
+ console.log(`Docs: ${status.docs_url}`);
1520
+ console.log(`Policy: ${status.storage_policy}`);
1521
+ for (const entry of status.access_token_env_vars) console.log(`${entry.name}: ${entry.present ? entry.value : 'missing'}`);
1522
+ for (const warning of status.warnings) console.log(`- ${warning}`);
1523
+ return;
1524
+ }
1502
1525
  if (action === 'check' || action === 'status') {
1503
1526
  const status = await codexAppIntegrationStatus();
1504
1527
  const skills = await codexAppSkillReadiness();
@@ -1526,7 +1549,7 @@ async function codexAppHelp(args = []) {
1526
1549
  'Codex App', '',
1527
1550
  formatCodexAppStatus(status), '',
1528
1551
  `Skills: project=${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} global=${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`}`, '',
1529
- 'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks tmux check', '',
1552
+ 'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks codex-app pat status', ' sks codex-app remote-control --status', ' sks tmux check', '',
1530
1553
  'Generated files:', ' .codex/config.toml', ' .codex/hooks.json', ' .agents/skills/', ' .codex/agents/', ' .codex/SNEAKOSCOPE.md', ' AGENTS.md', '',
1531
1554
  'Git ignore:', ' default setup writes .gitignore entries for .sneakoscope/, .codex/, .agents/, AGENTS.md', ' --local-only writes those patterns to .git/info/exclude instead', '',
1532
1555
  'Prompt routes:', formatDollarCommandsCompact(' ')
@@ -2717,7 +2740,18 @@ async function selftest() {
2717
2740
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$From-Chat-IMG')) throw new Error('selftest: dollar-commands missing From-Chat-IMG guidance');
2718
2741
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$MAD-SKS')) throw new Error('selftest: dollar-commands missing MAD-SKS scoped override guidance');
2719
2742
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$Image-UX-Review')) throw new Error('selftest: dollar-commands missing Image UX Review guidance');
2720
- if (!COMMAND_CATALOG.some((c) => c.name === 'context7') || !COMMAND_CATALOG.some((c) => c.name === 'pipeline') || !COMMAND_CATALOG.some((c) => c.name === 'qa-loop') || !COMMAND_CATALOG.some((c) => c.name === 'image-ux-review') || !COMMAND_CATALOG.some((c) => c.name === 'root') || !COMMAND_CATALOG.some((c) => c.name === 'openclaw')) throw new Error('selftest: context7/pipeline/qa-loop/image-ux-review/root/openclaw commands missing from catalog');
2743
+ for (const name of ['context7', 'pipeline', 'qa-loop', 'image-ux-review', 'root', 'openclaw', 'hooks', 'features', 'all-features']) {
2744
+ if (!COMMAND_CATALOG.some((c) => c.name === name)) throw new Error(`selftest: catalog missing ${name}`);
2745
+ }
2746
+ const featureRegistry = await buildFeatureRegistry({ root: packageRoot() });
2747
+ const featureCoverage = validateFeatureRegistry(featureRegistry);
2748
+ if (!featureCoverage.ok) throw new Error(`selftest: feature registry coverage blocked: ${featureCoverage.blockers.join(', ')}`);
2749
+ const allFeaturesResult = buildAllFeaturesSelftest(featureRegistry);
2750
+ if (!allFeaturesResult.ok) throw new Error(`selftest: all-features contract blocked: ${allFeaturesResult.checks.filter((check) => !check.ok).map((check) => check.id).join(', ')}`);
2751
+ const patRedactionProbe = codexAccessTokenStatus({ CODEX_ACCESS_TOKEN: 'secret-probe-value', CODEX_LB_API_KEY: 'lb-secret-probe' });
2752
+ if (patRedactionProbe.status !== 'present_redacted' || JSON.stringify(patRedactionProbe).includes('secret-probe-value') || JSON.stringify(patRedactionProbe).includes('lb-secret-probe')) throw new Error('selftest: Codex access token status leaked a token value');
2753
+ const hooksReport = hooksExplainReport();
2754
+ if (!hooksReport.events.includes('UserPromptSubmit') || !hooksReport.events.includes('Stop') || !hooksReport.sources.some((source) => source.url.includes('/access-tokens'))) throw new Error('selftest: hooks explain coverage');
2721
2755
  const openClawTmp = tmpdir();
2722
2756
  const openClawResult = await installOpenClawSkill({ targetDir: path.join(openClawTmp, 'skills', OPENCLAW_SKILL_NAME) });
2723
2757
  if (!openClawResult.ok) throw new Error(`selftest: OpenClaw skill install blocked: ${openClawResult.reason}`);
@@ -7,6 +7,7 @@ import { DEFAULT_CODEX_APP_PLUGINS as DEFAULT_CODEX_APP_PLUGIN_TUPLES, RESERVED_
7
7
 
8
8
  export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
9
9
  export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
10
+ export const CODEX_ACCESS_TOKENS_DOCS_URL = 'https://developers.openai.com/codex/enterprise/access-tokens';
10
11
  export const CODEX_REMOTE_CONTROL_MIN_VERSION = '0.130.0';
11
12
  const REQUIRED_CODEX_APP_FEATURE_FLAGS = [
12
13
  'codex_git_commit',
@@ -253,6 +254,37 @@ export function formatCodexRemoteControlStatus(status) {
253
254
  return lines.filter(Boolean).join('\n');
254
255
  }
255
256
 
257
+ export function codexAccessTokenStatus(env = process.env) {
258
+ const accessTokenVars = ['CODEX_ACCESS_TOKEN'];
259
+ const adjacentSecretVars = ['OPENAI_API_KEY', 'CODEX_LB_API_KEY'];
260
+ const accessTokens = accessTokenVars.map((name) => ({ name, present: Boolean(env[name]), value: env[name] ? '[redacted]' : null }));
261
+ const adjacentSecrets = adjacentSecretVars.map((name) => ({ name, present: Boolean(env[name]), value: env[name] ? '[redacted]' : null }));
262
+ const extraTokenLikeVars = Object.keys(env)
263
+ .filter((name) => /(?:CODEX|OPENAI|CHATGPT).*TOKEN/i.test(name) && !accessTokenVars.includes(name))
264
+ .sort()
265
+ .map((name) => ({ name, present: true, value: '[redacted]' }));
266
+ const present = accessTokens.some((entry) => entry.present);
267
+ return {
268
+ schema: 'sks.codex-access-token-status.v1',
269
+ ok: true,
270
+ status: present ? 'present_redacted' : 'missing',
271
+ supported_for: 'ChatGPT Business and Enterprise workspace programmatic local Codex workflows',
272
+ docs_url: CODEX_ACCESS_TOKENS_DOCS_URL,
273
+ official_cli_ingest: 'codex login --with-access-token reads CODEX_ACCESS_TOKEN from stdin when the caller provides it',
274
+ storage_policy: 'Store access tokens in an external secret manager or ephemeral environment variable; never write plaintext tokens into .sneakoscope, hooks, proof, stdout, stderr, or screenshots.',
275
+ access_token_env_vars: accessTokens,
276
+ adjacent_secret_env_vars: adjacentSecrets,
277
+ extra_token_like_env_vars: extraTokenLikeVars,
278
+ redaction: {
279
+ ok: true,
280
+ strategy: 'presence-only reporting with literal [redacted] value placeholders'
281
+ },
282
+ warnings: present
283
+ ? ['Token presence was detected without printing the value. Rotate regularly and use only trusted runners.']
284
+ : ['No CODEX_ACCESS_TOKEN detected in the current process environment. This is fine for interactive ChatGPT login or API-key auth.']
285
+ };
286
+ }
287
+
256
288
  export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, pluginSkillShadows = { ok: true, blocking: [] }, fastModeConfig = { ok: true, blockers: [] }, gitActions = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
257
289
  const lines = [];
258
290
  if (!appInstalled) {
@@ -0,0 +1,461 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS } from './routes.mjs';
4
+ import { exists, nowIso, packageRoot, readJson, readText, writeTextAtomic } from './fsx.mjs';
5
+
6
+ export const FEATURE_REGISTRY_SCHEMA = 'sks.feature-registry.v1';
7
+ export const FEATURE_INVENTORY_SCHEMA = 'sks.feature-inventory.v1';
8
+ export const ALL_FEATURES_SELFTEST_SCHEMA = 'sks.all-features-selftest.v1';
9
+
10
+ const HANDLER_ALIAS_TO_COMMAND = Object.freeze({
11
+ ui: 'wizard',
12
+ auth: 'codex-lb',
13
+ hook: 'hooks',
14
+ memory: 'gc',
15
+ postinstall: 'postinstall',
16
+ 'ux-review': 'image-ux-review',
17
+ 'visual-review': 'image-ux-review',
18
+ 'ui-ux-review': 'image-ux-review'
19
+ });
20
+
21
+ const FEATURE_ACCEPTANCE_DEFAULTS = Object.freeze([
22
+ 'contracts tracked',
23
+ 'unknowns explicit',
24
+ 'release-mapped'
25
+ ]);
26
+
27
+ export async function buildFeatureRegistry({ root = packageRoot(), generatedAt = nowIso() } = {}) {
28
+ const handlerKeys = await parseMainHandlerKeys(root);
29
+ const skillNames = await listProjectSkillNames(root);
30
+ const docRouteMentions = await collectDocRouteMentions(root);
31
+ const handlerToFeature = mapHandlerKeysToFeatureIds(handlerKeys);
32
+ const features = [];
33
+
34
+ for (const command of COMMAND_CATALOG) {
35
+ const handlerAliases = Object.entries(handlerToFeature)
36
+ .filter(([, featureId]) => featureId === `cli-${command.name}`)
37
+ .map(([handler]) => handler)
38
+ .filter((handler) => handler !== command.name);
39
+ features.push(commandFeature(command, handlerAliases));
40
+ }
41
+
42
+ for (const handler of handlerKeys) {
43
+ const featureId = handlerToFeature[handler];
44
+ if (!features.some((feature) => feature.id === featureId)) {
45
+ features.push(hiddenHandlerFeature(handler));
46
+ }
47
+ }
48
+
49
+ for (const route of DOLLAR_COMMANDS) features.push(routeFeature(route));
50
+ for (const skillName of skillNames) {
51
+ if (!skillCoveredByRoute(skillName)) features.push(skillFeature(skillName));
52
+ }
53
+
54
+ const registry = {
55
+ schema: FEATURE_REGISTRY_SCHEMA,
56
+ generated_at: generatedAt,
57
+ inventory_sources: {
58
+ commands_json: 'sks commands --json',
59
+ main_handlers: 'src/cli/main.mjs',
60
+ dollar_routes: 'src/core/routes.mjs',
61
+ docs: ['README.md', '.codex/SNEAKOSCOPE.md', 'AGENTS.md', '.agents/skills/.sks-generated.json'],
62
+ skills: '.agents/skills'
63
+ },
64
+ features,
65
+ source_inventory: {
66
+ cli_command_names: COMMAND_CATALOG.map((entry) => entry.name),
67
+ handler_keys: handlerKeys,
68
+ dollar_commands: DOLLAR_COMMANDS.map((entry) => entry.command),
69
+ app_skill_aliases: DOLLAR_COMMAND_ALIASES.map((entry) => entry.app_skill),
70
+ skills: skillNames,
71
+ doc_route_mentions: docRouteMentions
72
+ }
73
+ };
74
+ registry.coverage = validateFeatureRegistry(registry);
75
+ return registry;
76
+ }
77
+
78
+ export function validateFeatureRegistry(registry = {}) {
79
+ const features = Array.isArray(registry.features) ? registry.features : [];
80
+ const source = registry.source_inventory || {};
81
+ const mappedCli = new Set(flatMapSourceRefs(features, 'cli_command_names'));
82
+ const mappedHandlers = new Set(flatMapSourceRefs(features, 'handler_keys'));
83
+ const mappedRoutes = new Set(flatMapSourceRefs(features, 'dollar_commands'));
84
+ const mappedAliases = new Set(flatMapSourceRefs(features, 'app_skill_aliases'));
85
+ const mappedSkills = new Set(flatMapSourceRefs(features, 'skills'));
86
+ const mappedRouteMentions = new Set([...mappedRoutes, ...mappedAliases].map(normalizeDollar));
87
+
88
+ const unmapped = {
89
+ cli_command_names: (source.cli_command_names || []).filter((name) => !mappedCli.has(name)),
90
+ handler_keys: (source.handler_keys || []).filter((name) => !mappedHandlers.has(name)),
91
+ dollar_commands: (source.dollar_commands || []).filter((name) => !mappedRoutes.has(name)),
92
+ app_skill_aliases: (source.app_skill_aliases || []).filter((name) => !mappedAliases.has(name)),
93
+ skills: (source.skills || []).filter((name) => !mappedSkills.has(name))
94
+ };
95
+ const duplicateFeatureIds = duplicateValues(features.map((feature) => feature.id));
96
+ const routeMentionsWithoutRoute = (source.doc_route_mentions || [])
97
+ .filter((mention) => !mappedRouteMentions.has(normalizeDollar(mention)) && !isExternalPromptCommandMention(mention));
98
+ const blockers = [
99
+ ...Object.entries(unmapped).flatMap(([kind, values]) => values.map((value) => `${kind}:${value}`)),
100
+ ...duplicateFeatureIds.map((id) => `duplicate_feature_id:${id}`),
101
+ ...routeMentionsWithoutRoute.map((mention) => `doc_route_mention_without_route:${mention}`)
102
+ ];
103
+ return {
104
+ ok: blockers.length === 0,
105
+ status: blockers.length ? 'blocked' : 'verified_partial',
106
+ counts: {
107
+ features: features.length,
108
+ cli_command_names: source.cli_command_names?.length || 0,
109
+ handler_keys: source.handler_keys?.length || 0,
110
+ dollar_commands: source.dollar_commands?.length || 0,
111
+ app_skill_aliases: source.app_skill_aliases?.length || 0,
112
+ skills: source.skills?.length || 0
113
+ },
114
+ unmapped,
115
+ duplicate_feature_ids: duplicateFeatureIds,
116
+ doc_route_mentions_without_route: routeMentionsWithoutRoute,
117
+ blockers,
118
+ nonblocking_known_gaps: [
119
+ 'feature fixtures remain progressive',
120
+ 'registry proves coverage, not full roadmap completion'
121
+ ]
122
+ };
123
+ }
124
+
125
+ export async function writeFeatureInventoryDocs({ root = packageRoot(), outFile = path.join(root, 'docs', 'feature-inventory.md') } = {}) {
126
+ const registry = await buildFeatureRegistry({ root });
127
+ const markdown = renderFeatureInventoryMarkdown(registry);
128
+ await writeTextAtomic(outFile, markdown);
129
+ return { ok: registry.coverage.ok, path: outFile, registry };
130
+ }
131
+
132
+ export function buildAllFeaturesSelftest(registry) {
133
+ const coverage = validateFeatureRegistry(registry);
134
+ const checks = [
135
+ checkRow('feature_registry_completeness', coverage.ok, coverage.blockers),
136
+ checkRow('command_lazy_load_availability', coverage.unmapped.cli_command_names.length === 0 && coverage.unmapped.handler_keys.length === 0, [...coverage.unmapped.cli_command_names, ...coverage.unmapped.handler_keys]),
137
+ checkRow('json_schema_validation', registry.schema === FEATURE_REGISTRY_SCHEMA && Array.isArray(registry.features), []),
138
+ checkRow('proof_integration_contracts_present', registry.features.every((feature) => Boolean(feature.completion_proof_integration)), missingFeatureField(registry, 'completion_proof_integration')),
139
+ checkRow('voxel_triwiki_contracts_present', registry.features.every((feature) => Boolean(feature.voxel_triwiki_integration)), missingFeatureField(registry, 'voxel_triwiki_integration')),
140
+ checkRow('failure_contracts_present', registry.features.every((feature) => Array.isArray(feature.known_gaps)), missingFeatureField(registry, 'known_gaps'))
141
+ ];
142
+ const ok = checks.every((check) => check.ok);
143
+ return {
144
+ schema: ALL_FEATURES_SELFTEST_SCHEMA,
145
+ generated_at: registry.generated_at || nowIso(),
146
+ ok,
147
+ status: ok ? 'verified_partial' : 'blocked',
148
+ checks,
149
+ coverage,
150
+ note: 'Mock selftest verifies the shared contract spine; feature fixtures remain progressive.'
151
+ };
152
+ }
153
+
154
+ export function renderFeatureInventoryMarkdown(registry) {
155
+ const coverage = registry.coverage || validateFeatureRegistry(registry);
156
+ const lines = [
157
+ '# SKS Feature Inventory',
158
+ '',
159
+ `Generated from \`${registry.inventory_sources.commands_json}\`, \`${registry.inventory_sources.main_handlers}\`, \`${registry.inventory_sources.dollar_routes}\`, docs, and skill manifests.`,
160
+ '',
161
+ '## Coverage',
162
+ '',
163
+ `- Status: ${coverage.ok ? 'coverage-ok' : 'blocked'}`,
164
+ `- Features: ${coverage.counts.features}`,
165
+ `- CLI commands: ${coverage.counts.cli_command_names}`,
166
+ `- Handler keys: ${coverage.counts.handler_keys}`,
167
+ `- Dollar routes: ${coverage.counts.dollar_commands}`,
168
+ `- App skill aliases: ${coverage.counts.app_skill_aliases}`,
169
+ `- Skills: ${coverage.counts.skills}`,
170
+ '',
171
+ '## Release Coverage Rule',
172
+ '',
173
+ '`sks features check --json` fails when a CLI command, hidden handler, dollar route, app skill alias, or project skill is not mapped to the feature registry. `npm run release:check` runs that check.',
174
+ '',
175
+ '## Stable / Beta / Labs Map',
176
+ '',
177
+ '| Feature | Category | Maturity | Commands / Routes | Known Gaps |',
178
+ '| --- | --- | --- | --- | --- |'
179
+ ];
180
+ for (const feature of registry.features) {
181
+ const commands = [...(feature.commands || []), ...(feature.aliases || [])].map(markdownTableCell).join('<br>');
182
+ const gaps = (feature.known_gaps || []).map(markdownTableCell).join('<br>') || 'none recorded';
183
+ lines.push(`| \`${feature.id}\` | ${feature.category} | ${feature.maturity} | ${commands || '-'} | ${gaps} |`);
184
+ }
185
+ lines.push('', '## Unmapped Coverage', '');
186
+ for (const [kind, values] of Object.entries(coverage.unmapped || {})) {
187
+ lines.push(`- ${kind}: ${values.length ? values.join(', ') : 'none'}`);
188
+ }
189
+ if (coverage.doc_route_mentions_without_route?.length) {
190
+ lines.push(`- doc_route_mentions_without_route: ${coverage.doc_route_mentions_without_route.join(', ')}`);
191
+ }
192
+ lines.push('', '## Prompt Checklist Coverage', '');
193
+ lines.push('- [x] Collected `sks commands --json` command surface via `COMMAND_CATALOG`.');
194
+ lines.push('- [x] Parsed `src/cli/main.mjs` handler keys, including hidden handlers and aliases.');
195
+ lines.push('- [x] Collected dollar routes and app skill aliases from `src/core/routes.mjs`.');
196
+ lines.push('- [x] Scanned README, Codex quick reference, AGENTS, and generated skill manifest for dollar-route mentions.');
197
+ lines.push('- [x] Mapped project skills from `.agents/skills` into the registry.');
198
+ lines.push('- [x] Exposed the registry through `sks features list --json`.');
199
+ lines.push('- [x] Added a release coverage check through `sks features check --json`.');
200
+ lines.push('');
201
+ return `${lines.join('\n')}\n`;
202
+ }
203
+
204
+ function commandFeature(command, handlerAliases = []) {
205
+ const name = command.name;
206
+ const category = commandCategory(name);
207
+ const maturity = commandMaturity(name);
208
+ const aliases = [...new Set(handlerAliases.map((alias) => `sks ${alias}`))];
209
+ return baseFeature({
210
+ id: `cli-${name}`,
211
+ commands: [command.usage],
212
+ aliases,
213
+ category,
214
+ maturity,
215
+ intent: command.description,
216
+ completion_proof_integration: proofContract(category),
217
+ voxel_triwiki_integration: voxelContract(category),
218
+ known_gaps: knownGapsForCommand(name),
219
+ source_refs: {
220
+ cli_command_names: [name],
221
+ handler_keys: [name, ...handlerAliases],
222
+ dollar_commands: [],
223
+ app_skill_aliases: [],
224
+ skills: []
225
+ }
226
+ });
227
+ }
228
+
229
+ function hiddenHandlerFeature(handler) {
230
+ return baseFeature({
231
+ id: `handler-${handler}`,
232
+ commands: [`sks ${handler}`],
233
+ aliases: [],
234
+ category: 'internal',
235
+ maturity: 'beta',
236
+ intent: `Hidden or internal handler for ${handler}.`,
237
+ voxel_triwiki_integration: 'context-anchor optional unless route output creates evidence',
238
+ completion_proof_integration: 'required for stateful route use',
239
+ known_gaps: ['hidden handler docs needed if promoted'],
240
+ source_refs: {
241
+ cli_command_names: [],
242
+ handler_keys: [handler],
243
+ dollar_commands: [],
244
+ app_skill_aliases: [],
245
+ skills: []
246
+ }
247
+ });
248
+ }
249
+
250
+ function routeFeature(route) {
251
+ const aliases = DOLLAR_COMMAND_ALIASES
252
+ .filter((entry) => entry.canonical === route.command)
253
+ .map((entry) => entry.app_skill);
254
+ return baseFeature({
255
+ id: `route-${slug(route.command)}`,
256
+ commands: [route.command],
257
+ aliases,
258
+ category: 'route',
259
+ maturity: routeMaturity(route.command),
260
+ intent: route.description,
261
+ voxel_triwiki_integration: routeVoxelContract(route.command),
262
+ completion_proof_integration: 'route gate, reflection, Honest Mode',
263
+ known_gaps: routeKnownGaps(route.command),
264
+ source_refs: {
265
+ cli_command_names: [],
266
+ handler_keys: [],
267
+ dollar_commands: [route.command],
268
+ app_skill_aliases: aliases,
269
+ skills: aliases.map((alias) => alias.replace(/^\$/, ''))
270
+ }
271
+ });
272
+ }
273
+
274
+ function skillFeature(skillName) {
275
+ return baseFeature({
276
+ id: `skill-${slug(skillName)}`,
277
+ commands: [],
278
+ aliases: [`$${skillName}`],
279
+ category: 'skill',
280
+ maturity: 'labs',
281
+ intent: `Codex skill surface for ${skillName}.`,
282
+ voxel_triwiki_integration: 'inherits owning route contract',
283
+ completion_proof_integration: 'inherits owning route proof',
284
+ known_gaps: ['runtime fixtures owned by route'],
285
+ source_refs: {
286
+ cli_command_names: [],
287
+ handler_keys: [],
288
+ dollar_commands: [],
289
+ app_skill_aliases: [],
290
+ skills: [skillName]
291
+ }
292
+ });
293
+ }
294
+
295
+ function baseFeature(feature) {
296
+ return {
297
+ contract: {
298
+ input: feature.commands?.[0] || feature.aliases?.[0] || 'skill invocation',
299
+ output: 'stdout/json/artifacts by command',
300
+ state: 'route state when stateful',
301
+ safety: 'policy-gated',
302
+ proof: feature.completion_proof_integration,
303
+ voxel: feature.voxel_triwiki_integration,
304
+ tests: 'release/selftest coverage',
305
+ docs: 'feature inventory'
306
+ },
307
+ acceptance: FEATURE_ACCEPTANCE_DEFAULTS,
308
+ ...feature
309
+ };
310
+ }
311
+
312
+ function mapHandlerKeysToFeatureIds(handlerKeys = []) {
313
+ const catalogNames = new Set(COMMAND_CATALOG.map((entry) => entry.name));
314
+ const out = {};
315
+ for (const handler of handlerKeys) {
316
+ const commandName = HANDLER_ALIAS_TO_COMMAND[handler] || handler;
317
+ out[handler] = catalogNames.has(commandName) ? `cli-${commandName}` : `handler-${handler}`;
318
+ }
319
+ return out;
320
+ }
321
+
322
+ async function parseMainHandlerKeys(root) {
323
+ const text = await readText(path.join(root, 'src', 'cli', 'main.mjs'), '');
324
+ const match = text.match(/const handlers = \{([\s\S]*?)\n\s*\};/);
325
+ if (!match) return [];
326
+ const keys = [];
327
+ for (const keyMatch of match[1].matchAll(/(?:^|[,\n])\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$-]*))\s*:/g)) {
328
+ keys.push(keyMatch[1] || keyMatch[2] || keyMatch[3]);
329
+ }
330
+ return [...new Set(keys)].sort();
331
+ }
332
+
333
+ async function listProjectSkillNames(root) {
334
+ const dir = path.join(root, '.agents', 'skills');
335
+ if (!await exists(dir)) return [];
336
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
337
+ const names = [];
338
+ for (const entry of entries) {
339
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
340
+ const skillFile = path.join(dir, entry.name, 'SKILL.md');
341
+ const text = await readText(skillFile, '').catch(() => '');
342
+ const name = text.match(/^name:\s*"?([^"\n]+)"?/m)?.[1]?.trim() || entry.name;
343
+ if (name) names.push(name);
344
+ }
345
+ return [...new Set(names)].sort();
346
+ }
347
+
348
+ async function collectDocRouteMentions(root) {
349
+ const docs = ['README.md', '.codex/SNEAKOSCOPE.md', 'AGENTS.md', '.agents/skills/.sks-generated.json'];
350
+ const mentions = new Set();
351
+ for (const file of docs) {
352
+ const text = file.endsWith('.json')
353
+ ? JSON.stringify(await readJson(path.join(root, file), null).catch(() => null) || {})
354
+ : await readText(path.join(root, file), '').catch(() => '');
355
+ for (const match of text.matchAll(/\$[A-Za-z][A-Za-z0-9_-]*/g)) mentions.add(canonicalDollar(match[0]));
356
+ }
357
+ return [...mentions].sort();
358
+ }
359
+
360
+ function flatMapSourceRefs(features, key) {
361
+ return features.flatMap((feature) => feature.source_refs?.[key] || []);
362
+ }
363
+
364
+ function duplicateValues(values) {
365
+ const seen = new Set();
366
+ const dupes = new Set();
367
+ for (const value of values) {
368
+ if (seen.has(value)) dupes.add(value);
369
+ seen.add(value);
370
+ }
371
+ return [...dupes].sort();
372
+ }
373
+
374
+ function markdownTableCell(value) {
375
+ return String(value || '').replace(/\|/g, '\\|').replace(/\n/g, '<br>');
376
+ }
377
+
378
+ function skillCoveredByRoute(skillName) {
379
+ const normalized = String(skillName || '').toLowerCase();
380
+ return DOLLAR_COMMAND_ALIASES.some((entry) => entry.app_skill.replace(/^\$/, '').toLowerCase() === normalized);
381
+ }
382
+
383
+ function isExternalPromptCommandMention(mention) {
384
+ return ['$IMAGEGEN'].includes(String(mention || '').toUpperCase());
385
+ }
386
+
387
+ function canonicalDollar(value) {
388
+ const raw = String(value || '').trim();
389
+ const hit = DOLLAR_COMMANDS.find((entry) => entry.command.toLowerCase() === raw.toLowerCase());
390
+ if (hit) return hit.command;
391
+ const aliasHit = DOLLAR_COMMAND_ALIASES.find((entry) => entry.app_skill.toLowerCase() === raw.toLowerCase());
392
+ return aliasHit ? aliasHit.app_skill : raw;
393
+ }
394
+
395
+ function normalizeDollar(value) {
396
+ return String(value || '').trim().toLowerCase();
397
+ }
398
+
399
+ function slug(value) {
400
+ return String(value || '').replace(/^\$/, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
401
+ }
402
+
403
+ function commandCategory(name) {
404
+ if (['team', 'pipeline', 'goal', 'hproof', 'proof-field', 'validate-artifacts'].includes(name)) return 'proof-route';
405
+ if (['qa-loop', 'research', 'recallpulse', 'skill-dream', 'eval', 'perf'].includes(name)) return 'loop';
406
+ if (['codex-app', 'codex-lb', 'auth', 'hooks', 'context7', 'openclaw'].includes(name)) return 'integration';
407
+ if (['db', 'guard', 'conflicts', 'harness', 'versioning'].includes(name)) return 'safety';
408
+ if (['wiki', 'gx', 'image-ux-review', 'ppt'].includes(name)) return 'visual-memory';
409
+ if (['setup', 'bootstrap', 'doctor', 'deps', 'init', 'postinstall', 'fix-path'].includes(name)) return 'install';
410
+ return 'core-cli';
411
+ }
412
+
413
+ function commandMaturity(name) {
414
+ if (['help', 'version', 'commands', 'usage', 'root', 'quickstart', 'setup', 'doctor', 'selftest', 'update-check'].includes(name)) return 'stable';
415
+ if (['codex-app', 'codex-lb', 'hooks', 'features', 'all-features', 'wiki', 'team', 'pipeline', 'goal', 'db', 'guard'].includes(name)) return 'beta';
416
+ return 'labs';
417
+ }
418
+
419
+ function routeMaturity(command) {
420
+ if (['$Answer', '$DFix', '$SKS', '$Wiki', '$Help'].includes(command)) return 'stable';
421
+ if (['$Team', '$Goal', '$DB', '$Computer-Use', '$CU', '$QA-LOOP', '$MAD-SKS'].includes(command)) return 'beta';
422
+ return 'labs';
423
+ }
424
+
425
+ function voxelContract(category) {
426
+ if (category === 'visual-memory') return 'visual/image anchors required';
427
+ if (category === 'safety') return 'policy voxel required';
428
+ if (category === 'loop' || category === 'proof-route') return 'context/source/test anchors required';
429
+ return 'context anchor when evidence is written';
430
+ }
431
+
432
+ function proofContract(category) {
433
+ if (['proof-route', 'loop', 'safety', 'visual-memory'].includes(category)) return 'required';
434
+ return 'required for route/release use';
435
+ }
436
+
437
+ function knownGapsForCommand(name) {
438
+ if (['features', 'all-features'].includes(name)) return ['feature fixtures remain progressive'];
439
+ if (['codex-app', 'hooks'].includes(name)) return ['mobile/event payload details remain unknown'];
440
+ return [];
441
+ }
442
+
443
+ function routeVoxelContract(command) {
444
+ if (['$Image-UX-Review', '$UX-Review', '$PPT', '$From-Chat-IMG', '$GX'].includes(command)) return 'image/source/bbox voxel required';
445
+ if (command === '$DB' || command === '$MAD-SKS') return 'DB policy voxel required';
446
+ return 'TriWiki anchors required';
447
+ }
448
+
449
+ function routeKnownGaps(command) {
450
+ if (['$Image-UX-Review', '$UX-Review', '$PPT'].includes(command)) return ['live imagegen/CU evidence required'];
451
+ if (command === '$MAD-SKS') return ['permission closed by owning gate'];
452
+ return [];
453
+ }
454
+
455
+ function checkRow(id, ok, blockers = []) {
456
+ return { id, ok: Boolean(ok), blockers: ok ? [] : blockers };
457
+ }
458
+
459
+ function missingFeatureField(registry, field) {
460
+ return (registry.features || []).filter((feature) => !feature[field]).map((feature) => feature.id);
461
+ }
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.9.7';
8
+ export const PACKAGE_VERSION = '0.9.8';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -28,7 +28,7 @@ export const FROM_CHAT_IMG_CHECKLIST_ARTIFACT = 'from-chat-img-checklist.md';
28
28
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT = 'from-chat-img-temp-triwiki.json';
29
29
  export const FROM_CHAT_IMG_QA_LOOP_ARTIFACT = 'from-chat-img-qa-loop.json';
30
30
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS = 5;
31
- export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|tmux|auto-review|team|qa-loop|ppt|image-ux-review|goal|research|db|codex-app|openclaw|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|harness|hproof|gx|wiki|code-structure|proof-field|skill-dream';
31
+ export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|tmux|auto-review|team|qa-loop|ppt|image-ux-review|goal|research|db|codex-app|hooks|features|all-features|openclaw|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|harness|hproof|gx|wiki|code-structure|proof-field|skill-dream';
32
32
  export const CODEX_COMPUTER_USE_EVIDENCE_SOURCE = 'codex_computer_use';
33
33
  export const CODEX_IMAGEGEN_EVIDENCE_SOURCE = 'codex_app_imagegen_gpt_image_2';
34
34
  export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/codex/app/features#image-generation';
@@ -531,7 +531,8 @@ export const COMMAND_CATALOG = [
531
531
  { name: 'bootstrap', usage: 'sks bootstrap [--install-scope global|project] [--local-only] [--json]', description: 'Initialize the current project, install SKS Codex App files/skills, check Context7/Codex App/tmux, and print ready true/false.' },
532
532
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
533
533
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser tooling, Computer Use, tmux, and Homebrew on macOS.' },
534
- { name: 'codex-app', usage: 'sks codex-app [check|open|remote-control]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files, examples, and Codex CLI 0.130.0+ remote-control availability.' },
534
+ { name: 'codex-app', usage: 'sks codex-app [check|open|pat status|remote-control]', description: 'Check Codex App install, PAT-safe status, first-party MCP/plugin readiness, and Codex CLI 0.130.0+ remote-control availability.' },
535
+ { name: 'hooks', usage: 'sks hooks explain [--json]', description: 'Explain Codex hook events, config locations, handler support, and SKS hook policies without storing raw payloads.' },
535
536
  { name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup --host <domain> --api-key <key>', description: 'Configure, health-check, or repair codex-lb provider auth by writing ~/.codex/config.toml, restoring CODEX_LB_API_KEY env auth from stored or legacy login-cache state, and preserving the shared Codex login cache unless explicitly requested.' },
536
537
  { name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb provider auth status, health, repair, and setup commands.' },
537
538
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
@@ -549,6 +550,8 @@ export const COMMAND_CATALOG = [
549
550
  { name: 'guard', usage: 'sks guard check [--json]', description: 'Check SKS harness self-protection lock, fingerprints, and source-repo exception state.' },
550
551
  { name: 'conflicts', usage: 'sks conflicts check|prompt [--json]', description: 'Detect other Codex harnesses such as OMX/DCodex and print the GPT-5.5 high cleanup prompt.' },
551
552
  { name: 'versioning', usage: 'sks versioning status|bump|disable [--json]', description: 'Manage explicit project version syncs; SKS does not install Git pre-commit hooks.' },
553
+ { name: 'features', usage: 'sks features list|check|inventory [--json] [--write-docs]', description: 'Build and validate the feature registry that maps CLI commands, hidden handlers, dollar routes, app skill aliases, and skills.' },
554
+ { name: 'all-features', usage: 'sks all-features selftest --mock [--json]', description: 'Run the mock all-features contract selftest for feature registry, proof, Voxel TriWiki, and failure-contract coverage.' },
552
555
  { name: 'aliases', usage: 'sks aliases', description: 'Show command aliases and npm binary names.' },
553
556
  { name: 'setup', usage: 'sks setup [--bootstrap] [--install-scope global|project] [--local-only] [--force] [--json]', description: 'Initialize SKS state, Codex App files, hooks, skills, and rules.' },
554
557
  { name: 'fix-path', usage: 'sks fix-path [--install-scope global|project] [--json]', description: 'Refresh hook commands with the resolved SKS binary path.' },