sneakoscope 0.9.6 → 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 +3 -1
- package/package.json +4 -2
- package/src/cli/feature-commands.mjs +131 -0
- package/src/cli/install-helpers.mjs +38 -3
- package/src/cli/main.mjs +50 -9
- package/src/core/codex-app.mjs +32 -0
- package/src/core/feature-registry.mjs +461 -0
- package/src/core/fsx.mjs +1 -1
- package/src/core/routes.mjs +5 -2
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.
|
|
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
|
-
"
|
|
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
|
+
}
|
|
@@ -95,6 +95,17 @@ async function reportPostinstallCodexLbAuth() {
|
|
|
95
95
|
} else if (reconcile?.status === 'failed') {
|
|
96
96
|
console.log(`codex-lb auth: ChatGPT OAuth reconciliation could not complete (${reconcile.reason || 'unknown'}${reconcile.error ? `: ${reconcile.error}` : ''}). Run \`sks codex-lb repair\` to retry.`);
|
|
97
97
|
}
|
|
98
|
+
if (codexLbAuth.base_url && codexLbAuth.codex_lb?.env_key_configured && canAskYesNo() && process.env.SKS_SKIP_CODEX_LB_KEY_PROMPT !== '1') {
|
|
99
|
+
const changeKey = (await askPostinstallQuestion('codex-lb key changed? Update now? [y/N] ')).trim();
|
|
100
|
+
if (/^(y|yes|예|네|응)$/i.test(changeKey)) {
|
|
101
|
+
const newKey = (await askPostinstallQuestion('New codex-lb API key (sk-clb-...): ')).trim();
|
|
102
|
+
if (newKey) {
|
|
103
|
+
const result = await configureCodexLb({ host: codexLbAuth.base_url, apiKey: newKey });
|
|
104
|
+
if (result.ok) console.log(`codex-lb key updated: ${result.base_url}`);
|
|
105
|
+
else console.log(`codex-lb key update failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
98
109
|
return codexLbAuth;
|
|
99
110
|
}
|
|
100
111
|
|
|
@@ -457,6 +468,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
457
468
|
codex_lb: status
|
|
458
469
|
};
|
|
459
470
|
}
|
|
471
|
+
await migrateCodexAuthKeyFormat({ home: opts.home });
|
|
460
472
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
461
473
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
462
474
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
@@ -482,6 +494,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
482
494
|
if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
|
|
483
495
|
const status = await codexLbStatus(opts);
|
|
484
496
|
if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
|
|
497
|
+
await migrateCodexAuthKeyFormat({ home: opts.home });
|
|
485
498
|
if (status.ok && (!status.selected || !status.provider_requires_openai_auth)) return repairCodexLbAuth(opts);
|
|
486
499
|
if (!status.ok) {
|
|
487
500
|
if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
|
|
@@ -552,6 +565,28 @@ function parseCodexAuthApiKey(text = '') {
|
|
|
552
565
|
}
|
|
553
566
|
}
|
|
554
567
|
|
|
568
|
+
// Migrate auth.json from legacy {"auth_mode":"apikey","key":"..."} to the codex 0.130.0+
|
|
569
|
+
// format {"auth_mode":"apikey","OPENAI_API_KEY":"..."}. Safe: preserves key value, only renames field.
|
|
570
|
+
async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
571
|
+
const home = opts.home || process.env.HOME || os.homedir();
|
|
572
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
573
|
+
const text = await readText(authPath, '');
|
|
574
|
+
if (!text.trim()) return { status: 'skipped', reason: 'empty' };
|
|
575
|
+
try {
|
|
576
|
+
const parsed = JSON.parse(text);
|
|
577
|
+
if (parsed?.auth_mode !== 'apikey') return { status: 'skipped', reason: 'not_apikey' };
|
|
578
|
+
if (parsed.OPENAI_API_KEY) return { status: 'skipped', reason: 'already_migrated' };
|
|
579
|
+
const legacyKey = parsed.key || parsed.api_key || parsed.apiKey || parsed.openai_api_key;
|
|
580
|
+
if (!legacyKey) return { status: 'skipped', reason: 'no_key_found' };
|
|
581
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: legacyKey })}\n`;
|
|
582
|
+
await writeTextAtomic(authPath, replacement);
|
|
583
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
584
|
+
return { status: 'migrated', auth_path: authPath };
|
|
585
|
+
} catch {
|
|
586
|
+
return { status: 'skipped', reason: 'parse_error' };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
555
590
|
// When codex-lb is selected with env_key auth AND auth.json also carries a real ChatGPT OAuth
|
|
556
591
|
// token blob, Codex CLI/App can pick the OAuth bearer over the env_key bearer and fail against
|
|
557
592
|
// the load balancer. We back the OAuth blob up to ~/.codex/auth.chatgpt-backup.json and replace
|
|
@@ -597,7 +632,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
597
632
|
};
|
|
598
633
|
}
|
|
599
634
|
try {
|
|
600
|
-
const replacement = `${JSON.stringify({ auth_mode: 'apikey',
|
|
635
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`;
|
|
601
636
|
await writeTextAtomic(authPath, replacement);
|
|
602
637
|
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
603
638
|
} catch (err) {
|
|
@@ -1567,7 +1602,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1567
1602
|
// NOTE: printf format uses literal double-quotes inside single-quoted shell strings so the
|
|
1568
1603
|
// fake login writes proper JSON in both bash and dash (where `\"` is a non-standard printf
|
|
1569
1604
|
// escape that dash emits literally and bash collapses to `"`).
|
|
1570
|
-
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\"auth_mode\":\"apikey\",\"
|
|
1605
|
+
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\"auth_mode\":\"apikey\",\"OPENAI_API_KEY\":\"%s\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
|
|
1571
1606
|
await fsp.chmod(codexLbFakeCodex, 0o755);
|
|
1572
1607
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
|
|
1573
1608
|
const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}`, SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' };
|
|
@@ -1710,7 +1745,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1710
1745
|
// the provider stays selected and whether the backup file is removed after restore.
|
|
1711
1746
|
const codexLbReleaseConfig = 'model_provider = "codex-lb"\n\n[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = true\n';
|
|
1712
1747
|
const codexLbReleaseEnv = "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n";
|
|
1713
|
-
const codexLbReleaseApikeyAuth = '{"auth_mode":"apikey","
|
|
1748
|
+
const codexLbReleaseApikeyAuth = '{"auth_mode":"apikey","OPENAI_API_KEY":"sk-test"}\n';
|
|
1714
1749
|
const codexLbReleaseOauthBackup = `${oauthAuthJson}\n`;
|
|
1715
1750
|
// Happy path: deselect model_provider and preserve backup file.
|
|
1716
1751
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
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>
|
|
@@ -1248,13 +1254,20 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1248
1254
|
return;
|
|
1249
1255
|
}
|
|
1250
1256
|
if (sub === 'setup' || sub === 'reconfigure') {
|
|
1251
|
-
|
|
1252
|
-
|
|
1257
|
+
let host = readOption(args, '--host', readOption(args, '--domain', null));
|
|
1258
|
+
let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
|
|
1253
1259
|
if (!host || !apiKey) {
|
|
1254
1260
|
if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1261
|
+
if (!canAskYesNo()) {
|
|
1262
|
+
console.error('Usage: sks codex-lb setup|reconfigure --host <domain> --api-key <key>');
|
|
1263
|
+
process.exitCode = 1;
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
console.log('codex-lb setup — configure your Codex load balancer connection.\n');
|
|
1267
|
+
if (!host) host = (await askPostinstallQuestion('Your codex-lb domain (e.g. https://codex.example.com/backend-api/codex): ')).trim();
|
|
1268
|
+
if (!host) { console.error('Setup cancelled: no domain provided.'); process.exitCode = 1; return; }
|
|
1269
|
+
if (!apiKey) apiKey = (await askPostinstallQuestion('Your codex-lb API key (sk-clb-...): ')).trim();
|
|
1270
|
+
if (!apiKey) { console.error('Setup cancelled: no API key provided.'); process.exitCode = 1; return; }
|
|
1258
1271
|
}
|
|
1259
1272
|
const result = await configureCodexLb({ host, apiKey });
|
|
1260
1273
|
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
@@ -1492,6 +1505,23 @@ async function autoReviewCommand(sub = 'status', args = []) {
|
|
|
1492
1505
|
async function codexAppHelp(args = []) {
|
|
1493
1506
|
const action = args[0] || 'help';
|
|
1494
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
|
+
}
|
|
1495
1525
|
if (action === 'check' || action === 'status') {
|
|
1496
1526
|
const status = await codexAppIntegrationStatus();
|
|
1497
1527
|
const skills = await codexAppSkillReadiness();
|
|
@@ -1519,7 +1549,7 @@ async function codexAppHelp(args = []) {
|
|
|
1519
1549
|
'Codex App', '',
|
|
1520
1550
|
formatCodexAppStatus(status), '',
|
|
1521
1551
|
`Skills: project=${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} global=${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`}`, '',
|
|
1522
|
-
'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', '',
|
|
1523
1553
|
'Generated files:', ' .codex/config.toml', ' .codex/hooks.json', ' .agents/skills/', ' .codex/agents/', ' .codex/SNEAKOSCOPE.md', ' AGENTS.md', '',
|
|
1524
1554
|
'Git ignore:', ' default setup writes .gitignore entries for .sneakoscope/, .codex/, .agents/, AGENTS.md', ' --local-only writes those patterns to .git/info/exclude instead', '',
|
|
1525
1555
|
'Prompt routes:', formatDollarCommandsCompact(' ')
|
|
@@ -2710,7 +2740,18 @@ async function selftest() {
|
|
|
2710
2740
|
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$From-Chat-IMG')) throw new Error('selftest: dollar-commands missing From-Chat-IMG guidance');
|
|
2711
2741
|
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$MAD-SKS')) throw new Error('selftest: dollar-commands missing MAD-SKS scoped override guidance');
|
|
2712
2742
|
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$Image-UX-Review')) throw new Error('selftest: dollar-commands missing Image UX Review guidance');
|
|
2713
|
-
|
|
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');
|
|
2714
2755
|
const openClawTmp = tmpdir();
|
|
2715
2756
|
const openClawResult = await installOpenClawSkill({ targetDir: path.join(openClawTmp, 'skills', OPENCLAW_SKILL_NAME) });
|
|
2716
2757
|
if (!openClawResult.ok) throw new Error(`selftest: OpenClaw skill install blocked: ${openClawResult.reason}`);
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
package/src/core/routes.mjs
CHANGED
|
@@ -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
|
|
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.' },
|