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
@@ -22,11 +22,13 @@ export function resolveBridgeRequestRoute(request, policy, options) {
22
22
  if (options.active_catalog_generation && options.active_catalog_generation !== policy.catalog_generation) {
23
23
  return blocked(base, 'catalog_route_index_stale');
24
24
  }
25
- const pin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
26
- if (pin && canonicalizeBridgeModelId(pin.public_model) === model) {
27
- const pinBlocker = validateSessionPin(pin, policy, options.route_index, options.registry);
28
- if (pinBlocker)
29
- return blocked(base, pinBlocker);
25
+ const threadPin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
26
+ const pin = threadPin && canonicalizeBridgeModelId(threadPin.public_model) === model ? threadPin : null;
27
+ const pinAffinity = pin ? sessionPinAffinity(pin, policy, options.route_index, options.registry) : null;
28
+ if (pinAffinity === 'unprovable')
29
+ return blocked(base, 'session_pin_route_unavailable');
30
+ const pinClaim = pinAffinity && pinAffinity !== 'honored' ? pinAffinity : null;
31
+ if (pin && pinAffinity === 'honored') {
30
32
  const route = { provider_id: pin.provider_id, upstream_model: pin.upstream_model };
31
33
  const providerBlocker = validateProviderRoute(route, request.requested_endpoint_origin, options.registry, options.route_index);
32
34
  if (providerBlocker)
@@ -51,6 +53,8 @@ export function resolveBridgeRequestRoute(request, policy, options) {
51
53
  return blocked(base, 'catalog_model_route_missing');
52
54
  if (!sameTarget(indexed, policyTarget))
53
55
  return blocked(base, 'bridge_route_policy_route_index_mismatch');
56
+ if (pinClaim && !sameTarget(indexed, pinClaim))
57
+ return blocked(base, 'session_pin_route_unavailable');
54
58
  const providerBlocker = validateProviderRoute(indexed, request.requested_endpoint_origin, options.registry, options.route_index);
55
59
  if (providerBlocker)
56
60
  return blocked(base, providerBlocker);
@@ -76,23 +80,22 @@ export function resolveBridgeRequestRoute(request, policy, options) {
76
80
  recovery_action: null
77
81
  };
78
82
  }
79
- function validateSessionPin(pin, policy, routeIndex, registry) {
80
- if (pin.catalog_generation !== policy.catalog_generation
81
- || pin.route_policy_generation !== policy.policy_generation) {
82
- return 'session_pin_route_unavailable';
83
- }
84
- if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id]) {
85
- return 'session_pin_route_unavailable';
86
- }
83
+ function sessionPinAffinity(pin, policy, routeIndex, registry) {
84
+ if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id])
85
+ return 'unprovable';
87
86
  const model = canonicalizeBridgeModelId(pin.public_model);
88
87
  const upstream = normalizeBridgeUpstreamModelId(pin.upstream_model);
89
88
  if (!model || !upstream)
90
- return 'session_pin_route_unavailable';
91
- const indexed = routeIndex.routes[model] || routeIndex.routes[`${pin.provider_id}:${model}`];
92
- if (!indexed || indexed.provider_id !== pin.provider_id || indexed.upstream_model !== upstream) {
93
- return 'session_pin_route_unavailable';
89
+ return 'unprovable';
90
+ const claim = { provider_id: pin.provider_id, upstream_model: upstream };
91
+ if (pin.catalog_generation !== policy.catalog_generation
92
+ || pin.route_policy_generation !== policy.policy_generation) {
93
+ return claim;
94
94
  }
95
- return null;
95
+ const indexed = routeIndex.routes[model] || routeIndex.routes[`${pin.provider_id}:${model}`];
96
+ if (!indexed || !sameTarget(indexed, claim))
97
+ return claim;
98
+ return 'honored';
96
99
  }
97
100
  function validateProviderRoute(route, requestedOrigin, registry, routeIndex) {
98
101
  if (!isProviderId(route.provider_id))
@@ -0,0 +1,372 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { flag, readOption } from '../../cli/args.js';
4
+ import { ARCHITECTURE_MAP_MANIFEST_SCHEMA, ARCHITECTURE_MAP_VIEW_IDS, GLOBAL_ARCHITECTURE_MAP_VIEW_IDS } from '../triwiki/context-graph/architecture/contracts.js';
5
+ import { ARCHITECTURE_MAP_DIR_REL } from '../triwiki/context-graph/store/architecture-map-store.js';
6
+ export const TRIWIKI_ATLAS_SUBCOMMANDS = [
7
+ 'atlas-status',
8
+ 'atlas-lint',
9
+ 'atlas-list',
10
+ 'atlas-show',
11
+ 'atlas-why'
12
+ ];
13
+ const REPAIR_COMMAND = 'sks align run';
14
+ const VIEW_ID_SET = new Set(ARCHITECTURE_MAP_VIEW_IDS);
15
+ const GLOBAL_VIEW_ID_SET = new Set(GLOBAL_ARCHITECTURE_MAP_VIEW_IDS);
16
+ export function isTriWikiAtlasSubcommand(value) {
17
+ return typeof value === 'string' && TRIWIKI_ATLAS_SUBCOMMANDS.includes(value);
18
+ }
19
+ function mapRoot(root) {
20
+ return path.join(root, ...ARCHITECTURE_MAP_DIR_REL.split('/'));
21
+ }
22
+ function missionMapRoot(root, missionId) {
23
+ return path.join(root, '.sneakoscope', 'missions', missionId, 'architecture-map');
24
+ }
25
+ function resolveMapDir(root, args) {
26
+ const missionId = readOption(args, '--mission', '') || null;
27
+ if (missionId) {
28
+ return { dir: missionMapRoot(root, missionId), mode: 'mission', missionId };
29
+ }
30
+ return { dir: mapRoot(root), mode: 'global', missionId: null };
31
+ }
32
+ function readJsonFile(file) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function loadManifest(dir) {
41
+ const file = path.join(dir, 'manifest.json');
42
+ if (!fs.existsSync(file))
43
+ return { ok: false, manifest: null, path: file };
44
+ const manifest = readJsonFile(file);
45
+ return { ok: Boolean(manifest && manifest.schema === ARCHITECTURE_MAP_MANIFEST_SCHEMA), manifest, path: file };
46
+ }
47
+ function loadFindings(dir) {
48
+ const file = path.join(dir, 'findings.json');
49
+ const doc = readJsonFile(file);
50
+ return Array.isArray(doc?.findings) ? doc.findings : [];
51
+ }
52
+ function countBySeverity(findings) {
53
+ let blocking = 0;
54
+ let warning = 0;
55
+ let info = 0;
56
+ for (const finding of findings) {
57
+ if (finding.severity === 'blocking')
58
+ blocking += 1;
59
+ else if (finding.severity === 'warning')
60
+ warning += 1;
61
+ else
62
+ info += 1;
63
+ }
64
+ return { blocking, warning, info };
65
+ }
66
+ function graphFreshness(root, manifest) {
67
+ if (!manifest)
68
+ return 'missing';
69
+ const graphPath = path.join(root, '.sneakoscope', 'wiki', 'context-graph.json');
70
+ if (!fs.existsSync(graphPath))
71
+ return 'missing';
72
+ const graph = readJsonFile(graphPath);
73
+ const expected = String(graph?.snapshotHash || '');
74
+ const observed = String(manifest.graphHash || manifest.sourceBinding?.graphHash || '');
75
+ if (!expected || !observed)
76
+ return 'missing';
77
+ return expected === observed ? 'fresh' : 'stale';
78
+ }
79
+ async function atlasStatus(root, args) {
80
+ const resolved = resolveMapDir(root, args);
81
+ const loaded = loadManifest(resolved.dir);
82
+ const findings = loaded.manifest ? loadFindings(resolved.dir) : [];
83
+ const severity = countBySeverity(findings);
84
+ const freshness = graphFreshness(root, loaded.manifest);
85
+ const ok = loaded.ok && freshness === 'fresh' && severity.blocking === 0;
86
+ const result = {
87
+ schema: 'sks.triwiki-atlas-status.v1',
88
+ ok,
89
+ mode: resolved.mode,
90
+ mission_id: resolved.missionId,
91
+ manifest_schema: loaded.manifest?.schema ?? null,
92
+ graph_hash: loaded.manifest?.graphHash ?? null,
93
+ policy_hash: loaded.manifest?.policyHash ?? null,
94
+ generated_from: loaded.manifest?.canonicalHash ?? null,
95
+ freshness,
96
+ view_count: loaded.manifest?.views?.length ?? 0,
97
+ blocking_count: severity.blocking,
98
+ warning_count: severity.warning,
99
+ projection_accounting: loaded.manifest?.projectionAccounting ?? null,
100
+ next_action: ok ? null : REPAIR_COMMAND,
101
+ repair_command: REPAIR_COMMAND
102
+ };
103
+ const lines = [
104
+ `Architecture map (${resolved.mode}): ${ok ? 'ok' : 'blocked'}`,
105
+ `Freshness: ${freshness}`,
106
+ `Views: ${result.view_count}; blocking=${severity.blocking} warning=${severity.warning}`,
107
+ ...(ok ? [] : [`Repair: ${REPAIR_COMMAND}`])
108
+ ];
109
+ return { result, ok, lines };
110
+ }
111
+ async function atlasLint(root, args) {
112
+ const resolved = resolveMapDir(root, args);
113
+ const loaded = loadManifest(resolved.dir);
114
+ const errors = [];
115
+ if (!loaded.manifest)
116
+ errors.push('manifest_missing');
117
+ else {
118
+ if (loaded.manifest.schema !== ARCHITECTURE_MAP_MANIFEST_SCHEMA)
119
+ errors.push('schema');
120
+ if (!loaded.manifest.graphHash)
121
+ errors.push('graph_hash');
122
+ if (!loaded.manifest.policyHash)
123
+ errors.push('policy_hash');
124
+ if (!Array.isArray(loaded.manifest.views) || loaded.manifest.views.length === 0)
125
+ errors.push('views');
126
+ if (!loaded.manifest.projectionAccounting)
127
+ errors.push('projection_accounting');
128
+ for (const view of loaded.manifest.views ?? []) {
129
+ if (!VIEW_ID_SET.has(view.viewId))
130
+ errors.push(`unknown_view:${view.viewId}`);
131
+ const viewPath = path.join(resolved.dir, 'views', `${view.viewId}.mmd`);
132
+ if (!fs.existsSync(viewPath)) {
133
+ errors.push(`missing_view_file:${view.viewId}`);
134
+ continue;
135
+ }
136
+ const text = fs.readFileSync(viewPath, 'utf8');
137
+ if (!text.startsWith('%% GENERATED BY SKS'))
138
+ errors.push(`header:${view.viewId}`);
139
+ if (!/\bflowchart (TD|LR)\b/.test(text))
140
+ errors.push(`grammar:${view.viewId}`);
141
+ if (/\/Users\/|\/home\/|C:\\\\/.test(text))
142
+ errors.push(`absolute_path:${view.viewId}`);
143
+ const rel = path.relative(resolved.dir, viewPath);
144
+ if (rel.startsWith('..') || path.isAbsolute(rel))
145
+ errors.push(`path_escape:${view.viewId}`);
146
+ }
147
+ }
148
+ const freshness = graphFreshness(root, loaded.manifest);
149
+ if (freshness !== 'fresh')
150
+ errors.push(`freshness_${freshness}`);
151
+ const ok = errors.length === 0;
152
+ const result = {
153
+ schema: 'sks.triwiki-atlas-lint.v1',
154
+ ok,
155
+ mode: resolved.mode,
156
+ mission_id: resolved.missionId,
157
+ error_count: errors.length,
158
+ errors,
159
+ repair_command: REPAIR_COMMAND
160
+ };
161
+ return {
162
+ result,
163
+ ok,
164
+ lines: [
165
+ `Architecture map lint: ${ok ? 'ok' : 'blocked'} (${errors.length} errors)`,
166
+ ...errors.slice(0, 20).map((error) => `- ${error}`),
167
+ ...(ok ? [] : [`Repair: ${REPAIR_COMMAND}`])
168
+ ]
169
+ };
170
+ }
171
+ async function atlasList(root, args) {
172
+ const resolved = resolveMapDir(root, args);
173
+ const loaded = loadManifest(resolved.dir);
174
+ if (!loaded.manifest) {
175
+ return {
176
+ result: {
177
+ schema: 'sks.triwiki-atlas-list.v1',
178
+ ok: false,
179
+ error_code: 'manifest_missing',
180
+ repair_command: REPAIR_COMMAND
181
+ },
182
+ ok: false,
183
+ lines: ['Architecture map list: manifest_missing', `Repair: ${REPAIR_COMMAND}`]
184
+ };
185
+ }
186
+ const views = (loaded.manifest.views ?? []).map((view) => {
187
+ const rel = `${ARCHITECTURE_MAP_DIR_REL}/views/${view.viewId}.mmd`;
188
+ const abs = path.join(resolved.dir, 'views', `${view.viewId}.mmd`);
189
+ let direction = null;
190
+ let nodeCount = 0;
191
+ let edgeCount = 0;
192
+ if (fs.existsSync(abs)) {
193
+ const text = fs.readFileSync(abs, 'utf8');
194
+ direction = text.match(/\bflowchart (TD|LR)\b/)?.[1] ?? null;
195
+ nodeCount = [...text.matchAll(/^\s*[A-Za-z0-9_]+\[/gm)].length;
196
+ edgeCount = [...text.matchAll(/-->/g)].length;
197
+ }
198
+ return {
199
+ view_id: view.viewId,
200
+ path: rel,
201
+ direction,
202
+ node_count: nodeCount,
203
+ edge_count: edgeCount,
204
+ byte_count: view.byteLength,
205
+ hash: view.contentHash
206
+ };
207
+ });
208
+ const result = {
209
+ schema: 'sks.triwiki-atlas-list.v1',
210
+ ok: true,
211
+ mode: resolved.mode,
212
+ mission_id: resolved.missionId,
213
+ views
214
+ };
215
+ return {
216
+ result,
217
+ ok: true,
218
+ lines: views.map((view) => `${view.view_id} ${view.direction ?? '?'} nodes=${view.node_count} edges=${view.edge_count} bytes=${view.byte_count} ${view.hash.slice(0, 12)}`)
219
+ };
220
+ }
221
+ async function atlasShow(root, args) {
222
+ const viewId = args.find((arg) => !arg.startsWith('-')) ?? '';
223
+ if (!viewId || !VIEW_ID_SET.has(viewId)) {
224
+ return {
225
+ result: {
226
+ schema: 'sks.triwiki-atlas-show.v1',
227
+ ok: false,
228
+ error_code: 'invalid_view_id',
229
+ allowed: [...ARCHITECTURE_MAP_VIEW_IDS]
230
+ },
231
+ ok: false,
232
+ lines: [
233
+ 'Usage: sks triwiki atlas-show <view-id> [--mission <id>] [--format mermaid|json]',
234
+ `Allowed: ${ARCHITECTURE_MAP_VIEW_IDS.join(', ')}`
235
+ ]
236
+ };
237
+ }
238
+ if (!GLOBAL_VIEW_ID_SET.has(viewId) && !readOption(args, '--mission', '')) {
239
+ return {
240
+ result: {
241
+ schema: 'sks.triwiki-atlas-show.v1',
242
+ ok: false,
243
+ error_code: 'mission_view_requires_mission',
244
+ view_id: viewId
245
+ },
246
+ ok: false,
247
+ lines: [`View ${viewId} is mission-scoped; pass --mission <id>`]
248
+ };
249
+ }
250
+ const format = readOption(args, '--format', 'mermaid') || 'mermaid';
251
+ const resolved = resolveMapDir(root, args);
252
+ const viewPath = path.join(resolved.dir, 'views', `${viewId}.mmd`);
253
+ const confined = path.relative(resolved.dir, viewPath);
254
+ if (confined.startsWith('..') || path.isAbsolute(confined)) {
255
+ return {
256
+ result: { schema: 'sks.triwiki-atlas-show.v1', ok: false, error_code: 'path_escape' },
257
+ ok: false,
258
+ lines: ['Architecture map show blocked: path_escape']
259
+ };
260
+ }
261
+ if (!fs.existsSync(viewPath)) {
262
+ return {
263
+ result: {
264
+ schema: 'sks.triwiki-atlas-show.v1',
265
+ ok: false,
266
+ error_code: 'view_missing',
267
+ view_id: viewId,
268
+ repair_command: REPAIR_COMMAND
269
+ },
270
+ ok: false,
271
+ lines: [`Architecture map show: view_missing (${viewId})`, `Repair: ${REPAIR_COMMAND}`]
272
+ };
273
+ }
274
+ const text = fs.readFileSync(viewPath, 'utf8');
275
+ if (format === 'json') {
276
+ const result = {
277
+ schema: 'sks.triwiki-atlas-show.v1',
278
+ ok: true,
279
+ view_id: viewId,
280
+ format: 'json',
281
+ path: `${ARCHITECTURE_MAP_DIR_REL}/views/${viewId}.mmd`,
282
+ text
283
+ };
284
+ return { result, ok: true, lines: [JSON.stringify(result, null, 2)] };
285
+ }
286
+ return {
287
+ result: {
288
+ schema: 'sks.triwiki-atlas-show.v1',
289
+ ok: true,
290
+ view_id: viewId,
291
+ format: 'mermaid',
292
+ path: `${ARCHITECTURE_MAP_DIR_REL}/views/${viewId}.mmd`,
293
+ text
294
+ },
295
+ ok: true,
296
+ lines: [text.trimEnd()]
297
+ };
298
+ }
299
+ async function atlasWhy(root, args) {
300
+ const findingId = args.find((arg) => !arg.startsWith('-')) ?? '';
301
+ const missionId = readOption(args, '--mission', '');
302
+ if (!findingId || !missionId) {
303
+ return {
304
+ result: { schema: 'sks.triwiki-atlas-why.v1', ok: false, error_code: 'usage' },
305
+ ok: false,
306
+ lines: ['Usage: sks triwiki atlas-why <finding-id> --mission <id> [--json]']
307
+ };
308
+ }
309
+ const resolved = resolveMapDir(root, [`--mission`, missionId]);
310
+ const findings = loadFindings(resolved.dir);
311
+ const finding = findings.find((entry) => entry.id === findingId) ?? null;
312
+ if (!finding) {
313
+ return {
314
+ result: {
315
+ schema: 'sks.triwiki-atlas-why.v1',
316
+ ok: false,
317
+ error_code: 'finding_not_found',
318
+ finding_id: findingId,
319
+ mission_id: missionId
320
+ },
321
+ ok: false,
322
+ lines: [`Architecture map why: finding_not_found (${findingId})`]
323
+ };
324
+ }
325
+ const result = {
326
+ schema: 'sks.triwiki-atlas-why.v1',
327
+ ok: true,
328
+ finding_id: finding.id,
329
+ code: finding.code ?? null,
330
+ severity: finding.severity ?? null,
331
+ subject: finding.subjectIds ?? [],
332
+ evidence_path: finding.evidenceIds ?? [],
333
+ before: null,
334
+ after: null,
335
+ policy_rule: finding.ruleId ?? null,
336
+ exception: finding.disposition ?? null,
337
+ inspect_nodes: finding.subjectIds ?? [],
338
+ message: finding.message ?? null
339
+ };
340
+ return {
341
+ result,
342
+ ok: true,
343
+ lines: [
344
+ `${finding.id}: ${finding.code ?? 'unknown'} (${finding.severity ?? 'info'})`,
345
+ ...(finding.subjectIds?.length ? [`Subject: ${finding.subjectIds.join(', ')}`] : []),
346
+ ...(finding.message ? [finding.message] : [])
347
+ ]
348
+ };
349
+ }
350
+ export async function triwikiAtlasCommand(root, sub, args) {
351
+ let answer;
352
+ if (sub === 'atlas-status')
353
+ answer = await atlasStatus(root, args);
354
+ else if (sub === 'atlas-lint')
355
+ answer = await atlasLint(root, args);
356
+ else if (sub === 'atlas-list')
357
+ answer = await atlasList(root, args);
358
+ else if (sub === 'atlas-show')
359
+ answer = await atlasShow(root, args);
360
+ else
361
+ answer = await atlasWhy(root, args);
362
+ const usageFailed = typeof answer.result === 'object' &&
363
+ answer.result !== null &&
364
+ 'error_code' in answer.result &&
365
+ answer.result.error_code === 'usage';
366
+ const invalidView = typeof answer.result === 'object' &&
367
+ answer.result !== null &&
368
+ 'error_code' in answer.result &&
369
+ answer.result.error_code === 'invalid_view_id';
370
+ const exitCode = answer.ok ? 0 : usageFailed || invalidView ? 2 : 1;
371
+ return { ...answer, exitCode };
372
+ }
@@ -5,6 +5,7 @@ import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.j
5
5
  import { buildTriWikiGateImpactMap } from '../triwiki/triwiki-gate-impact-map.js';
6
6
  import { DEFAULT_TRIWIKI_MODULE_CARDS } from '../triwiki/triwiki-module-card.js';
7
7
  import { summarizeTriWikiProofBank } from '../triwiki/triwiki-proof-bank.js';
8
+ import { isTriWikiAtlasSubcommand, triwikiAtlasCommand } from './triwiki-atlas-command.js';
8
9
  import { isTriWikiGraphSubcommand, triwikiGraphCommand } from './triwiki-graph-command.js';
9
10
  export async function triwikiCommand(args = []) {
10
11
  const root = await projectRoot();
@@ -20,6 +21,16 @@ export async function triwikiCommand(args = []) {
20
21
  console.log(line);
21
22
  return answer.result;
22
23
  }
24
+ if (isTriWikiAtlasSubcommand(sub)) {
25
+ const answer = await triwikiAtlasCommand(root, sub, args.slice(1));
26
+ process.exitCode = answer.exitCode;
27
+ if (json)
28
+ return printJson(answer.result);
29
+ const stream = answer.ok ? console.log : console.error;
30
+ for (const line of answer.lines)
31
+ stream(line);
32
+ return answer.result;
33
+ }
23
34
  if (sub === 'index') {
24
35
  result = {
25
36
  schema: 'sks.triwiki-index.v1',
@@ -36,7 +47,7 @@ export async function triwikiCommand(args = []) {
36
47
  result = summarizeTriWikiProofBank(root);
37
48
  }
38
49
  else {
39
- console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query [--json]');
50
+ console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query|atlas-status|atlas-lint|atlas-list|atlas-show|atlas-why [--json]');
40
51
  process.exitCode = 1;
41
52
  return null;
42
53
  }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
@@ -104,7 +105,21 @@ function phaseInputHash(root, id) {
104
105
  return { rel, hash: 'missing' };
105
106
  return hashSemanticPath(root, rel);
106
107
  });
107
- return hashJson({ id, package: packageIdentity(), files, env: phaseEnvPresence(id), semantic_state: phaseSemanticState(root, id), phase_schema_version: 2 });
108
+ return hashJson({
109
+ id,
110
+ package: packageIdentity(),
111
+ files,
112
+ env: phaseEnvPresence(id),
113
+ semantic_state: phaseSemanticState(root, id),
114
+ project_codex_config: hashSemanticPath(root, '.codex/config.toml').hash,
115
+ codex_home_config: hashFileIfExists(codexHomeConfigPath()),
116
+ phase_schema_version: 3
117
+ });
118
+ }
119
+ function codexHomeConfigPath() {
120
+ const codexHome = process.env.CODEX_HOME
121
+ || path.join(process.env.HOME || os.homedir(), '.codex');
122
+ return path.join(codexHome, 'config.toml');
108
123
  }
109
124
  function phaseInputFiles(id) {
110
125
  if (id.includes('context7'))
@@ -17,6 +17,8 @@ export async function runDoctorIdempotence(root) {
17
17
  blockers.push(`second_doctor_exit_${second.exit_code}`);
18
18
  if (changedFilesSecond.length > 0)
19
19
  blockers.push('second_doctor_not_noop');
20
+ if (!reportsChangedFiles(second))
21
+ blockers.push('second_doctor_changed_files_unavailable');
20
22
  const report = {
21
23
  schema: 'sks.doctor-idempotence.v1',
22
24
  ok: blockers.length === 0,
@@ -60,6 +62,15 @@ async function runDoctor(root, home, codexHome) {
60
62
  parsed
61
63
  };
62
64
  }
65
+ function reportsChangedFiles(run) {
66
+ if (!run.parsed)
67
+ return false;
68
+ return [
69
+ run.parsed?.repair?.doctor_transaction?.changed_files,
70
+ run.parsed?.doctor_fix_transaction?.changed_files,
71
+ run.parsed?.changed_files
72
+ ].some((value) => Array.isArray(value));
73
+ }
63
74
  function extractChangedFiles(run) {
64
75
  const repair = run.parsed?.repair || {};
65
76
  const candidates = [
@@ -1,4 +1,53 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { inspectOfficialSubagentToml, readOfficialSubagentConfig } from '../subagents/official-subagent-config.js';
1
5
  import {} from './doctor-transaction.js';
6
+ export async function verifyCodexConfigsOnDisk(input) {
7
+ const root = path.resolve(input.root);
8
+ const home = input.home || process.env.HOME || os.homedir();
9
+ const codexHome = input.codexHome || process.env.CODEX_HOME || path.join(home, '.codex');
10
+ const projectConfigPath = path.join(root, '.codex', 'config.toml');
11
+ const codexHomeConfigPath = path.join(codexHome, 'config.toml');
12
+ const blockers = [];
13
+ const [projectText, homeText] = await Promise.all([
14
+ fs.readFile(projectConfigPath, 'utf8').catch(() => null),
15
+ fs.readFile(codexHomeConfigPath, 'utf8').catch(() => null)
16
+ ]);
17
+ if (projectText !== null && !inspectOfficialSubagentToml(projectText).ok) {
18
+ blockers.push('project_codex_config_unparseable_after_repair');
19
+ }
20
+ if (homeText !== null && !inspectOfficialSubagentToml(homeText).ok) {
21
+ blockers.push('codex_home_config_unparseable_after_repair');
22
+ }
23
+ let multiAgentV2Enabled = null;
24
+ let agentsEnabled = null;
25
+ if (!blockers.length) {
26
+ const config = await readOfficialSubagentConfig(root, { home, codexHome }).catch(() => null);
27
+ if (config) {
28
+ multiAgentV2Enabled = config.multiAgentV2.enabled;
29
+ agentsEnabled = config.enabled;
30
+ for (const blocker of config.blockers)
31
+ blockers.push(`official_subagent_config:${blocker}`);
32
+ if (multiAgentV2Enabled === false) {
33
+ blockers.push('official_subagent_multi_agent_v2_disabled_after_repair');
34
+ if (agentsEnabled === false)
35
+ blockers.push('official_subagent_agents_disabled_after_repair');
36
+ }
37
+ }
38
+ }
39
+ return {
40
+ schema: 'sks.doctor-config-disk-verification.v1',
41
+ ok: blockers.length === 0,
42
+ project_config_path: projectConfigPath,
43
+ codex_home_config_path: codexHomeConfigPath,
44
+ project_config_present: projectText !== null,
45
+ codex_home_config_present: homeText !== null,
46
+ multi_agent_v2_enabled: multiAgentV2Enabled,
47
+ agents_enabled: agentsEnabled,
48
+ blockers: [...new Set(blockers)]
49
+ };
50
+ }
2
51
  export function doctorRepairPostcheck(transaction) {
3
52
  const phases = transaction?.phases || [];
4
53
  const requiredBlockers = phases
@@ -3,6 +3,47 @@ import { nowIso, writeJsonAtomic } from '../fsx.js';
3
3
  import { isDoctorPhaseClean, markDoctorPhaseClean } from './doctor-dirty-planner.js';
4
4
  import { ui as cliUi } from '../../cli/cli-theme.js';
5
5
  import { messageOf } from '../errors/message.js';
6
+ const CHANGED_PATH_KEYS = Object.freeze([
7
+ 'repaired_paths', 'created_files', 'created', 'updated', 'changed_files',
8
+ 'written_files', 'removed_files', 'removed', 'quarantined', 'backups'
9
+ ]);
10
+ export function changedFilesFromRepairReport(report) {
11
+ const found = new Set();
12
+ const walk = (value, depth) => {
13
+ if (depth > 6 || !value || typeof value !== 'object')
14
+ return;
15
+ if (Array.isArray(value)) {
16
+ for (const item of value)
17
+ walk(item, depth + 1);
18
+ return;
19
+ }
20
+ const row = value;
21
+ for (const key of CHANGED_PATH_KEYS) {
22
+ const entries = row[key];
23
+ if (Array.isArray(entries)) {
24
+ for (const entry of entries) {
25
+ const text = String(entry || '').trim();
26
+ if (text)
27
+ found.add(text);
28
+ }
29
+ }
30
+ else if (typeof entries === 'string' && entries.trim()) {
31
+ found.add(entries.trim());
32
+ }
33
+ }
34
+ if (row.changed === true || row.applied === true) {
35
+ for (const key of ['config_path', 'backup_path', 'path', 'file']) {
36
+ const text = String(row[key] || '').trim();
37
+ if (text)
38
+ found.add(text);
39
+ }
40
+ }
41
+ for (const nested of Object.values(row))
42
+ walk(nested, depth + 1);
43
+ };
44
+ walk(report, 0);
45
+ return [...found].sort();
46
+ }
6
47
  export async function runDoctorFixTransaction(input) {
7
48
  const startedAt = nowIso();
8
49
  const phases = [];
@@ -110,7 +151,8 @@ export async function writeDoctorFixTransaction(input) {
110
151
  started_at: phase.started_at || null,
111
152
  completed_at: phase.completed_at || null,
112
153
  duration_ms: Number.isFinite(phase.duration_ms) ? Number(phase.duration_ms) : null,
113
- rollback_performed: phase.rollback_performed === true
154
+ rollback_performed: phase.rollback_performed === true,
155
+ changed_files: [...new Set(phase.changed_files || [])].sort()
114
156
  }));
115
157
  const postcheckOk = phases.every((phase) => phase.ok || phase.required_for_ready === false);
116
158
  const mutationsWithoutRollback = phases.filter((phase) => phase.required_for_ready && phase.repaired && !phase.rollback_evidence).length;
@@ -124,6 +166,7 @@ export async function writeDoctorFixTransaction(input) {
124
166
  postcheck_ok: postcheckOk && mutationsWithoutRollback === 0,
125
167
  rollback_performed: input.rollbackPerformed === true,
126
168
  mutations_without_rollback: mutationsWithoutRollback,
169
+ changed_files: [...new Set(phases.flatMap((phase) => phase.changed_files))].sort(),
127
170
  raw_secret_values_recorded: false,
128
171
  skipped_clean_phases: phases.filter((phase) => phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
129
172
  dirty_phases: input.dirtyPlan?.phases.filter((phase) => phase.status === 'dirty').map((phase) => phase.id) || phases.filter((phase) => !phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
@@ -158,7 +201,9 @@ function normalizePhase(definition, result, fallback, startedMs) {
158
201
  started_at: phase.started_at || fallback.started_at || nowIso(),
159
202
  completed_at: phase.completed_at || nowIso(),
160
203
  duration_ms: phase.duration_ms ?? Math.max(0, Date.now() - startedMs),
161
- rollback_performed: phase.rollback_performed === true
204
+ rollback_performed: phase.rollback_performed === true,
205
+ changed_files: phase.changed_files
206
+ ?? (definition.report ? changedFilesFromRepairReport(definition.report()) : [])
162
207
  };
163
208
  }
164
209
  function mergePhase(phase, update) {
@@ -144,7 +144,7 @@ function projectSksHookCommand(value) {
144
144
  const command = String(value || '').trim();
145
145
  if (/[;&|`$<>\r\n]/.test(command))
146
146
  return null;
147
- return command.match(/^node\s+\.\/dist\/bin\/sks\.js\s+hook\s+([a-z0-9-]+)$/i)?.[1] || null;
147
+ return command.match(/^(?:\S*[\\/])?(?:node\s+\S*sks\.js|sks)\s+hook\s+([a-z0-9-]+)$/i)?.[1] || null;
148
148
  }
149
149
  function hookRef(event, matcher, command) {
150
150
  return `${event}:${String(matcher ?? '')}:${command}`;