sneakoscope 8.3.2 → 8.6.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 (122) hide show
  1. package/README.md +1 -1
  2. package/config/architecture-map-policy.v1.json +168 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/commands/bridge.js +19 -1
  6. package/dist/commands/doctor-profile.js +1 -1
  7. package/dist/commands/doctor.js +97 -16
  8. package/dist/config/skills-manifest.json +1 -1
  9. package/dist/core/align/align-route.js +8 -2
  10. package/dist/core/align/code-navigation-align.js +88 -32
  11. package/dist/core/architecture-map-pipeline.js +173 -0
  12. package/dist/core/architecture-map-review.js +180 -0
  13. package/dist/core/codex/agent-config-file-repair.js +17 -3
  14. package/dist/core/codex/codex-config-guard.js +3 -1
  15. package/dist/core/codex/codex-feature-flags.js +10 -0
  16. package/dist/core/codex/codex-project-config-policy.js +54 -30
  17. package/dist/core/codex/codex-startup-config-postcheck.js +5 -0
  18. package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
  19. package/dist/core/codex-app/menubar/constants.js +1 -0
  20. package/dist/core/codex-hooks/codex-hook-state-writer.js +2 -0
  21. package/dist/core/codex-lb/codex-lb-env.js +25 -3
  22. package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
  23. package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
  24. package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
  25. package/dist/core/codex-lb/desktop-bridge/security.js +11 -8
  26. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
  27. package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
  28. package/dist/core/codex-lb/request-route-resolver.js +21 -18
  29. package/dist/core/commands/triwiki-atlas-command.js +372 -0
  30. package/dist/core/commands/triwiki-command.js +12 -1
  31. package/dist/core/doctor/doctor-dirty-planner.js +16 -1
  32. package/dist/core/doctor/doctor-idempotence.js +11 -0
  33. package/dist/core/doctor/doctor-repair-postcheck.js +49 -0
  34. package/dist/core/doctor/doctor-transaction.js +47 -2
  35. package/dist/core/doctor/legacy-global-hook-cleanup.js +1 -1
  36. package/dist/core/fsx.js +14 -0
  37. package/dist/core/git-hygiene/git-policy.js +4 -0
  38. package/dist/core/git-hygiene/gitignore-writer.js +2 -0
  39. package/dist/core/gx-renderer.js +2 -1
  40. package/dist/core/init.js +40 -17
  41. package/dist/core/managed-paths.js +1 -0
  42. package/dist/core/perf/release-latency-slo.js +57 -11
  43. package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
  44. package/dist/core/pipeline-internals/runtime-core.js +18 -172
  45. package/dist/core/pipeline-internals/runtime-gates.js +12 -1
  46. package/dist/core/release/content-digest-cache.js +70 -0
  47. package/dist/core/release/gate-manifest.js +9 -0
  48. package/dist/core/release/main-push-guard.js +4 -3
  49. package/dist/core/release/package-dist-snapshot.js +18 -2
  50. package/dist/core/release/physical-release-gates.js +26 -3
  51. package/dist/core/release/release-authorization-snapshot.js +4 -0
  52. package/dist/core/release/release-gate-cache-v2.js +27 -3
  53. package/dist/core/release/release-gate-contract.js +2 -0
  54. package/dist/core/release/release-gate-dag.js +3 -0
  55. package/dist/core/release/release-inspection-memo.js +69 -0
  56. package/dist/core/release/release-pack-content-scanner.js +5 -0
  57. package/dist/core/release/release-pack-receipt.js +28 -18
  58. package/dist/core/release/release-pack-tarball.js +10 -0
  59. package/dist/core/release/release-upgrade-baseline.js +4 -0
  60. package/dist/core/routes.js +1 -1
  61. package/dist/core/safety/ssot-authority-inventory.js +114 -0
  62. package/dist/core/safety/ssot-guard.js +2 -44
  63. package/dist/core/subagents/agent-catalog.js +4 -2
  64. package/dist/core/subagents/official-subagent-config.js +92 -15
  65. package/dist/core/subagents/official-subagent-preparation.js +1 -0
  66. package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
  67. package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
  68. package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
  69. package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
  70. package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
  71. package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
  72. package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
  73. package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
  74. package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
  75. package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
  76. package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
  77. package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
  78. package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
  79. package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
  80. package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
  81. package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
  82. package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
  83. package/dist/core/triwiki/context-graph/projections/index.js +2 -0
  84. package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
  85. package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
  86. package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
  87. package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
  88. package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
  89. package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
  90. package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
  91. package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
  92. package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
  93. package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
  94. package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
  95. package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
  96. package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
  97. package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
  98. package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
  99. package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
  100. package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
  101. package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
  102. package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
  103. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
  104. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
  105. package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
  106. package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
  107. package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
  108. package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
  109. package/dist/core/version.js +1 -1
  110. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
  111. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
  112. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
  113. package/dist/scripts/architecture-map-check.js +299 -0
  114. package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
  115. package/dist/scripts/release-metadata-check.js +2 -0
  116. package/package.json +6 -3
  117. package/release-gates.v2.json +204 -0
  118. package/schemas/architecture-baseline.v1.schema.json +47 -0
  119. package/schemas/architecture-map-manifest.v1.schema.json +40 -0
  120. package/schemas/architecture-map-policy.v1.schema.json +147 -0
  121. package/schemas/architecture-review.v1.schema.json +46 -0
  122. package/schemas/mermaid-projection.v1.schema.json +47 -0
@@ -7,15 +7,40 @@ import { guardedRename, guardedRm, guardContextForRoute } from '../safety/mutati
7
7
  import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
8
8
  import { projectTriwikiToAgentsMdTransactional } from '../triwiki/agents-md-projector.js';
9
9
  import { buildCodeNavigationContextPack, validateCodeNavigationContextPack } from '../triwiki/code-navigation-context-pack.js';
10
- import { CODE_NAVIGATION_FATAL_SKIP_REASONS, CODE_NAVIGATION_LIMITS, inspectCodeNavigationSources } from '../triwiki/code-navigation-policy.js';
10
+ import { CODE_NAVIGATION_FATAL_SKIP_REASONS, CODE_NAVIGATION_LIMITS } from '../triwiki/code-navigation-policy.js';
11
11
  import { isCodePackProjectionBoundToSnapshot, projectCodePackFromGraph, validateCodePack } from '../triwiki/code-pack.js';
12
12
  import { validateContextGraphSnapshot } from '../triwiki/context-graph/contracts.js';
13
13
  import { compileContextGraph } from '../triwiki/context-graph/compiler/index.js';
14
14
  import { readSourceHashes } from '../triwiki/context-graph/compiler/freshness.js';
15
- import { codeNavigationGraphExtractors } from '../triwiki/context-graph/extractors/index.js';
15
+ import { ARCHITECTURE_MAP_EXTRACTOR_IDS, architectureMapGraphExtractors } from '../triwiki/context-graph/extractors/index.js';
16
+ import { computeContextGraphCacheKey } from '../triwiki/context-graph/compiler/cache-key.js';
17
+ import { walkCodeInventory } from '../triwiki/context-graph/extractors/code/inventory.js';
18
+ import { codeInventoryInputHashes } from '../triwiki/code-navigation-policy.js';
16
19
  import { clearContextGraphSnapshotCache } from '../triwiki/context-graph/query/snapshot-cache.js';
17
20
  import { withTriWikiStateLock } from '../triwiki/triwiki-cleanup.js';
21
+ import { publishArchitectureMapToStage } from '../triwiki/context-graph/store/architecture-map-store.js';
18
22
  import { ALIGN_GATE_ARTIFACT, ALIGN_LEDGER_ARTIFACT, ALIGN_OUTPUT_ARTIFACTS, ALIGN_PLAN_ARTIFACT, ALIGN_STAGING_ROOT_REL, buildInitialAlignLedger, refreshAlignGate } from './align-route.js';
23
+ const WIKI_PREFIX = '.sneakoscope/wiki/';
24
+ function isArchitectureMapOptionalSkip(skip) {
25
+ const rel = skip.path.replace(/\\/g, '/');
26
+ if (skip.reason !== 'unreadable' && skip.reason !== 'excluded')
27
+ return false;
28
+ if (rel === '.sneakoscope/wiki/context-pack.json')
29
+ return true;
30
+ if (rel.startsWith('.sneakoscope/triwiki/proof-bank'))
31
+ return true;
32
+ if (rel.startsWith('.sneakoscope/wiki/'))
33
+ return true;
34
+ return false;
35
+ }
36
+ function isAlignFatalSkip(skip) {
37
+ if (!CODE_NAVIGATION_FATAL_SKIP_REASONS.has(skip.reason))
38
+ return false;
39
+ return !isArchitectureMapOptionalSkip(skip);
40
+ }
41
+ if (new Set(ALIGN_OUTPUT_ARTIFACTS).size !== ALIGN_OUTPUT_ARTIFACTS.length) {
42
+ throw new Error('align_output_artifacts_not_unique');
43
+ }
19
44
  const ALIGN_REPLACED_SURFACES = Object.freeze([
20
45
  { key: 'wiki', rel: '.sneakoscope/wiki' },
21
46
  { key: 'memory', rel: '.sneakoscope/memory' },
@@ -116,19 +141,19 @@ async function buildPack(root, snapshot, meta, generatedAt) {
116
141
  observedSourceHashes: observed
117
142
  }).pack;
118
143
  }
119
- function buildManifest(generatedAt, inspection, snapshot) {
144
+ function buildManifest(generatedAt, inventory, inventoryDigest, snapshot) {
120
145
  return {
121
146
  schema: 'sks.code-navigation-manifest.v1',
122
147
  generated_at: generatedAt,
123
148
  source_policy: 'repository_code_only',
124
149
  exhaustive_graph: 'context-graph.json',
125
- inventory_digest: inspection.inventoryDigest,
150
+ inventory_digest: inventoryDigest,
126
151
  snapshot_hash: snapshot.snapshotHash,
127
- source_file_count: inspection.inventory.files.length,
152
+ source_file_count: inventory.files.length,
128
153
  symbol_count: snapshot.nodes.filter((node) => node.kind === 'symbol').length,
129
154
  edge_count: snapshot.edgeCount,
130
155
  extractors: snapshot.extractors.map(({ id, revision }) => ({ id, revision })),
131
- source_files: inspection.inventory.files.map((file) => ({
156
+ source_files: inventory.files.map((file) => ({
132
157
  path: file.rel,
133
158
  sha256: file.hash,
134
159
  language: file.language,
@@ -141,13 +166,15 @@ function buildManifest(generatedAt, inspection, snapshot) {
141
166
  }
142
167
  async function validateStaging(input) {
143
168
  for (const artifact of ALIGN_OUTPUT_ARTIFACTS) {
144
- const stageFile = path.join(input.stageWiki, path.basename(artifact));
169
+ if (!artifact.startsWith(WIKI_PREFIX))
170
+ return false;
171
+ const stageFile = path.join(input.stageWiki, artifact.slice(WIKI_PREFIX.length));
145
172
  const stat = await fsp.stat(stageFile).catch(() => null);
146
173
  if (!stat?.isFile() || stat.size <= 0)
147
174
  return false;
148
175
  }
149
176
  const meta = await readJson(path.join(input.stageWiki, 'context-graph.meta.json'), null);
150
- if (meta?.snapshotHash !== input.snapshot.snapshotHash || meta?.cacheKeyParts?.sourcePolicy !== 'repository_code_only')
177
+ if (meta?.snapshotHash !== input.snapshot.snapshotHash || meta?.cacheKeyParts?.sourcePolicy !== 'workspace')
151
178
  return false;
152
179
  const manifest = await readJson(path.join(input.stageWiki, 'code-navigation-manifest.json'), null);
153
180
  if (manifest?.inventory_digest !== input.manifest.inventory_digest || manifest?.source_file_count !== input.manifest.source_file_count)
@@ -174,21 +201,31 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
174
201
  ledger.validation.absent_or_existing_input_supported = plan.requires_cleanup_receipt === false
175
202
  && plan.acceptance.absent_or_existing_triwiki_supported === true;
176
203
  ledger.validation.prior_state_ignored = true;
177
- const extractorIdentities = codeNavigationGraphExtractors();
178
- const inspection = await inspectCodeNavigationSources(root, extractorIdentities, CODE_NAVIGATION_LIMITS);
179
- const extractors = codeNavigationGraphExtractors({ preparedInventory: inspection.inventory });
180
- ledger.scan.inventory_digest = inspection.inventoryDigest;
181
- ledger.scan.source_file_count = inspection.inventory.files.length;
182
- ledger.scan.source_bytes = inspection.inventory.files.reduce((sum, file) => sum + file.bytes, 0);
183
- ledger.scan.source_lines = inspection.inventory.files.reduce((sum, file) => sum + file.lines, 0);
184
- ledger.scan.languages = countsBy(inspection.inventory.files, (file) => file.language);
185
- ledger.scan.allowed_exclusions = inspection.inventory.skipped
186
- .filter((skip) => !CODE_NAVIGATION_FATAL_SKIP_REASONS.has(skip.reason))
204
+ const extractorIdentities = architectureMapGraphExtractors();
205
+ const inventory = walkCodeInventory(root, CODE_NAVIGATION_LIMITS);
206
+ const inputHashes = codeInventoryInputHashes(inventory);
207
+ const codeInventoryDigest = crypto
208
+ .createHash('sha256')
209
+ .update(Object.entries(inputHashes)
210
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
211
+ .map(([relative, hash]) => `${relative}\0${hash}`)
212
+ .join('\n'))
213
+ .digest('hex');
214
+ const cacheKey = await computeContextGraphCacheKey({ root, extractors: extractorIdentities });
215
+ const fatalSkips = inventory.skipped.filter((skip) => isAlignFatalSkip(skip));
216
+ ledger.scan.inventory_digest = codeInventoryDigest;
217
+ ledger.scan.source_file_count = inventory.files.length;
218
+ ledger.scan.source_bytes = inventory.files.reduce((sum, file) => sum + file.bytes, 0);
219
+ ledger.scan.source_lines = inventory.files.reduce((sum, file) => sum + file.lines, 0);
220
+ ledger.scan.languages = countsBy(inventory.files, (file) => file.language);
221
+ ledger.scan.allowed_exclusions = inventory.skipped
222
+ .filter((skip) => !isAlignFatalSkip(skip))
187
223
  .map((skip) => ({ path: skip.path, reason: skip.reason }));
188
- ledger.scan.fatal_skips = inspection.fatalSkips.map((skip) => ({ ...skip }));
189
- ledger.validation.source_inventory_complete = inspection.fatalSkips.length === 0;
190
- if (inspection.fatalSkips.length)
191
- throw new Error(`code_navigation_fatal_skips:${inspection.fatalSkips.map((skip) => `${skip.reason}:${skip.path}`).join('|')}`);
224
+ ledger.scan.fatal_skips = fatalSkips.map((skip) => ({ ...skip }));
225
+ ledger.validation.source_inventory_complete = fatalSkips.length === 0;
226
+ if (fatalSkips.length)
227
+ throw new Error(`code_navigation_fatal_skips:${fatalSkips.map((skip) => `${skip.reason}:${skip.path}`).join('|')}`);
228
+ const extractors = architectureMapGraphExtractors({ preparedInventory: inventory });
192
229
  const generatedAt = nowIso();
193
230
  const compiled = await compileContextGraph({
194
231
  root,
@@ -197,12 +234,12 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
197
234
  limits: CODE_NAVIGATION_LIMITS,
198
235
  observedAt: generatedAt,
199
236
  useFragmentCache: false,
200
- cacheKey: inspection.cacheKey,
237
+ cacheKey,
201
238
  persistArtifacts: false,
202
239
  useCompileLock: false
203
240
  });
204
241
  ledger.validation.graph_compile = compiled.ok;
205
- const compileFatal = compiled.skipped.filter((skip) => CODE_NAVIGATION_FATAL_SKIP_REASONS.has(skip.reason));
242
+ const compileFatal = compiled.skipped.filter((skip) => isAlignFatalSkip(skip));
206
243
  if (compileFatal.length)
207
244
  ledger.scan.fatal_skips.push(...compileFatal.map((skip) => ({ ...skip })));
208
245
  if (!compiled.ok || !compiled.snapshot || !compiled.meta || compileFatal.length) {
@@ -218,7 +255,7 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
218
255
  ledger.graph.test_nodes = snapshot.nodes.filter((node) => node.kind === 'test').length;
219
256
  ledger.graph.edge_count = snapshot.edgeCount;
220
257
  ledger.graph.edges_by_type = countsBy(snapshot.edges, (edge) => edge.type);
221
- const inventoryPaths = inspection.inventory.files.map((file) => file.rel).sort();
258
+ const inventoryPaths = inventory.files.map((file) => file.rel).sort();
222
259
  const fileNodePaths = snapshot.nodes.filter((node) => node.kind === 'file').map((node) => String(node.path || '')).filter(Boolean).sort();
223
260
  const inventorySet = new Set(inventoryPaths);
224
261
  const fileNodeSet = new Set(fileNodePaths);
@@ -229,12 +266,16 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
229
266
  && fileNodePaths.length === inventoryPaths.length;
230
267
  if (!ledger.graph.exact_file_coverage)
231
268
  throw new Error('code_navigation_exact_file_coverage_failed');
232
- if (ledger.graph.extractor_ids.length !== 1 || ledger.graph.extractor_ids[0] !== 'code')
233
- throw new Error('code_navigation_non_code_extractor_present');
269
+ const expectedExtractors = [...ARCHITECTURE_MAP_EXTRACTOR_IDS];
270
+ if (ledger.graph.extractor_ids.length !== expectedExtractors.length
271
+ || expectedExtractors.some((id, index) => ledger.graph.extractor_ids[index] !== id)) {
272
+ throw new Error(`code_navigation_architecture_map_extractors_mismatch:${ledger.graph.extractor_ids.join(',')}`);
273
+ }
234
274
  const graphValidation = validateContextGraphSnapshot(snapshot);
235
275
  ledger.validation.graph_schema = graphValidation.ok;
236
- if (!graphValidation.ok || meta.cacheKeyParts.sourcePolicy !== 'repository_code_only')
276
+ if (!graphValidation.ok || meta.cacheKeyParts.sourcePolicy !== 'workspace') {
237
277
  throw new Error('code_navigation_graph_validation_failed');
278
+ }
238
279
  const pack = await buildPack(root, snapshot, meta, generatedAt);
239
280
  const packValidation = await validateCodePack(pack, root);
240
281
  ledger.validation.code_pack = packValidation.ok
@@ -255,7 +296,7 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
255
296
  ledger.validation.context_pack = contextValidation.ok;
256
297
  if (!contextValidation.ok)
257
298
  throw new Error('code_navigation_context_pack_invalid');
258
- const manifest = buildManifest(generatedAt, inspection, snapshot);
299
+ const manifest = buildManifest(generatedAt, inventory, codeInventoryDigest, snapshot);
259
300
  const stageRoot = path.join(stageBase, `${missionId}-${randomUUID().slice(0, 8)}`);
260
301
  const stageWiki = path.join(stageRoot, 'new-wiki');
261
302
  const previousRoot = path.join(stageRoot, 'previous');
@@ -271,6 +312,7 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
271
312
  await writeJsonAtomic(path.join(stageWiki, 'code-navigation-manifest.json'), manifest);
272
313
  await writeJsonAtomic(path.join(stageWiki, 'code-pack.json'), pack);
273
314
  await writeJsonAtomic(path.join(stageWiki, 'context-pack.json'), contextPack);
315
+ await publishArchitectureMapToStage(stageWiki, snapshot, { root, missionId });
274
316
  ledger.publication.staged = true;
275
317
  ledger.validation.staged_readback = await validateStaging({ stageWiki, root, snapshot, pack, contextPack, manifest });
276
318
  if (!ledger.validation.staged_readback)
@@ -308,9 +350,23 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
308
350
  }
309
351
  ledger.validation.artifact_hashes = Object.keys(ledger.publication.artifact_sha256).length === ALIGN_OUTPUT_ARTIFACTS.length;
310
352
  await beforeFinalSourceCas?.();
311
- const finalInspection = await inspectCodeNavigationSources(root, extractorIdentities, CODE_NAVIGATION_LIMITS);
312
- ledger.scan.source_cas_verified = finalInspection.fatalSkips.length === 0
313
- && finalInspection.cacheKey.key === inspection.cacheKey.key;
353
+ const finalInventory = walkCodeInventory(root, CODE_NAVIGATION_LIMITS);
354
+ const finalFatal = finalInventory.skipped.filter((skip) => isAlignFatalSkip(skip));
355
+ const finalCacheKey = await computeContextGraphCacheKey({ root, extractors: extractorIdentities });
356
+ const finalCodeDigest = crypto
357
+ .createHash('sha256')
358
+ .update(Object.entries(codeInventoryInputHashes(finalInventory))
359
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
360
+ .map(([relative, hash]) => `${relative}\0${hash}`)
361
+ .join('\n'))
362
+ .digest('hex');
363
+ const casStable = finalCacheKey.parts.tsconfigHash === cacheKey.parts.tsconfigHash
364
+ && finalCacheKey.parts.commandManifestHash === cacheKey.parts.commandManifestHash
365
+ && finalCacheKey.parts.gateManifestHash === cacheKey.parts.gateManifestHash
366
+ && finalCacheKey.parts.schemaRevision === cacheKey.parts.schemaRevision
367
+ && finalCacheKey.parts.proofIndexHash === cacheKey.parts.proofIndexHash
368
+ && finalCodeDigest === codeInventoryDigest;
369
+ ledger.scan.source_cas_verified = finalFatal.length === 0 && casStable;
314
370
  if (!ledger.scan.source_cas_verified)
315
371
  throw new Error('code_navigation_source_changed_during_scan');
316
372
  commitStarted = true;
@@ -0,0 +1,173 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { exists, nowIso, readJson, writeJsonAtomic } from './fsx.js';
4
+ import { ARCHITECTURE_CAPSULE_ARTIFACT, ARCHITECTURE_MAP_BASELINE_ARTIFACT, ARCHITECTURE_MAP_MANIFEST_ARTIFACT, ARCHITECTURE_MAP_REVIEW_ARTIFACT, bindArchitectureMapBaseline, buildAfterReview, sealBaseline, validateArchitectureMapBaselineArtifact, validateArchitectureMapReviewArtifact } from './architecture-map-review.js';
5
+ import { readContextGraphSnapshot } from './triwiki/context-graph/store/snapshot-store.js';
6
+ export { ARCHITECTURE_MAP_REVIEW_ARTIFACT };
7
+ export const ARCHITECTURE_MAP_BASELINE_STAGE = 'architecture_map_baseline';
8
+ export const ARCHITECTURE_MAP_REVIEW_STAGE = 'architecture_map_review';
9
+ const ARCHITECTURE_MAP_EXEMPT_ROUTES = new Set([
10
+ 'Answer',
11
+ 'DFix',
12
+ 'Help',
13
+ 'Wiki',
14
+ 'Goal',
15
+ 'GX',
16
+ 'DB'
17
+ ]);
18
+ export function routeNeedsArchitectureMap(route) {
19
+ const id = String(route?.id || '');
20
+ if (!id)
21
+ return false;
22
+ if (ARCHITECTURE_MAP_EXEMPT_ROUTES.has(id))
23
+ return false;
24
+ return true;
25
+ }
26
+ export function createArchitectureMapPlanBinding() {
27
+ return Object.freeze({
28
+ baseline_artifact: ARCHITECTURE_MAP_BASELINE_ARTIFACT,
29
+ review_artifact: ARCHITECTURE_MAP_REVIEW_ARTIFACT,
30
+ capsule_artifact: ARCHITECTURE_CAPSULE_ARTIFACT,
31
+ manifest_artifact: ARCHITECTURE_MAP_MANIFEST_ARTIFACT,
32
+ required: true,
33
+ seeded_at: nowIso()
34
+ });
35
+ }
36
+ export function planStagesArchitectureMap(plan) {
37
+ return Array.isArray(plan?.stages)
38
+ && plan.stages.some((stage) => String(stage?.id || '') === ARCHITECTURE_MAP_BASELINE_STAGE
39
+ && !['skipped', 'not_applicable'].includes(String(stage?.status || '')));
40
+ }
41
+ export async function seedArchitectureMapPlanBinding(dir, plan) {
42
+ if (!planStagesArchitectureMap(plan))
43
+ return plan;
44
+ plan.architecture_map = createArchitectureMapPlanBinding();
45
+ await writeJsonAtomic(path.join(dir, 'pipeline-plan.json'), plan);
46
+ return plan;
47
+ }
48
+ export async function maybeSeedArchitectureMapForPlan(input) {
49
+ if (!input.plan?.architecture_map || !input.missionId) {
50
+ return { ok: false, reason: 'architecture_map_not_planned' };
51
+ }
52
+ return seedArchitectureMapBaselineArtifacts({
53
+ root: input.root,
54
+ dir: input.dir,
55
+ missionId: String(input.missionId),
56
+ routeId: String(input.plan.route?.id || input.plan.route?.command || 'SKS'),
57
+ taskProfile: String(input.taskProfile || input.plan.task_profile || '')
58
+ });
59
+ }
60
+ export async function seedArchitectureMapBaselineArtifacts(input) {
61
+ const baselinePath = path.join(input.dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT);
62
+ if (await exists(baselinePath))
63
+ return { ok: true, baselinePath };
64
+ const load = await readContextGraphSnapshot(input.root);
65
+ if (load.status !== 'ok' || !load.snapshot) {
66
+ return { ok: false, reason: load.blocker || 'architecture_map_graph_unavailable' };
67
+ }
68
+ const sealed = sealBaseline({
69
+ missionId: input.missionId,
70
+ routeId: input.routeId,
71
+ snapshot: load.snapshot,
72
+ policyRoot: input.root,
73
+ capturedAt: nowIso(),
74
+ ...(input.taskProfile ? { taskProfile: input.taskProfile } : {})
75
+ });
76
+ await fsp.mkdir(input.dir, { recursive: true });
77
+ await writeJsonAtomic(baselinePath, sealed.baseline);
78
+ await writeJsonAtomic(path.join(input.dir, ARCHITECTURE_MAP_MANIFEST_ARTIFACT), sealed.manifest);
79
+ await writeArchitectureMapCapsule(input.dir, sealed.capsule.text);
80
+ return { ok: true, baselinePath };
81
+ }
82
+ export async function ensureArchitectureMapReviewArtifacts(input) {
83
+ const reviewPath = path.join(input.dir, ARCHITECTURE_MAP_REVIEW_ARTIFACT);
84
+ if (await exists(reviewPath))
85
+ return { ok: true, reviewPath };
86
+ const baselinePath = path.join(input.dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT);
87
+ if (!(await exists(baselinePath))) {
88
+ return { ok: false, reason: 'architecture_map_baseline_missing' };
89
+ }
90
+ const baseline = await readJson(baselinePath, null);
91
+ if (!baseline)
92
+ return { ok: false, reason: 'architecture_map_baseline_unreadable' };
93
+ const load = await readContextGraphSnapshot(input.root);
94
+ if (load.status !== 'ok' || !load.snapshot) {
95
+ return { ok: false, reason: load.blocker || 'architecture_map_graph_unavailable' };
96
+ }
97
+ const built = buildAfterReview({
98
+ missionId: input.missionId,
99
+ ...(input.routeId ? { routeId: input.routeId } : {}),
100
+ baseline,
101
+ afterSnapshot: load.snapshot,
102
+ policyRoot: input.root,
103
+ generatedAt: nowIso()
104
+ });
105
+ await fsp.mkdir(input.dir, { recursive: true });
106
+ await writeJsonAtomic(reviewPath, built.review);
107
+ await writeJsonAtomic(path.join(input.dir, ARCHITECTURE_MAP_MANIFEST_ARTIFACT), built.manifest);
108
+ await writeArchitectureMapCapsule(input.dir, built.capsule.text);
109
+ await writeJsonAtomic(path.join(input.dir, 'architecture-map-delta.json'), built.delta);
110
+ return { ok: true, reviewPath };
111
+ }
112
+ export async function architectureMapGateStatus(root, state = {}) {
113
+ if (state?.architecture_map_required !== true)
114
+ return { ok: true, blockers: [] };
115
+ const missionId = state?.mission_id;
116
+ if (!missionId)
117
+ return { ok: false, blockers: ['architecture_map_mission_missing'] };
118
+ const dir = path.join(root, '.sneakoscope/missions', missionId);
119
+ const plan = await readJson(path.join(dir, 'pipeline-plan.json'), null);
120
+ if (!plan?.architecture_map)
121
+ return { ok: true, blockers: [], not_applicable: true };
122
+ if (await exists(path.join(dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT))) {
123
+ await ensureArchitectureMapReviewArtifacts({
124
+ root,
125
+ dir,
126
+ missionId,
127
+ routeId: String(plan?.route?.id || state?.route || '')
128
+ });
129
+ }
130
+ const blockers = [];
131
+ const baselinePath = path.join(dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT);
132
+ const reviewPath = path.join(dir, ARCHITECTURE_MAP_REVIEW_ARTIFACT);
133
+ if (!(await exists(baselinePath))) {
134
+ blockers.push('architecture_map_baseline_missing');
135
+ }
136
+ else {
137
+ const baseline = await readJson(baselinePath, null);
138
+ const validation = validateArchitectureMapBaselineArtifact(baseline);
139
+ if (!validation.ok)
140
+ blockers.push('architecture_map_baseline_invalid', ...validation.blockers.map((b) => `baseline:${b}`));
141
+ else {
142
+ const binding = bindArchitectureMapBaseline({ baseline: baseline, missionId });
143
+ if (!binding.ok)
144
+ blockers.push(...binding.blockers.map((b) => `architecture_map_baseline_${b}`));
145
+ if (baseline && baseline.capturedBeforeMutation !== true)
146
+ blockers.push('architecture_map_baseline_late');
147
+ }
148
+ }
149
+ if (!(await exists(reviewPath))) {
150
+ blockers.push('architecture_map_review_missing');
151
+ }
152
+ else {
153
+ const review = await readJson(reviewPath, null);
154
+ const validation = validateArchitectureMapReviewArtifact(review);
155
+ if (!validation.ok)
156
+ blockers.push('architecture_map_review_invalid', ...validation.blockers.map((b) => `review:${b}`));
157
+ else if (review?.verdict !== 'pass') {
158
+ blockers.push('architecture_map_review_blocked');
159
+ if (Array.isArray(review?.unaccountedChangedPaths) && review.unaccountedChangedPaths.length) {
160
+ blockers.push('architecture_map_unaccounted_changed_file');
161
+ }
162
+ for (const id of review?.blockingFindingIds || [])
163
+ blockers.push(`architecture_map_finding:${id}`);
164
+ }
165
+ }
166
+ return { ok: blockers.length === 0, blockers: [...new Set(blockers)] };
167
+ }
168
+ export async function writeArchitectureMapCapsule(dir, text) {
169
+ const file = path.join(dir, ARCHITECTURE_CAPSULE_ARTIFACT);
170
+ await fsp.mkdir(dir, { recursive: true });
171
+ await fsp.writeFile(file, text.endsWith('\n') ? text : `${text}\n`, 'utf8');
172
+ return file;
173
+ }
@@ -0,0 +1,180 @@
1
+ import { sha256 } from './fsx.js';
2
+ import { ARCHITECTURE_BASELINE_SCHEMA, ARCHITECTURE_MAP_MANIFEST_SCHEMA, ARCHITECTURE_REVIEW_SCHEMA } from './triwiki/context-graph/architecture/contracts.js';
3
+ import { buildArchitectureBaseline, verifyArchitectureBaselineSeal } from './triwiki/context-graph/architecture/baseline.js';
4
+ import { renderArchitectureCapsule } from './triwiki/context-graph/architecture/capsule.js';
5
+ import { buildArchitectureDelta, buildArchitectureReview } from './triwiki/context-graph/architecture/delta.js';
6
+ import { buildArchitectureInputBundle, policyContentHash } from './triwiki/context-graph/architecture/input-bundle.js';
7
+ import { loadArchitectureMapPolicy } from './triwiki/context-graph/architecture/policy.js';
8
+ import { buildArchitectureScope } from './triwiki/context-graph/architecture/slice.js';
9
+ import { validateArchitectureBaseline, validateArchitectureInputBundle, validateArchitectureMapManifest, validateArchitectureReview } from './triwiki/context-graph/architecture/validation.js';
10
+ import { buildArchitectureMapManifest, buildArchitectureMapViews } from './triwiki/context-graph/projections/mermaid/index.js';
11
+ export const ARCHITECTURE_MAP_BASELINE_ARTIFACT = 'architecture-map-baseline.json';
12
+ export const ARCHITECTURE_MAP_REVIEW_ARTIFACT = 'architecture-map-review.json';
13
+ export const ARCHITECTURE_CAPSULE_ARTIFACT = 'architecture-capsule.txt';
14
+ export const ARCHITECTURE_MAP_MANIFEST_ARTIFACT = 'architecture-map-manifest.json';
15
+ export const ARCHITECTURE_MAP_REVIEW_SCHEMA = ARCHITECTURE_REVIEW_SCHEMA;
16
+ export const ARCHITECTURE_MAP_BASELINE_SCHEMA = ARCHITECTURE_BASELINE_SCHEMA;
17
+ export const ARCHITECTURE_MAP_MANIFEST_SCHEMA_ID = ARCHITECTURE_MAP_MANIFEST_SCHEMA;
18
+ export function resolveArchitectureMapPolicy(input) {
19
+ if (input.policy)
20
+ return input.policy;
21
+ if (input.policyRoot)
22
+ return loadArchitectureMapPolicy(input.policyRoot);
23
+ throw new Error('architecture_map_policy_required');
24
+ }
25
+ export function sealBaseline(input) {
26
+ const policy = resolveArchitectureMapPolicy(input);
27
+ const rootId = input.rootId ?? 'workspace';
28
+ const profile = input.profile ?? 'global';
29
+ const bundle = buildArchitectureInputBundle({
30
+ rootId,
31
+ graph: input.snapshot,
32
+ policy,
33
+ profile
34
+ });
35
+ const views = buildArchitectureMapViews(input.snapshot, policy, {
36
+ rootId,
37
+ missionId: input.missionId
38
+ });
39
+ const viewHashes = Object.freeze(Object.fromEntries(views.views.map((view) => [view.viewId, view.projection.contentHash])));
40
+ const scope = buildArchitectureScope({
41
+ profile,
42
+ ...(input.seedNodeIds ? { seedNodeIds: input.seedNodeIds } : {}),
43
+ ...(input.seedPaths ? { seedPaths: input.seedPaths } : {})
44
+ });
45
+ const baseline = buildArchitectureBaseline({
46
+ missionId: input.missionId,
47
+ routeId: input.routeId,
48
+ taskProfile: input.taskProfile ?? profile,
49
+ capturedAt: input.capturedAt,
50
+ bundle,
51
+ policy,
52
+ scope,
53
+ viewHashes,
54
+ findings: views.findings,
55
+ metrics: views.metrics
56
+ });
57
+ const capsule = renderArchitectureCapsule({
58
+ profile,
59
+ missionId: input.missionId,
60
+ routeId: input.routeId,
61
+ baseline,
62
+ metrics: views.metrics,
63
+ findings: views.findings,
64
+ ...(input.seedPaths ? { seedPaths: input.seedPaths } : {}),
65
+ mapArtifact: ARCHITECTURE_MAP_MANIFEST_ARTIFACT
66
+ });
67
+ return Object.freeze({
68
+ baseline,
69
+ bundle,
70
+ manifest: views.manifest,
71
+ capsule,
72
+ policy
73
+ });
74
+ }
75
+ export function buildAfterReview(input) {
76
+ const policy = resolveArchitectureMapPolicy(input);
77
+ const rootId = input.rootId ?? 'workspace';
78
+ const profile = input.profile ?? input.baseline.scope.profile;
79
+ const afterBundle = buildArchitectureInputBundle({
80
+ rootId,
81
+ graph: input.afterSnapshot,
82
+ policy,
83
+ profile,
84
+ policyHash: policyContentHash(policy)
85
+ });
86
+ const views = buildArchitectureMapViews(input.afterSnapshot, policy, {
87
+ rootId,
88
+ missionId: input.missionId
89
+ });
90
+ const projectionAccounting = Object.freeze(Object.fromEntries(views.views.map((view) => [
91
+ view.viewId,
92
+ {
93
+ selectedNodeCount: view.projection.accounting.selectedNodeIds.length,
94
+ emittedNodeCount: view.projection.accounting.emittedNodeIds.length,
95
+ omittedNodeCount: view.projection.accounting.omittedNodes.length,
96
+ selectedEdgeCount: view.projection.accounting.selectedEdgeIds.length,
97
+ emittedEdgeCount: view.projection.accounting.emittedEdgeIds.length,
98
+ omittedEdgeCount: view.projection.accounting.omittedEdges.length,
99
+ accountingRatio: view.projection.accounting.selectedNodeIds.length === 0
100
+ ? 1
101
+ : view.projection.accounting.emittedNodeIds.length /
102
+ view.projection.accounting.selectedNodeIds.length
103
+ }
104
+ ])));
105
+ const review = buildArchitectureReview({
106
+ missionId: input.missionId,
107
+ baseline: input.baseline,
108
+ afterBundle,
109
+ policy,
110
+ generatedAt: input.generatedAt,
111
+ projectionAccounting,
112
+ ...(input.accountedChangedPaths
113
+ ? { accountedChangedPaths: input.accountedChangedPaths }
114
+ : {})
115
+ });
116
+ const delta = buildArchitectureDelta({
117
+ missionId: input.missionId,
118
+ baseline: input.baseline,
119
+ afterBundle,
120
+ policy
121
+ });
122
+ const capsule = renderArchitectureCapsule({
123
+ profile,
124
+ missionId: input.missionId,
125
+ ...(input.routeId ? { routeId: input.routeId } : {}),
126
+ baseline: input.baseline,
127
+ metrics: views.metrics,
128
+ findings: views.findings,
129
+ mapArtifact: 'architecture-map-delta.mmd'
130
+ });
131
+ return Object.freeze({
132
+ review,
133
+ delta,
134
+ afterBundle,
135
+ manifest: views.manifest,
136
+ capsule,
137
+ policy
138
+ });
139
+ }
140
+ export function validateArchitectureMapBaselineArtifact(value) {
141
+ return validateArchitectureBaseline(value);
142
+ }
143
+ export function validateArchitectureMapReviewArtifact(value) {
144
+ return validateArchitectureReview(value);
145
+ }
146
+ export function validateArchitectureMapManifestArtifact(value) {
147
+ return validateArchitectureMapManifest(value);
148
+ }
149
+ export function validateArchitectureMapInputBundleArtifact(value) {
150
+ return validateArchitectureInputBundle(value);
151
+ }
152
+ export function bindArchitectureMapBaseline(input) {
153
+ const validation = validateArchitectureBaseline(input.baseline);
154
+ const blockers = [...validation.blockers];
155
+ if (input.baseline.missionId !== input.missionId)
156
+ blockers.push('mission_mismatch');
157
+ if (!verifyArchitectureBaselineSeal(input.baseline))
158
+ blockers.push('seal_mismatch');
159
+ if (blockers.length)
160
+ return { ok: false, blockers, binding: null };
161
+ return {
162
+ ok: true,
163
+ blockers: [],
164
+ binding: {
165
+ missionId: input.baseline.missionId,
166
+ seal: input.baseline.seal,
167
+ canonicalPayloadHash: input.baseline.canonicalPayloadHash,
168
+ policyHash: input.baseline.policyHash,
169
+ graphHash: input.baseline.graphHash,
170
+ worktreeFingerprintHash: input.baseline.worktreeFingerprintHash
171
+ }
172
+ };
173
+ }
174
+ export function architectureMapReviewDigest(review) {
175
+ return sha256(JSON.stringify(review ?? null));
176
+ }
177
+ export function architectureMapBaselineDigest(baseline) {
178
+ return sha256(JSON.stringify(baseline ?? null));
179
+ }
180
+ export { buildArchitectureMapManifest, buildArchitectureMapViews };
@@ -1,8 +1,13 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
- import { backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfig, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText } from '../subagents/official-subagent-config.js';
4
+ import { SKS_MANAGED_CODEX_CONFIG_MARKER, backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfigResult, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText } from '../subagents/official-subagent-config.js';
5
5
  import { writeCodexConfigGuarded } from './codex-config-guard.js';
6
+ function unmanagedConfigOperatorAction(configPath) {
7
+ return `Refused to modify ${configPath}: no SKS ownership proof. `
8
+ + `If SKS owns this file, add the line \`${SKS_MANAGED_CODEX_CONFIG_MARKER}\` as its first line `
9
+ + 'and re-run `sks doctor --fix`. Otherwise run `sks setup` to regenerate a managed config.';
10
+ }
6
11
  export async function repairAgentConfigFileReferences(input) {
7
12
  const root = path.resolve(input.root);
8
13
  const configPath = path.join(root, '.codex', 'config.toml');
@@ -37,6 +42,10 @@ export async function repairAgentConfigFileReferences(input) {
37
42
  ...(!ownershipProof.owned ? ['user_owned_file_without_sks_marker'] : [])
38
43
  ],
39
44
  warnings: [],
45
+ operator_actions: [
46
+ `${configPath} is not valid TOML; SKS preserved it and wrote a backup. Fix the syntax, then re-run \`sks doctor --fix\`.`,
47
+ ...(!ownershipProof.owned ? [unmanagedConfigOperatorAction(configPath)] : [])
48
+ ],
40
49
  ownership_proof: ownershipProof
41
50
  });
42
51
  }
@@ -55,6 +64,7 @@ export async function repairAgentConfigFileReferences(input) {
55
64
  manual_required: true,
56
65
  blockers: ['user_owned_file_without_sks_marker'],
57
66
  warnings: [],
67
+ operator_actions: [unmanagedConfigOperatorAction(configPath)],
58
68
  ownership_proof: ownershipProof
59
69
  });
60
70
  }
@@ -62,13 +72,14 @@ export async function repairAgentConfigFileReferences(input) {
62
72
  ...(input.home ? { home: input.home } : {}),
63
73
  ...(input.codexHome ? { codexHome: input.codexHome } : {})
64
74
  });
65
- const merged = mergeOfficialSubagentConfig(original, {
75
+ const mergeResult = mergeOfficialSubagentConfigResult(original, {
66
76
  sksOwned: ownershipProof.owned,
67
77
  inheritedText
68
78
  });
79
+ const merged = mergeResult.text;
69
80
  const validation = inspectOfficialSubagentToml(merged);
70
81
  const warnings = officialSubagentConfigWarnings(merged, inheritedText);
71
- const blockers = [];
82
+ const blockers = [...mergeResult.blockers];
72
83
  let changed = merged !== original;
73
84
  let writeSucceeded = input.apply !== true;
74
85
  let backupPath = null;
@@ -104,6 +115,9 @@ export async function repairAgentConfigFileReferences(input) {
104
115
  manual_required: blockers.length > 0,
105
116
  blockers,
106
117
  warnings,
118
+ operator_actions: blockers.map((blocker) => blocker === 'user_owned_file_without_sks_marker'
119
+ ? unmanagedConfigOperatorAction(configPath)
120
+ : `${configPath}: ${blocker}. Re-run \`sks doctor --fix\` after resolving it, or run \`sks setup\` to regenerate a managed config.`),
107
121
  ownership_proof: ownershipProof
108
122
  };
109
123
  return writeReport(input.reportPath, root, report);
@@ -4,6 +4,7 @@ import { randomBytes } from 'node:crypto';
4
4
  import { appendJsonl, ensureDir, nowIso, readText, sha256, writeTextAtomic } from '../fsx.js';
5
5
  import { diffCodexAppUiSnapshots, snapshotCodexAppUiState } from '../codex-app/codex-app-ui-state-snapshot.js';
6
6
  import { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from './codex-config-toml.js';
7
+ import { hasManagedAgentsConfigFingerprint } from '../subagents/official-subagent-config.js';
7
8
  import { escapeRegExp } from '../text/regex.js';
8
9
  import { withFileLock } from '../locks/file-lock.js';
9
10
  export const SKS_MANAGED_CODEX_CONFIG_MARKER = '# SKS-MANAGED-CODEX-CONFIG';
@@ -576,7 +577,8 @@ export function isProjectCodexConfig(root, configPath) {
576
577
  export function hasSksManagedCodexConfigMarker(text) {
577
578
  const source = String(text || '');
578
579
  return hasExplicitSksManagedCodexConfigMarker(source)
579
- || /(?:SKS managed|Sneakoscope|sneakoscope|sks_|agents\.native_agent|agents\.implementation_worker|multi_agent)/i.test(source)
580
+ || /(?:SKS managed|Sneakoscope|sneakoscope|sks_|agents\.native_agent|agents\.implementation_worker)/i.test(source)
581
+ || hasManagedAgentsConfigFingerprint(source)
580
582
  || /^\s*model_provider\s*=\s*["']codex-lb["']\s*(?:#.*)?$/mi.test(source)
581
583
  || /^\s*default_profile\s*=\s*["']sks-fast-high["']\s*(?:#.*)?$/mi.test(source)
582
584
  || /^\s*\[(?:user\.fast_mode|model_providers\.(?:"codex-lb"|codex-lb)|profiles\.(?:"sks-fast-high"|sks-fast-high))\]\s*(?:#.*)?$/mi.test(source);
@@ -0,0 +1,10 @@
1
+ export const MANAGED_CODEX_FEATURE_FLAGS = Object.freeze(['hooks', 'fast_mode', 'apps']);
2
+ export const REMOVED_CODEX_FEATURE_FLAGS = Object.freeze([
3
+ 'fast_mode_ui',
4
+ 'codex_hooks',
5
+ 'remote_control',
6
+ 'codex_git_commit',
7
+ 'plugin_hooks',
8
+ 'js_repl',
9
+ 'multi_agent_mode'
10
+ ]);