sneakoscope 7.3.0 → 7.5.0

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.
Files changed (151) hide show
  1. package/README.md +117 -5
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +2 -1
  6. package/dist/cli/install-helpers-codex-lb-selftest.js +2 -1
  7. package/dist/cli/install-helpers-install-support.js +6 -16
  8. package/dist/cli/install-helpers.js +59 -50
  9. package/dist/cli/router.js +9 -1
  10. package/dist/config/skills-manifest.json +50 -71
  11. package/dist/core/codex-app.js +5 -5
  12. package/dist/core/codex-native/core-skill-manifest.js +9 -6
  13. package/dist/core/commands/image-ux-review-command.js +243 -50
  14. package/dist/core/commands/naruto-command.js +245 -8
  15. package/dist/core/commands/ppt-command.js +2 -2
  16. package/dist/core/commands/telegram-command.js +123 -10
  17. package/dist/core/commands/triwiki-command.js +11 -1
  18. package/dist/core/commands/triwiki-graph-command.js +160 -0
  19. package/dist/core/commands/wiki-command.js +100 -20
  20. package/dist/core/errors/message.js +3 -0
  21. package/dist/core/feature-registry.js +2 -2
  22. package/dist/core/fsx.js +7 -3
  23. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  24. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  25. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  26. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  27. package/dist/core/hooks-runtime.js +28 -86
  28. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  29. package/dist/core/image-ux-review/imagegen-adapter.js +54 -18
  30. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  31. package/dist/core/image-ux-review.js +182 -7
  32. package/dist/core/imagegen/codex-lb-imagegen-target.js +22 -7
  33. package/dist/core/imagegen/imagegen-capability.js +11 -4
  34. package/dist/core/imagegen/require-imagegen.js +29 -5
  35. package/dist/core/init/skills.js +22 -10
  36. package/dist/core/install/installed-package-smoke.js +157 -4
  37. package/dist/core/managed-path-safety.js +8 -10
  38. package/dist/core/mcp-config/backup.js +2 -4
  39. package/dist/core/mcp-config/config-reader.js +2 -4
  40. package/dist/core/mcp-config/scope.js +3 -5
  41. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  42. package/dist/core/naruto/context-graph-advisor.js +0 -0
  43. package/dist/core/pipeline-internals/runtime-core.js +44 -0
  44. package/dist/core/policy/latest-version-guidance.js +113 -0
  45. package/dist/core/ppt.js +8 -8
  46. package/dist/core/release/gate-manifest.js +32 -1
  47. package/dist/core/release/npm-stage-contract.js +21 -0
  48. package/dist/core/release/package-size-budget.js +2 -2
  49. package/dist/core/release/release-gate-contract.js +5 -0
  50. package/dist/core/release/stage-publish.js +27 -0
  51. package/dist/core/responses-stream.js +65 -19
  52. package/dist/core/routes/design-policy.js +7 -2
  53. package/dist/core/routes/dollar-manifest-lite.js +0 -4
  54. package/dist/core/routes/evidence.js +1 -1
  55. package/dist/core/routes.js +12 -8
  56. package/dist/core/safety/command-contract/contracts.js +2 -0
  57. package/dist/core/safety/command-contract/types.js +1 -1
  58. package/dist/core/search/context-graph-seeds.js +205 -0
  59. package/dist/core/search/context.js +0 -0
  60. package/dist/core/search/provider.js +2 -2
  61. package/dist/core/subagents/naruto-help-contract.js +2 -1
  62. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  63. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  64. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  65. package/dist/core/subagents/official-subagent-runner.js +26 -8
  66. package/dist/core/subagents/triwiki-attention.js +135 -123
  67. package/dist/core/telegram/bot-api.js +59 -4
  68. package/dist/core/telegram/config.js +3 -2
  69. package/dist/core/telegram/hub.js +3 -2
  70. package/dist/core/telegram/owner-lock.js +4 -6
  71. package/dist/core/telegram/setup.js +223 -73
  72. package/dist/core/triwiki/code-pack.js +12 -120
  73. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  74. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  75. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  76. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  77. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  78. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  79. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  80. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  81. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  82. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  83. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  84. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  85. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  86. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  87. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  88. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  89. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  90. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  91. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  92. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  93. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  94. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  95. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  96. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  97. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  98. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  99. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  100. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  101. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  102. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  103. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  104. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  105. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  106. package/dist/core/triwiki/context-graph/ids.js +0 -0
  107. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  108. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  109. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  110. package/dist/core/triwiki/context-graph/paths.js +116 -0
  111. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  112. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  113. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  114. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  115. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  116. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  117. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  118. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  119. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  120. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  121. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  122. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  123. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  124. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  125. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  126. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  127. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  128. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  129. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  130. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  131. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  132. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  133. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  134. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  135. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  136. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  137. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  138. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  139. package/dist/core/verification/context-graph-affected.js +271 -0
  140. package/dist/core/version.js +1 -1
  141. package/dist/scripts/context-graph-check.js +191 -0
  142. package/dist/scripts/helper-dedup-check.js +0 -1
  143. package/dist/scripts/installed-package-smoke-check.js +4 -1
  144. package/dist/scripts/latest-version-guidance-check.js +74 -0
  145. package/dist/scripts/release-metadata-check.js +20 -3
  146. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  147. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  148. package/package.json +5 -3
  149. package/release-gates.v2.json +174 -0
  150. package/schemas/triwiki/context-graph.schema.json +119 -0
  151. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -30,8 +30,31 @@ export async function runInstalledPackageSmoke(root, options = {}) {
30
30
  const npmCache = path.join(tmp, 'npm-cache');
31
31
  const installPrefix = path.join(tmp, 'prefix');
32
32
  const packDestination = path.join(tmp, 'pack');
33
- await Promise.all([home, codexHome, npmCache, installPrefix, packDestination, path.join(home, '.tmp')]
33
+ const globalRoot = path.join(tmp, 'global-sks');
34
+ const installCwd = path.join(tmp, 'install-cwd');
35
+ const runtimeTmp = path.join(tmp, 'runtime-tmp');
36
+ const stubBin = path.join(tmp, 'bin');
37
+ const launchctlLog = path.join(tmp, 'launchctl-calls.log');
38
+ const launchctlStub = path.join(stubBin, process.platform === 'win32' ? 'launchctl.cmd' : 'launchctl');
39
+ const npmUserConfig = path.join(tmp, 'npmrc');
40
+ const npmGlobalConfig = path.join(tmp, 'npm-globalrc');
41
+ await Promise.all([home, codexHome, npmCache, installPrefix, packDestination, globalRoot, installCwd, runtimeTmp, stubBin, path.join(home, '.tmp')]
34
42
  .map((dir) => fs.mkdir(dir, { recursive: true })));
43
+ await Promise.all([
44
+ fs.writeFile(launchctlLog, ''),
45
+ fs.writeFile(launchctlStub, launchctlStubSource()),
46
+ fs.writeFile(npmUserConfig, ''),
47
+ fs.writeFile(npmGlobalConfig, '')
48
+ ]);
49
+ await fs.chmod(launchctlStub, 0o700).catch(() => { });
50
+ const externalRoots = [
51
+ { label: 'home', root: home },
52
+ { label: 'codex-home', root: codexHome },
53
+ { label: 'global-sks', root: globalRoot },
54
+ { label: 'install-cwd', root: installCwd },
55
+ { label: 'runtime-tmp', root: runtimeTmp }
56
+ ];
57
+ const externalBeforeInstall = await snapshotTrees(externalRoots);
35
58
  const blockers = [];
36
59
  const commands = [];
37
60
  let tarball = null;
@@ -40,6 +63,14 @@ export async function runInstalledPackageSmoke(root, options = {}) {
40
63
  let packedVersion = null;
41
64
  let expectedSha256 = normalizeSha256(options.expectedSha256);
42
65
  let receiptPath = null;
66
+ let postinstallDefault = {
67
+ scripts_enabled: true,
68
+ external_snapshot_match: false,
69
+ external_findings: [],
70
+ launchctl_calls: [],
71
+ package_local_stamp_present: false,
72
+ opt_in_guidance_present: false
73
+ };
43
74
  if (options.expectedSha256 && !expectedSha256)
44
75
  blockers.push('expected_sha256_invalid');
45
76
  if (options.receipt && !options.tarball)
@@ -85,12 +116,66 @@ export async function runInstalledPackageSmoke(root, options = {}) {
85
116
  }
86
117
  }
87
118
  if (tarball && blockers.length === 0) {
88
- const install = await runJsonCommand(tmp, [
89
- 'npm', 'install', '--global', '--prefix', installPrefix, '--ignore-scripts', '--no-audit', '--no-fund', tarball
90
- ], { home, codexHome, npmCache });
119
+ const install = await runJsonCommand(installCwd, [
120
+ 'npm', 'install', '--global', '--prefix', installPrefix, '--foreground-scripts', '--no-audit', '--no-fund', tarball
121
+ ], {
122
+ home,
123
+ codexHome,
124
+ npmCache,
125
+ env: {
126
+ HOME: home,
127
+ USERPROFILE: home,
128
+ CODEX_HOME: codexHome,
129
+ XDG_CONFIG_HOME: path.join(home, '.config'),
130
+ XDG_CACHE_HOME: path.join(home, '.cache'),
131
+ XDG_DATA_HOME: path.join(home, '.local', 'share'),
132
+ INIT_CWD: installCwd,
133
+ TMPDIR: runtimeTmp,
134
+ PATH: `${stubBin}${path.delimiter}${String(process.env.PATH || '')}`,
135
+ NODE_DISABLE_COMPILE_CACHE: '1',
136
+ NODE_COMPILE_CACHE: '',
137
+ SKS_GLOBAL_ROOT: globalRoot,
138
+ SKS_MENUBAR_LAUNCHCTL: launchctlStub,
139
+ SKS_INSTALLED_SMOKE_LAUNCHCTL_LOG: launchctlLog,
140
+ SKS_POSTINSTALL_BOOTSTRAP: '',
141
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '',
142
+ SKS_POSTINSTALL_PROMPT: '',
143
+ npm_config_userconfig: npmUserConfig,
144
+ NPM_CONFIG_USERCONFIG: npmUserConfig,
145
+ npm_config_globalconfig: npmGlobalConfig,
146
+ NPM_CONFIG_GLOBALCONFIG: npmGlobalConfig,
147
+ npm_config_ignore_scripts: 'false',
148
+ NPM_CONFIG_IGNORE_SCRIPTS: 'false',
149
+ NO_UPDATE_NOTIFIER: '1'
150
+ }
151
+ });
91
152
  commands.push(summarizeInstalledSmokeCommand(install.command, 'install'));
92
153
  if (install.exit_code !== 0)
93
154
  blockers.push('npm_install_tarball_failed');
155
+ const externalAfterInstall = await snapshotTrees(externalRoots);
156
+ const externalFindings = diffTreeSnapshots(externalBeforeInstall, externalAfterInstall);
157
+ const launchctlCalls = await nonEmptyLines(launchctlLog);
158
+ const packageLocalStamp = path.join(installedPackageRoot(installPrefix), 'dist', '.sks-build-stamp.json');
159
+ const packageLocalStampPresent = await regularFileExists(packageLocalStamp);
160
+ const installOutput = `${install.stdout}\n${install.command.stderr_tail}`;
161
+ const optInGuidancePresent = /Automatic bootstrap was not run/.test(installOutput)
162
+ && /SKS_POSTINSTALL_BOOTSTRAP=1/.test(installOutput);
163
+ postinstallDefault = {
164
+ scripts_enabled: true,
165
+ external_snapshot_match: externalFindings.length === 0,
166
+ external_findings: externalFindings,
167
+ launchctl_calls: launchctlCalls,
168
+ package_local_stamp_present: packageLocalStampPresent,
169
+ opt_in_guidance_present: optInGuidancePresent
170
+ };
171
+ if (externalFindings.length)
172
+ blockers.push(...externalFindings.map((finding) => `postinstall_default_external_mutation:${finding}`));
173
+ if (launchctlCalls.length)
174
+ blockers.push('postinstall_default_launchctl_called');
175
+ if (!packageLocalStampPresent)
176
+ blockers.push('postinstall_package_local_stamp_missing');
177
+ if (!optInGuidancePresent)
178
+ blockers.push('postinstall_default_opt_in_guidance_missing');
94
179
  }
95
180
  const bin = process.platform === 'win32'
96
181
  ? path.join(installPrefix, 'sks.cmd')
@@ -187,6 +272,7 @@ export async function runInstalledPackageSmoke(root, options = {}) {
187
272
  required_dollar_commands: [...INSTALLED_REQUIRED_DOLLAR_COMMANDS],
188
273
  closure
189
274
  },
275
+ postinstall_default: postinstallDefault,
190
276
  forbidden_findings: forbiddenFindings,
191
277
  blockers: [...new Set(blockers)]
192
278
  };
@@ -253,6 +339,7 @@ async function runJsonCommand(cwd, argv, opts) {
253
339
  env.npm_config_cache = opts.npmCache;
254
340
  env.NPM_CONFIG_CACHE = opts.npmCache;
255
341
  }
342
+ Object.assign(env, opts.env || {});
256
343
  const res = await runProcess(command || 'node', args, {
257
344
  cwd,
258
345
  timeoutMs: 120_000,
@@ -275,6 +362,72 @@ async function runJsonCommand(cwd, argv, opts) {
275
362
  }
276
363
  };
277
364
  }
365
+ async function snapshotTrees(roots) {
366
+ const snapshot = new Map();
367
+ for (const entry of roots)
368
+ await walkSnapshot(entry.root, entry.root, entry.label, snapshot);
369
+ return snapshot;
370
+ }
371
+ async function walkSnapshot(root, current, label, snapshot) {
372
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
373
+ entries.sort((a, b) => a.name.localeCompare(b.name));
374
+ for (const entry of entries) {
375
+ const absolute = path.join(current, entry.name);
376
+ const relative = path.relative(root, absolute) || '.';
377
+ const key = `${label}:${relative}`;
378
+ const stat = await fs.lstat(absolute);
379
+ const mode = (stat.mode & 0o777).toString(8).padStart(3, '0');
380
+ if (entry.isDirectory()) {
381
+ snapshot.set(key, `directory:${mode}`);
382
+ await walkSnapshot(root, absolute, label, snapshot);
383
+ }
384
+ else if (entry.isFile()) {
385
+ const bytes = await fs.readFile(absolute);
386
+ snapshot.set(key, `file:${mode}:${bytes.length}:${crypto.createHash('sha256').update(bytes).digest('hex')}`);
387
+ }
388
+ else if (entry.isSymbolicLink()) {
389
+ snapshot.set(key, `symlink:${mode}:${await fs.readlink(absolute)}`);
390
+ }
391
+ else {
392
+ snapshot.set(key, `other:${mode}:${stat.size}`);
393
+ }
394
+ }
395
+ }
396
+ function diffTreeSnapshots(before, after) {
397
+ const findings = [];
398
+ const keys = [...new Set([...before.keys(), ...after.keys()])].sort();
399
+ for (const key of keys) {
400
+ if (!before.has(key))
401
+ findings.push(`added:${key}`);
402
+ else if (!after.has(key))
403
+ findings.push(`removed:${key}`);
404
+ else if (before.get(key) !== after.get(key))
405
+ findings.push(`changed:${key}`);
406
+ }
407
+ return findings;
408
+ }
409
+ async function nonEmptyLines(file) {
410
+ const value = await fs.readFile(file, 'utf8').catch(() => '');
411
+ return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
412
+ }
413
+ async function regularFileExists(file) {
414
+ const stat = await fs.lstat(file).catch(() => null);
415
+ return Boolean(stat?.isFile() && !stat.isSymbolicLink());
416
+ }
417
+ function installedPackageRoot(prefix) {
418
+ return process.platform === 'win32'
419
+ ? path.join(prefix, 'node_modules', 'sneakoscope')
420
+ : path.join(prefix, 'lib', 'node_modules', 'sneakoscope');
421
+ }
422
+ function launchctlStubSource() {
423
+ if (process.platform === 'win32') {
424
+ return '@echo %*>>\"%SKS_INSTALLED_SMOKE_LAUNCHCTL_LOG%\"\r\n@exit /b 0\r\n';
425
+ }
426
+ return `#!/bin/sh
427
+ printf '%s\n' "$*" >> "$SKS_INSTALLED_SMOKE_LAUNCHCTL_LOG"
428
+ exit 0
429
+ `;
430
+ }
278
431
  export function validateInstalledPublicSurface(commandManifest, dollarManifest) {
279
432
  const commands = Array.isArray(commandManifest?.commands)
280
433
  ? commandManifest.commands.map((row) => String(row?.name || '')) : [];
@@ -1,5 +1,6 @@
1
1
  import fsp from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { errorCodeOf } from './errors/message.js';
3
4
  export class ManagedPathSafetyError extends Error {
4
5
  code;
5
6
  target;
@@ -73,7 +74,7 @@ export async function ensureConfinedDirectory(boundary, directory) {
73
74
  await fsp.mkdir(current);
74
75
  }
75
76
  catch (error) {
76
- if (errorCode(error) !== 'EEXIST')
77
+ if (errorCodeOf(error) !== 'EEXIST')
77
78
  throw error;
78
79
  }
79
80
  stat = await lstatOrNull(current);
@@ -111,7 +112,7 @@ export async function walkConfinedEntries(boundary, root) {
111
112
  children = await fsp.readdir(current, { withFileTypes: true, encoding: 'utf8' });
112
113
  }
113
114
  catch (error) {
114
- errors.push(`${errorCode(error) || 'managed_path_readdir_failed'}:${current}`);
115
+ errors.push(`${errorCodeOf(error) || 'managed_path_readdir_failed'}:${current}`);
115
116
  return;
116
117
  }
117
118
  children.sort((left, right) => left.name.localeCompare(right.name));
@@ -222,7 +223,7 @@ export async function removeEmptyTreeVerified(boundary, root) {
222
223
  children = await fsp.readdir(current, { withFileTypes: true, encoding: 'utf8' });
223
224
  }
224
225
  catch (error) {
225
- errors.push(`${errorCode(error) || 'managed_path_readdir_failed'}:${current}`);
226
+ errors.push(`${errorCodeOf(error) || 'managed_path_readdir_failed'}:${current}`);
226
227
  remainingPaths.push(current);
227
228
  return;
228
229
  }
@@ -239,7 +240,7 @@ export async function removeEmptyTreeVerified(boundary, root) {
239
240
  after = await fsp.readdir(current);
240
241
  }
241
242
  catch (error) {
242
- errors.push(`${errorCode(error) || 'managed_path_readdir_failed'}:${current}`);
243
+ errors.push(`${errorCodeOf(error) || 'managed_path_readdir_failed'}:${current}`);
243
244
  remainingPaths.push(current);
244
245
  return;
245
246
  }
@@ -251,7 +252,7 @@ export async function removeEmptyTreeVerified(boundary, root) {
251
252
  await fsp.rmdir(current);
252
253
  }
253
254
  catch (error) {
254
- errors.push(`${errorCode(error) || 'managed_path_rmdir_failed'}:${current}`);
255
+ errors.push(`${errorCodeOf(error) || 'managed_path_rmdir_failed'}:${current}`);
255
256
  remainingPaths.push(current);
256
257
  return;
257
258
  }
@@ -270,18 +271,15 @@ export async function lstatConfinedOrNull(boundary, target) {
270
271
  export function publicPathError(error, fallback) {
271
272
  if (error instanceof ManagedPathSafetyError)
272
273
  return `${error.code}:${error.target}`;
273
- return `${errorCode(error) || 'managed_path_operation_failed'}:${fallback}`;
274
+ return `${errorCodeOf(error) || 'managed_path_operation_failed'}:${fallback}`;
274
275
  }
275
276
  async function lstatOrNull(target) {
276
277
  try {
277
278
  return await fsp.lstat(target);
278
279
  }
279
280
  catch (error) {
280
- if (errorCode(error) === 'ENOENT')
281
+ if (errorCodeOf(error) === 'ENOENT')
281
282
  return null;
282
283
  throw error;
283
284
  }
284
285
  }
285
- function errorCode(error) {
286
- return error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
287
- }
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fsp from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { errorCodeOf } from '../errors/message.js';
4
5
  import { nowIso, sha256, writeTextAtomic } from '../fsx.js';
5
6
  import { MCP_BACKUP_SCHEMA } from './types.js';
6
7
  export async function createMcpBackup(ref, before, operation, server) {
@@ -86,7 +87,7 @@ async function assertSafeBackupDirectory(directory, codexHome) {
86
87
  if (relative.startsWith('..') || path.isAbsolute(relative))
87
88
  throw new Error('mcp_backup_path_escape');
88
89
  for (const candidate of [path.join(codexHome, 'backups'), directory]) {
89
- const stat = await fsp.lstat(candidate).catch((error) => errorCode(error) === 'ENOENT' ? null : Promise.reject(error));
90
+ const stat = await fsp.lstat(candidate).catch((error) => errorCodeOf(error) === 'ENOENT' ? null : Promise.reject(error));
90
91
  if (stat?.isSymbolicLink() || (stat && !stat.isDirectory()))
91
92
  throw new Error('mcp_backup_directory_invalid');
92
93
  if (stat) {
@@ -97,6 +98,3 @@ async function assertSafeBackupDirectory(directory, codexHome) {
97
98
  }
98
99
  }
99
100
  }
100
- function errorCode(error) {
101
- return error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
102
- }
@@ -1,5 +1,6 @@
1
1
  import fsp from 'node:fs/promises';
2
2
  import { parseCodexConfigToml } from '../codex/codex-config-toml.js';
3
+ import { errorCodeOf } from '../errors/message.js';
3
4
  import { publicMcpCommand, redactMcpError, redactMcpUrl, sanitizeMcpArgs } from './redaction.js';
4
5
  import { normalizeApprovalMode } from './secret-policy.js';
5
6
  import { MCP_DEFAULT_STARTUP_TIMEOUT_SEC, MCP_DEFAULT_TOOL_TIMEOUT_SEC, MCP_SERVER_CONFIG_SCHEMA } from './types.js';
@@ -16,7 +17,7 @@ export class McpConfigReadError extends Error {
16
17
  }
17
18
  }
18
19
  export async function readMcpConfigDocument(ref) {
19
- const stat = await fsp.lstat(ref.configPath).catch((error) => errorCode(error) === 'ENOENT' ? null : Promise.reject(error));
20
+ const stat = await fsp.lstat(ref.configPath).catch((error) => errorCodeOf(error) === 'ENOENT' ? null : Promise.reject(error));
20
21
  if (!stat)
21
22
  return { ref, exists: false, text: '', parsed: {}, rawServers: {} };
22
23
  if (stat.isSymbolicLink())
@@ -142,6 +143,3 @@ function positiveNumber(value) {
142
143
  const number = Number(value);
143
144
  return Number.isFinite(number) && number > 0 ? number : null;
144
145
  }
145
- function errorCode(error) {
146
- return error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
147
- }
@@ -1,6 +1,7 @@
1
1
  import fsp from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { errorCodeOf } from '../errors/message.js';
4
5
  export class McpScopeError extends Error {
5
6
  code;
6
7
  constructor(code) {
@@ -41,7 +42,7 @@ export function isPathInside(root, candidate) {
41
42
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
42
43
  }
43
44
  async function assertSafeDirectory(directory, boundary, strictNoSymlink) {
44
- const stat = await fsp.lstat(directory).catch((error) => errorCode(error) === 'ENOENT' ? null : Promise.reject(error));
45
+ const stat = await fsp.lstat(directory).catch((error) => errorCodeOf(error) === 'ENOENT' ? null : Promise.reject(error));
45
46
  if (!stat)
46
47
  return;
47
48
  if (strictNoSymlink && stat.isSymbolicLink())
@@ -53,7 +54,7 @@ async function assertSafeDirectory(directory, boundary, strictNoSymlink) {
53
54
  throw new McpScopeError('mcp_codex_home_escape_refused');
54
55
  }
55
56
  async function assertSafeConfig(configPath, boundary) {
56
- const stat = await fsp.lstat(configPath).catch((error) => errorCode(error) === 'ENOENT' ? null : Promise.reject(error));
57
+ const stat = await fsp.lstat(configPath).catch((error) => errorCodeOf(error) === 'ENOENT' ? null : Promise.reject(error));
57
58
  if (!stat)
58
59
  return;
59
60
  if (stat.isSymbolicLink())
@@ -64,6 +65,3 @@ async function assertSafeConfig(configPath, boundary) {
64
65
  if (!resolved || !isPathInside(boundary, resolved))
65
66
  throw new McpScopeError('mcp_config_escape_refused');
66
67
  }
67
- function errorCode(error) {
68
- return error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
69
- }
@@ -0,0 +1,242 @@
1
+ import { incomingEdges, outgoingEdges } from '../triwiki/context-graph/graph-index.js';
2
+ import { contextGraphPathFromId } from '../triwiki/context-graph/ids.js';
3
+ import { isWorkspaceRelativePosixPath } from '../triwiki/context-graph/paths.js';
4
+ import { normalizeNarutoPath } from './naruto-work-item.js';
5
+ export const NARUTO_ADVISOR_CAPS = {
6
+ maxDepth: 2,
7
+ maxNodesPerWalk: 512,
8
+ maxEdgesPerWalk: 4096,
9
+ maxSharedPathsPerPair: 12,
10
+ maxRecommendationsPerSlice: 24,
11
+ maxTaskTokens: 32,
12
+ maxNodesPerLabel: 8
13
+ };
14
+ const DEPENDENCY_EDGE_TYPES = new Set(['imports', 'reexports', 'depends_on', 'references', 'calls', 'routes_to']);
15
+ const IMPACT_EDGE_TYPES = new Set([
16
+ 'imports', 'reexports', 'references', 'calls', 'tests', 'affected_by', 'verified_by', 'gated_by', 'depends_on', 'owns', 'routes_to'
17
+ ]);
18
+ const SEED_EXPANSION_EDGE_TYPES = new Set(['defines', 'contains']);
19
+ function explanationStep(edge) {
20
+ return { edgeId: edge.id, type: edge.type, from: edge.from, to: edge.to, confidence: edge.confidence, path: edge.provenance.path };
21
+ }
22
+ function nodePathOf(node) {
23
+ return node.path ?? contextGraphPathFromId(node.id);
24
+ }
25
+ function isTestNode(node) {
26
+ return node.kind === 'test' || node.metadata.isTest === true;
27
+ }
28
+ function isProtectedGateNode(node) {
29
+ return node.risk === 'protected' || node.metadata.requiredForPublish === true || node.metadata.alwaysOnRelease === true;
30
+ }
31
+ function riskDomainOf(node) {
32
+ if (node.kind === 'risk_domain')
33
+ return node.label || null;
34
+ if (node.kind !== 'gate')
35
+ return null;
36
+ const declared = node.metadata.namespace;
37
+ return (typeof declared === 'string' && declared ? declared : node.label.split(':')[0]) || null;
38
+ }
39
+ export function narutoAdvisorWorkspacePath(value) {
40
+ const normalized = normalizeNarutoPath(value);
41
+ return normalized && isWorkspaceRelativePosixPath(normalized) ? normalized : null;
42
+ }
43
+ export function narutoAdvisorPathList(values) {
44
+ return [...new Set((values ?? []).map(narutoAdvisorWorkspacePath).filter((value) => value !== null))].sort();
45
+ }
46
+ function rootHit(nodeId) {
47
+ return { nodeId, depth: 0, reasonPath: [nodeId], explanation: [] };
48
+ }
49
+ function walkGraph(index, roots, direction, types, maxDepth) {
50
+ const hits = new Map();
51
+ const queue = [];
52
+ for (const root of roots) {
53
+ if (hits.has(root.nodeId) || !index.nodesById.has(root.nodeId))
54
+ continue;
55
+ hits.set(root.nodeId, root);
56
+ queue.push(root);
57
+ }
58
+ let edgesVisited = 0;
59
+ for (let head = 0; head < queue.length; head += 1) {
60
+ const current = queue[head];
61
+ if (!current || current.depth >= maxDepth)
62
+ continue;
63
+ for (const edge of direction === 'out' ? outgoingEdges(index, current.nodeId) : incomingEdges(index, current.nodeId)) {
64
+ edgesVisited += 1;
65
+ if (edgesVisited > NARUTO_ADVISOR_CAPS.maxEdgesPerWalk)
66
+ return { hits, truncated: true };
67
+ if (!types.has(edge.type))
68
+ continue;
69
+ const next = direction === 'out' ? edge.to : edge.from;
70
+ if (hits.has(next) || !index.nodesById.has(next))
71
+ continue;
72
+ if (hits.size >= NARUTO_ADVISOR_CAPS.maxNodesPerWalk)
73
+ return { hits, truncated: true };
74
+ const hit = {
75
+ nodeId: next,
76
+ depth: current.depth + 1,
77
+ reasonPath: [...current.reasonPath, direction === 'out' ? edge.type : `<-${edge.type}`, next],
78
+ explanation: [...current.explanation, explanationStep(edge)]
79
+ };
80
+ hits.set(next, hit);
81
+ queue.push(hit);
82
+ }
83
+ }
84
+ return { hits, truncated: false };
85
+ }
86
+ export function narutoAdvisorProvenance(index, hit) {
87
+ const out = [];
88
+ const seen = new Set();
89
+ for (const step of hit.explanation) {
90
+ const edge = index.edgesById.get(step.edgeId);
91
+ if (!edge)
92
+ continue;
93
+ const key = `${edge.provenance.path}#${edge.provenance.line ?? 0}#${edge.provenance.hash}`;
94
+ if (seen.has(key))
95
+ continue;
96
+ seen.add(key);
97
+ const line = edge.provenance.line;
98
+ out.push({ path: edge.provenance.path, ...(line === undefined ? {} : { line }), hash: edge.provenance.hash });
99
+ }
100
+ if (out.length)
101
+ return out;
102
+ const node = index.nodesById.get(hit.nodeId);
103
+ const nodePath = node ? nodePathOf(node) : null;
104
+ if (node?.contentHash && nodePath)
105
+ out.push({ path: nodePath, hash: node.contentHash });
106
+ return out;
107
+ }
108
+ function resolveSeedNodes(index, paths, labels, unresolved) {
109
+ const out = [];
110
+ const seen = new Set();
111
+ const push = (id) => {
112
+ if (!seen.has(id) && index.nodesById.has(id)) {
113
+ seen.add(id);
114
+ out.push(id);
115
+ }
116
+ };
117
+ const miss = (value) => {
118
+ if (value && !unresolved.includes(value))
119
+ unresolved.push(value);
120
+ };
121
+ for (const raw of paths) {
122
+ const rel = narutoAdvisorWorkspacePath(raw);
123
+ const ids = rel ? index.nodesByPath.get(rel) ?? [] : [];
124
+ if (!ids.length)
125
+ miss(raw);
126
+ for (const id of ids)
127
+ push(id);
128
+ }
129
+ for (const label of labels) {
130
+ const ids = index.nodesByLabel.get(String(label ?? '').toLowerCase()) ?? [];
131
+ if (!ids.length || ids.length > NARUTO_ADVISOR_CAPS.maxNodesPerLabel)
132
+ miss(String(label ?? ''));
133
+ else
134
+ for (const id of ids)
135
+ push(id);
136
+ }
137
+ return out;
138
+ }
139
+ export function narutoAdvisorTaskTokens(text) {
140
+ const tokens = [];
141
+ for (const raw of String(text ?? '').split(/[^\w$./-]+/)) {
142
+ const token = raw.replace(/^[./-]+/, '').replace(/[./-]+$/, '');
143
+ if (token.length < 3 || tokens.includes(token))
144
+ continue;
145
+ if (!/^[A-Za-z_$][\w$]*$/.test(token) && !/^[\w$./-]+\.[A-Za-z]\w*$/.test(token))
146
+ continue;
147
+ tokens.push(token);
148
+ if (tokens.length >= NARUTO_ADVISOR_CAPS.maxTaskTokens)
149
+ break;
150
+ }
151
+ return tokens;
152
+ }
153
+ function expandRoots(index, seeds) {
154
+ const roots = seeds.map(rootHit);
155
+ const seen = new Set(seeds);
156
+ for (const seed of seeds) {
157
+ for (const edge of outgoingEdges(index, seed)) {
158
+ if (!SEED_EXPANSION_EDGE_TYPES.has(edge.type) || seen.has(edge.to))
159
+ continue;
160
+ seen.add(edge.to);
161
+ roots.push({ nodeId: edge.to, depth: 0, reasonPath: [seed, edge.type, edge.to], explanation: [explanationStep(edge)] });
162
+ if (roots.length >= NARUTO_ADVISOR_CAPS.maxNodesPerWalk)
163
+ return roots;
164
+ }
165
+ }
166
+ return roots;
167
+ }
168
+ function closurePaths(index, walk) {
169
+ const out = new Map();
170
+ for (const hit of walk.hits.values()) {
171
+ const node = index.nodesById.get(hit.nodeId);
172
+ if (!node || isTestNode(node))
173
+ continue;
174
+ const nodePath = nodePathOf(node);
175
+ if (nodePath && !out.has(nodePath))
176
+ out.set(nodePath, hit);
177
+ }
178
+ return out;
179
+ }
180
+ function recommendationsFrom(index, sliceId, writeSet, impact) {
181
+ const out = [];
182
+ for (const hit of impact.hits.values()) {
183
+ const node = index.nodesById.get(hit.nodeId);
184
+ if (!node)
185
+ continue;
186
+ const isGate = node.kind === 'gate';
187
+ if (!isGate && !isTestNode(node))
188
+ continue;
189
+ const nodePath = nodePathOf(node);
190
+ if (!nodePath || (!isGate && writeSet.has(nodePath)))
191
+ continue;
192
+ out.push({
193
+ slice_id: sliceId,
194
+ kind: isGate ? 'gate' : 'test',
195
+ id: isGate ? node.label : nodePath,
196
+ path: nodePath,
197
+ protected: isGate ? isProtectedGateNode(node) : false,
198
+ risk_domain: isGate ? riskDomainOf(node) : null,
199
+ reason_path: [...hit.reasonPath],
200
+ explanation: [...hit.explanation],
201
+ provenance: narutoAdvisorProvenance(index, hit)
202
+ });
203
+ if (out.length >= NARUTO_ADVISOR_CAPS.maxRecommendationsPerSlice)
204
+ break;
205
+ }
206
+ return out.sort((a, b) => (a.kind === b.kind ? (a.id < b.id ? -1 : a.id > b.id ? 1 : 0) : a.kind < b.kind ? -1 : 1));
207
+ }
208
+ export function buildNarutoSliceState(index, slice, taskTokens, maxDepth) {
209
+ const unresolved = [];
210
+ const writePaths = narutoAdvisorPathList(slice.writePaths);
211
+ const labels = [...(slice.symbols ?? [])];
212
+ if (!writePaths.length && !labels.length)
213
+ labels.push(...narutoAdvisorTaskTokens(slice.title ?? ''), ...taskTokens);
214
+ const seeds = resolveSeedNodes(index, writePaths, labels, unresolved);
215
+ const walk = walkGraph(index, seeds.map(rootHit), 'out', DEPENDENCY_EDGE_TYPES, maxDepth);
216
+ const impact = walkGraph(index, expandRoots(index, seeds), 'in', IMPACT_EDGE_TYPES, maxDepth);
217
+ const closure = closurePaths(index, walk);
218
+ const writeSet = new Set(writePaths);
219
+ return {
220
+ scope: {
221
+ slice_id: slice.id,
222
+ seed_node_ids: [...seeds].sort(),
223
+ write_paths: writePaths,
224
+ write_closure: [...closure.keys()].sort(),
225
+ unresolved_seeds: [...new Set(unresolved)].sort(),
226
+ truncated: walk.truncated || impact.truncated
227
+ },
228
+ closure,
229
+ writeSet,
230
+ recommendations: recommendationsFrom(index, slice.id, writeSet, impact)
231
+ };
232
+ }
233
+ export function emptyNarutoScope(sliceId, slice) {
234
+ return {
235
+ slice_id: sliceId,
236
+ seed_node_ids: [],
237
+ write_paths: narutoAdvisorPathList(slice?.writePaths),
238
+ write_closure: [],
239
+ unresolved_seeds: [],
240
+ truncated: false
241
+ };
242
+ }
@@ -956,6 +956,16 @@ export async function activeRouteContext(root, state) {
956
956
  return `Route contract sealed for ${state.route_command || state.route || state.mode}. Use decision-contract.json and ${PIPELINE_PLAN_ARTIFACT} before executing the route. Before the next route phase, read relevant TriWiki context, hydrate low-trust claims from source, and refresh/validate TriWiki again after new findings or artifact changes. Next atomic action: continue the original route lifecycle with the inferred goal, constraints, non-goals, risk boundary, and test scope.${planNote}`;
957
957
  }
958
958
  if (state.mode === 'NARUTO') {
959
+ if (state.session_scope) {
960
+ const parentEvidenceState = await appNarutoParentEvidenceState(root, state);
961
+ if (parentEvidenceState === 'completed') {
962
+ return `Active Codex App Naruto mission ${state.mission_id || 'latest'} has trustworthy current-run parent evidence and a passed Naruto gate. Do not resubmit or expose the sks.subagent-parent-summary.v1 JSON. Return concise Markdown in the user's language with an explicit completion summary, verification, remaining gaps, and Honest Mode.${reasoningNote}${planNote}`;
963
+ }
964
+ if (parentEvidenceState === 'blocked') {
965
+ return `Active Codex App Naruto mission ${state.mission_id || 'latest'} has trustworthy current-run blocked or failed parent evidence. Do not resubmit or expose the sks.subagent-parent-summary.v1 JSON. Return concise Markdown in the user's language that states the blocker or failure first, avoids completion or success wording, and includes verification, remaining gaps, and Honest Mode.${reasoningNote}${planNote}`;
966
+ }
967
+ return `Active Codex App Naruto mission ${state.mission_id || 'latest'} uses the official Codex subagent workflow. Read subagent-plan.json, keep ${SUBAGENT_EVENT_LOG_FILENAME} and ${SUBAGENT_EVIDENCE_FILENAME} current from SubagentStart/SubagentStop, and wait for all requested agent threads. Build the exact sks.subagent-parent-summary.v1 object with run_id matching subagent-plan.json.workflow_run_id, then send it only through stdin to "sks naruto parent-summary --mission ${state.mission_id} --stdin --json". Do not expose, paste, embed, quote, or fence that JSON in the user-visible response. Only after the command accepts it, return concise Markdown in the user's language with completion summary, verification, remaining gaps/blockers, and Honest Mode. A successful response must visibly include the completion summary and Honest Mode assessment. If any parent/thread status is blocked or failed, or blockers remain, state that first and do not use completion or success wording. Do not substitute process counts, PID evidence, retired process-pool artifacts, or a custom active pool.${reasoningNote}${planNote}`;
968
+ }
959
969
  return `Active Naruto mission ${state.mission_id || 'latest'} uses the official Codex subagent workflow. Read subagent-plan.json, keep ${SUBAGENT_EVENT_LOG_FILENAME} and ${SUBAGENT_EVIDENCE_FILENAME} current from SubagentStart/SubagentStop, wait for all requested agent threads, then return the exact sks.subagent-parent-summary.v1 JSON result so SKS can persist ${SUBAGENT_PARENT_SUMMARY_FILENAME} and derive naruto-summary.json/naruto-gate.json. Do not substitute process counts, PID evidence, retired process-pool artifacts, or a custom active pool.${reasoningNote}${planNote}`;
960
970
  }
961
971
  if (state.subagents_required && !(await hasSubagentEvidence(root, state))) {
@@ -968,6 +978,40 @@ export async function activeRouteContext(root, state) {
968
978
  }
969
979
  return planNote.trim();
970
980
  }
981
+ async function appNarutoParentEvidenceState(root, state) {
982
+ const missionId = String(state?.mission_id || '').trim();
983
+ const workflowRunId = String(state?.official_subagent_run_id || '').trim();
984
+ if (!missionId || !workflowRunId)
985
+ return null;
986
+ const dir = missionDir(root, missionId);
987
+ const [evidence, summary, gate] = await Promise.all([
988
+ subagentEvidence(root, state).catch(() => null),
989
+ readJson(path.join(dir, NARUTO_SUMMARY_FILENAME), null).catch(() => null),
990
+ readJson(path.join(dir, NARUTO_GATE_FILENAME), null).catch(() => null)
991
+ ]);
992
+ const currentRun = evidence?.run_id === workflowRunId
993
+ && summary?.workflow_run_id === workflowRunId
994
+ && gate?.workflow_run_id === workflowRunId;
995
+ if (!currentRun || evidence?.parent_summary_trustworthy !== true)
996
+ return null;
997
+ if (evidence.ok === true
998
+ && evidence.status === 'completed'
999
+ && summary?.status === 'completed'
1000
+ && summary?.completion_evidence === true
1001
+ && gate?.passed === true
1002
+ && gate?.terminal === true
1003
+ && gate?.terminal_state === 'completed') {
1004
+ return 'completed';
1005
+ }
1006
+ if (evidence.parent_summary_status === 'failed'
1007
+ && ['blocked', 'incomplete'].includes(String(summary?.status || ''))
1008
+ && summary?.completion_evidence !== true
1009
+ && gate?.passed !== true
1010
+ && gate?.terminal_state === 'blocked') {
1011
+ return 'blocked';
1012
+ }
1013
+ return null;
1014
+ }
971
1015
  async function activePipelinePlanNote(root, state = {}) {
972
1016
  if (!state?.mission_id)
973
1017
  return '';