knosky 0.6.3 → 0.7.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 (64) hide show
  1. package/CHANGELOG.md +149 -93
  2. package/CREDITS.md +14 -14
  3. package/LICENSE.md +76 -76
  4. package/LIMITATIONS.md +33 -23
  5. package/PRIVACY.md +30 -30
  6. package/README.md +170 -117
  7. package/SECURITY.md +78 -46
  8. package/action/post-comment.mjs +94 -89
  9. package/action.yml +62 -62
  10. package/bin/knosky.mjs +279 -105
  11. package/core/CONTRACT.md +70 -70
  12. package/core/append-only-checkpoint.mjs +215 -0
  13. package/core/audit-writer.mjs +317 -0
  14. package/core/benchmark-results.mjs +225 -225
  15. package/core/bundle.mjs +178 -178
  16. package/core/churn.mjs +23 -23
  17. package/core/ci.mjs +268 -268
  18. package/core/comparison.mjs +189 -189
  19. package/core/config.mjs +189 -189
  20. package/core/constants.mjs +13 -13
  21. package/core/contract.mjs +123 -123
  22. package/core/cross-repo.mjs +111 -111
  23. package/core/decision-codes.mjs +92 -0
  24. package/core/destination.mjs +161 -161
  25. package/core/district-classification.mjs +111 -0
  26. package/core/doctor-scorecard.mjs +369 -0
  27. package/core/domain-store.mjs +347 -0
  28. package/core/edges.mjs +43 -43
  29. package/core/escalate.mjs +68 -68
  30. package/core/freshness.mjs +198 -194
  31. package/core/fs-indexer.mjs +218 -218
  32. package/core/key-store.mjs +348 -348
  33. package/core/layout.mjs +46 -46
  34. package/core/ledger.mjs +176 -141
  35. package/core/local-ipc-identity.mjs +500 -0
  36. package/core/lod.mjs +155 -155
  37. package/core/mode-b.mjs +410 -0
  38. package/core/multi-model-benchmark.mjs +405 -405
  39. package/core/net-lockdown.mjs +421 -0
  40. package/core/onboarding.mjs +223 -223
  41. package/core/operator-auth.mjs +317 -0
  42. package/core/overlays.mjs +45 -45
  43. package/core/policy-lattice.mjs +142 -0
  44. package/core/pr-comment.mjs +198 -198
  45. package/core/protocol-spec.mjs +460 -460
  46. package/core/provenance.mjs +320 -0
  47. package/core/retrieve.mjs +63 -63
  48. package/core/route.mjs +304 -304
  49. package/core/schema.mjs +275 -275
  50. package/core/signing-tiers.mjs +1265 -0
  51. package/core/swarm-bench.mjs +106 -0
  52. package/core/swarm-coordinator.mjs +867 -0
  53. package/core/trust-root-rekey.mjs +410 -0
  54. package/mcp/server.mjs +264 -108
  55. package/package.json +56 -46
  56. package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
  57. package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
  58. package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
  59. package/renderer/art/kenney/sheet_allCars.xml +545 -545
  60. package/renderer/build-rich.mjs +43 -43
  61. package/renderer/city.template.html +808 -808
  62. package/ssot/decision-codes.json +133 -0
  63. package/ssot/ladder-l0-l3.md +232 -0
  64. package/ssot/tool-menu.json +130 -0
package/action.yml CHANGED
@@ -1,62 +1,62 @@
1
- name: 'KnoSky PR-GPS'
2
- description: >-
3
- Advisory, metadata-only navigation report posted on pull requests. Reads
4
- file/folder/import structure only (never uploads code bodies), suggests
5
- where to start reviewing a PR, and flags related tests/docs. Never blocks
6
- or gates the build.
7
- author: 'SathiaAI'
8
- branding:
9
- icon: 'compass'
10
- color: 'blue'
11
-
12
- inputs:
13
- base:
14
- description: 'Base commit SHA to diff from (e.g. github.event.pull_request.base.sha).'
15
- required: true
16
- head:
17
- description: 'Head commit SHA to diff to (e.g. github.event.pull_request.head.sha).'
18
- required: true
19
- root:
20
- description: 'Path to the checked-out repo to index.'
21
- required: false
22
- default: '.'
23
- github-token:
24
- description: 'Token used to post/update the PR-GPS comment. Defaults to the workflow token.'
25
- required: false
26
- default: ${{ github.token }}
27
- fail-on-secret:
28
- description: >-
29
- If "true", fail this step when a secret-like pattern is found in the
30
- emitted advisory artifacts themselves (never in your source tree).
31
- Defaults to "false" — this action is advisory-only by design.
32
- required: false
33
- default: 'false'
34
-
35
- runs:
36
- using: 'composite'
37
- steps:
38
- - name: Set up Node for the KnoSky action itself
39
- uses: actions/setup-node@v4
40
- with:
41
- node-version: '20'
42
-
43
- - name: Build the KnoSky index (best-effort — never fails the build)
44
- shell: bash
45
- run: |
46
- node "${{ github.action_path }}/core/fs-indexer.mjs" \
47
- --root "${{ inputs.root }}" \
48
- --out "${{ inputs.root }}/.knosky-ci-city.json" \
49
- --share-safe || true
50
-
51
- - name: Run KnoSky PR-GPS and post/update the PR comment
52
- shell: bash
53
- env:
54
- GITHUB_TOKEN: ${{ inputs.github-token }}
55
- KC_REPO: ${{ github.repository }}
56
- KC_PR_NUMBER: ${{ github.event.pull_request.number }}
57
- KC_BASE: ${{ inputs.base }}
58
- KC_HEAD: ${{ inputs.head }}
59
- KC_ROOT: ${{ inputs.root }}
60
- KC_CITY: ${{ inputs.root }}/.knosky-ci-city.json
61
- KC_FAIL_ON_SECRET: ${{ inputs.fail-on-secret }}
62
- run: node "${{ github.action_path }}/action/post-comment.mjs"
1
+ name: 'KnoSky PR-GPS'
2
+ description: >-
3
+ Advisory, metadata-only navigation report posted on pull requests. Reads
4
+ file/folder/import structure only (never uploads code bodies), suggests
5
+ where to start reviewing a PR, and flags related tests/docs. Never blocks
6
+ or gates the build.
7
+ author: 'SathiaAI'
8
+ branding:
9
+ icon: 'compass'
10
+ color: 'blue'
11
+
12
+ inputs:
13
+ base:
14
+ description: 'Base commit SHA to diff from (e.g. github.event.pull_request.base.sha).'
15
+ required: true
16
+ head:
17
+ description: 'Head commit SHA to diff to (e.g. github.event.pull_request.head.sha).'
18
+ required: true
19
+ root:
20
+ description: 'Path to the checked-out repo to index.'
21
+ required: false
22
+ default: '.'
23
+ github-token:
24
+ description: 'Token used to post/update the PR-GPS comment. Defaults to the workflow token.'
25
+ required: false
26
+ default: ${{ github.token }}
27
+ fail-on-secret:
28
+ description: >-
29
+ If "true", fail this step when a secret-like pattern is found in the
30
+ emitted advisory artifacts themselves (never in your source tree).
31
+ Defaults to "false" — this action is advisory-only by design.
32
+ required: false
33
+ default: 'false'
34
+
35
+ runs:
36
+ using: 'composite'
37
+ steps:
38
+ - name: Set up Node for the KnoSky action itself
39
+ uses: actions/setup-node@v4
40
+ with:
41
+ node-version: '20'
42
+
43
+ - name: Build the KnoSky index (best-effort — never fails the build)
44
+ shell: bash
45
+ run: |
46
+ node "${{ github.action_path }}/core/fs-indexer.mjs" \
47
+ --root "${{ inputs.root }}" \
48
+ --out "${{ inputs.root }}/.knosky-ci-city.json" \
49
+ --share-safe || true
50
+
51
+ - name: Run KnoSky PR-GPS and post/update the PR comment
52
+ shell: bash
53
+ env:
54
+ GITHUB_TOKEN: ${{ inputs.github-token }}
55
+ KC_REPO: ${{ github.repository }}
56
+ KC_PR_NUMBER: ${{ github.event.pull_request.number }}
57
+ KC_BASE: ${{ inputs.base }}
58
+ KC_HEAD: ${{ inputs.head }}
59
+ KC_ROOT: ${{ inputs.root }}
60
+ KC_CITY: ${{ inputs.root }}/.knosky-ci-city.json
61
+ KC_FAIL_ON_SECRET: ${{ inputs.fail-on-secret }}
62
+ run: node "${{ github.action_path }}/action/post-comment.mjs"
package/bin/knosky.mjs CHANGED
@@ -1,106 +1,280 @@
1
- #!/usr/bin/env node
2
- // KnoSky one-command launcher: index a folder -> build the city -> open it ->
3
- // print the MCP config + first prompts -> start the local MCP server.
4
- // Reuses the verified core/renderer/mcp scripts as child processes (no new logic to trust).
5
- //
6
- // npx knosky [path] # default: current folder
7
- // npx knosky . --no-open # don't auto-open the browser
8
- // npx knosky . --no-serve # build + print config, don't start the MCP server
9
- import { spawn, spawnSync } from 'node:child_process';
10
- import path from 'node:path';
11
- import fs from 'node:fs';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- const HERE = path.dirname(fileURLToPath(import.meta.url));
15
- const ROOT = path.resolve(HERE, '..');
16
- const argv = process.argv.slice(2);
17
- const flags = new Set(argv.filter(a => a.startsWith('--')));
18
- const target = path.resolve(argv.find(a => !a.startsWith('--')) || '.');
19
- const NODE = process.execPath;
20
-
21
- // ---------------------------------------------------------------------------
22
- // ci subcommand: generate PR-GPS advisory artifacts (advisory, never breaks builds)
23
- // ---------------------------------------------------------------------------
24
- if (argv.find(a => !a.startsWith('--')) === 'ci') {
25
- const { knoskyCi } = await import('../core/ci.mjs');
26
-
27
- // Resolve a named flag's value from argv. Supports --flag=value and --flag value.
28
- const getArgVal = (name) => {
29
- const prefix = name + '=';
30
- const eq = argv.find(a => a.startsWith(prefix));
31
- if (eq !== undefined) return eq.slice(prefix.length);
32
- const idx = argv.indexOf(name);
33
- if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) {
34
- return argv[idx + 1];
35
- }
36
- return undefined;
37
- };
38
-
39
- const ciBase = getArgVal('--base');
40
- const ciHead = getArgVal('--head');
41
- const ciCity = getArgVal('--city');
42
- const ciFailOnSecret = flags.has('--fail-on-secret');
43
-
44
- const { exitCode, summaryMd, routeJson, safetyJson } = await knoskyCi({
45
- root: process.cwd(),
46
- base: ciBase,
47
- head: ciHead,
48
- cityPath: ciCity,
49
- failOnSecret: ciFailOnSecret,
50
- });
51
-
52
- fs.writeFileSync('knosky-pr-summary.md', summaryMd, 'utf8');
53
- fs.writeFileSync('knosky-pr-route.json', JSON.stringify(routeJson, null, 2) + '\n', 'utf8');
54
- fs.writeFileSync('knosky-safety-report.json', JSON.stringify(safetyJson, null, 2) + '\n', 'utf8');
55
-
56
- console.log(summaryMd);
57
- process.exit(exitCode);
58
- }
59
-
60
- if (!fs.existsSync(target)) { console.error('KnoSky: path not found: ' + target); process.exit(1); }
61
-
62
- const outDir = path.join(target, '.knosky');
63
- fs.mkdirSync(outDir, { recursive: true });
64
- const cityJson = path.join(outDir, 'city-data.json');
65
- const cityHtml = path.join(outDir, 'city.html');
66
-
67
- const run = (script, args) => spawnSync(NODE, [path.join(ROOT, script), ...args], { stdio: 'inherit' });
68
-
69
- console.log('\nKnoSky -> indexing ' + target);
70
- let r = run('core/fs-indexer.mjs', ['--root', target, '--out', cityJson, '--share-safe']);
71
- if (r.status !== 0) { console.error('\nKnoSky: indexing was blocked or failed (see above). Nothing was opened.'); process.exit(r.status || 1); }
72
-
73
- r = run('renderer/build-rich.mjs', [cityJson, cityHtml]);
74
- if (r.status !== 0) { console.error('\nKnoSky: building the city failed.'); process.exit(1); }
75
-
76
- if (!flags.has('--no-open')) {
77
- const isWin = process.platform === 'win32', isMac = process.platform === 'darwin';
78
- const cmd = isWin ? 'cmd' : isMac ? 'open' : 'xdg-open';
79
- const a = isWin ? ['/c', 'start', '', cityHtml] : [cityHtml];
80
- try { spawn(cmd, a, { detached: true, stdio: 'ignore' }).unref(); } catch (_) {}
81
- }
82
-
83
- const mcpServer = path.join(ROOT, 'mcp', 'server.mjs');
84
- const cfg = JSON.stringify({ mcpServers: { knosky: { command: 'node', args: [mcpServer], env: { KC_CITY: cityJson } } } }, null, 2);
85
-
86
- console.log('\n City: ' + cityHtml);
87
- console.log('\nConnect your AI assistant so it answers from THIS repo, with citations.');
88
- console.log('Claude Code:');
89
- console.log(' claude mcp add knosky -e KC_CITY="' + cityJson + '" -- node "' + mcpServer + '"');
90
- console.log('\nClaude Desktop / Cursor / VS Code MCP config:');
91
- console.log(cfg.split('\n').map(l => ' ' + l).join('\n'));
92
- console.log('\nThen try one of these to see the loop (grounded, cited answers):');
93
- console.log(' - "Using KnoSky, where does authentication live in this repo?"');
94
- console.log(' - "Using KnoSky, what are the entry points of this project?"');
95
- console.log(' - "Using KnoSky, which files should I read to understand billing?"');
96
-
97
- if (flags.has('--no-serve')) {
98
- console.log('\n(--no-serve) Not starting the MCP server. Run it later with:');
99
- console.log(' KC_CITY="' + cityJson + '" node "' + mcpServer + '"\n');
100
- process.exit(0);
101
- }
102
-
103
- console.log('\nStarting the local MCP server (Ctrl+C to stop)...\n');
104
- const mcp = spawn(NODE, [mcpServer, cityJson], { stdio: 'inherit' });
105
- mcp.on('exit', c => process.exit(c || 0));
1
+ #!/usr/bin/env node
2
+ // KnoSky one-command launcher: index a folder -> build the city -> open it ->
3
+ // print the MCP config + first prompts -> start the local MCP server.
4
+ // Reuses the verified core/renderer/mcp scripts as child processes (no new logic to trust).
5
+ //
6
+ // npx knosky [path] # default: current folder
7
+ // npx knosky . --no-open # don't auto-open the browser
8
+ // npx knosky . --no-serve # build + print config, don't start the MCP server
9
+ import { spawn, spawnSync } from 'node:child_process';
10
+ import path from 'node:path';
11
+ import fs from 'node:fs';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
15
+ const ROOT = path.resolve(HERE, '..');
16
+ const argv = process.argv.slice(2);
17
+ const flags = new Set(argv.filter(a => a.startsWith('--')));
18
+ const NODE = process.execPath;
19
+ const subcommand = argv.find(a => !a.startsWith('--'));
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // doctor subcommand: surface security/sandbox status for operator review (F0.5)
23
+ // Dispatched BEFORE `target` is resolved (PR #60 Architect finding): `target`
24
+ // used to be computed unconditionally first, so `knosky doctor` would resolve
25
+ // 'doctor' as a filesystem path -- harmless only because this branch exits
26
+ // before `target` is ever read. Reordered so that stays true by construction.
27
+ // ---------------------------------------------------------------------------
28
+ if (subcommand === 'agent-register') {
29
+ // Mode B: register local agent + mint lease in .knosky domain.
30
+ // Elevated classes require TWO operator tokens (quorum).
31
+ // Bootstrap: --bootstrap-operator prints operator-a only; operator-b to 0600 file.
32
+ const { loadDomain, registerAgentWithLease, resolveDomainRoot } = await import('../core/domain-store.mjs');
33
+ const { bootstrapOperator } = await import('../core/operator-auth.mjs');
34
+ const getArgVal = (name) => {
35
+ const prefix = name + '=';
36
+ const eq = argv.find(a => a.startsWith(prefix));
37
+ if (eq !== undefined) return eq.slice(prefix.length);
38
+ const idx = argv.indexOf(name);
39
+ if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) return argv[idx + 1];
40
+ return undefined;
41
+ };
42
+ const domainRoot = resolveDomainRoot(undefined, getArgVal('--domain'));
43
+ if (flags.has('--bootstrap-operator')) {
44
+ if (flags.has('--allow-single-operator')) {
45
+ console.error(JSON.stringify({
46
+ ok: false,
47
+ reason: 'allow_single_operator_disabled_for_elevated_guarantee',
48
+ next_action: 'Use dual bootstrap (default). Single-op cannot elevate classes; dual is required for Rule-3 quorum. If you truly need a single identity for public/internal only domain explore later via addOperator.',
49
+ }, null, 2));
50
+ process.exit(2);
51
+ }
52
+ const boot = bootstrapOperator(domainRoot, {
53
+ operatorId: getArgVal('--operator-id') || 'operator-a',
54
+ operatorId2: getArgVal('--operator-id-2') || 'operator-b',
55
+ });
56
+ // Dual separation: only operator-a raw token on stdout.
57
+ // operator-b is file-only (tokenFileB path + fingerprint in response — not its secret).
58
+ console.log(JSON.stringify({
59
+ domain: domainRoot,
60
+ ok: boot.ok,
61
+ mode: boot.mode,
62
+ reason: boot.reason,
63
+ operatorId: boot.operatorId,
64
+ operatorToken: boot.operatorToken,
65
+ fingerprint: boot.fingerprint,
66
+ operatorId2: boot.operatorId2,
67
+ fingerprint2: boot.fingerprint2,
68
+ tokenFileB: boot.tokenFileB,
69
+ tokenB_delivery: boot.tokenB_delivery,
70
+ warning: boot.warning,
71
+ }, null, 2));
72
+ process.exit(boot.ok ? 0 : 1);
73
+ }
74
+ const agentId = getArgVal('--agent') || getArgVal('--id') || 'local-agent';
75
+ const domain = loadDomain(domainRoot);
76
+ const classes = (getArgVal('--classes') || 'public,internal').split(',').map(s => s.trim()).filter(Boolean);
77
+ // Flag names are plain CLI text. Token values come only from argv/env (never hardcoded).
78
+ const operatorToken = getArgVal('--operator-token') || process.env.KC_OPERATOR_TOKEN;
79
+ const operatorToken2 = getArgVal('--operator-token-2') || process.env.KC_OPERATOR_TOKEN_2;
80
+ const out = registerAgentWithLease(
81
+ domain,
82
+ { agentId, classes, role: getArgVal('--role') || 'coder' },
83
+ { operatorToken, operatorToken2 },
84
+ );
85
+ if (!out.ok) {
86
+ console.error(JSON.stringify({ domain: domainRoot, ...out }, null, 2));
87
+ process.exit(1);
88
+ }
89
+ console.log(JSON.stringify({
90
+ ok: true,
91
+ domain: domainRoot,
92
+ agentId: out.agentId,
93
+ leaseId: out.leaseId,
94
+ hint: 'Pass leaseId to kc_route / kc_policy_check / kc_bundle (Mode B). Elevated classes need --operator-token AND --operator-token-2 (quorum).',
95
+ }, null, 2));
96
+ process.exit(0);
97
+ }
98
+
99
+ if (subcommand === 'doctor') {
100
+ const { doctorScorecardLines, buildDoctorScorecard, doctorExitCode } = await import('../core/doctor-scorecard.mjs');
101
+ const getArgVal = (name) => {
102
+ const prefix = name + '=';
103
+ const eq = argv.find(a => a.startsWith(prefix));
104
+ if (eq !== undefined) return eq.slice(prefix.length);
105
+ const idx = argv.indexOf(name);
106
+ if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) return argv[idx + 1];
107
+ return undefined;
108
+ };
109
+ const domain = getArgVal('--domain');
110
+ const city = getArgVal('--city');
111
+ const json = flags.has('--json');
112
+ const card = buildDoctorScorecard({ domainRoot: domain, cityPath: city });
113
+ if (json) {
114
+ console.log(JSON.stringify(card, null, 2));
115
+ } else {
116
+ console.log('');
117
+ for (const line of doctorScorecardLines({ domainRoot: domain, cityPath: city })) console.log(line);
118
+ console.log('');
119
+ }
120
+ process.exit(doctorExitCode(card));
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // swarm subcommand: L3 operator heatmap / status (DEC-113 thin floor)
125
+ // knosky swarm status [--domain <path>]
126
+ // knosky swarm bench [--domain <path>]
127
+ // ---------------------------------------------------------------------------
128
+ if (subcommand === 'swarm') {
129
+ const { readSwarmHeatmap, createSwarmCoordinator } = await import('../core/swarm-coordinator.mjs');
130
+ const { resolveDomainRoot } = await import('../core/domain-store.mjs');
131
+ const getArgVal = (name) => {
132
+ const prefix = name + '=';
133
+ const eq = argv.find(a => a.startsWith(prefix));
134
+ if (eq !== undefined) return eq.slice(prefix.length);
135
+ const idx = argv.indexOf(name);
136
+ if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) return argv[idx + 1];
137
+ return undefined;
138
+ };
139
+ const action = argv.find((a, i) => i > 0 && !a.startsWith('--') && a !== 'swarm') || 'status';
140
+ const domainRoot = resolveDomainRoot(getArgVal('--city'), getArgVal('--domain'));
141
+
142
+ if (action === 'status') {
143
+ let out = readSwarmHeatmap(domainRoot);
144
+ if (!out.ok) {
145
+ // Build a live snapshot so status is useful before any swarm traffic.
146
+ const coord = createSwarmCoordinator({ domainRoot });
147
+ const wr = coord.writeHeatmap();
148
+ out = { ok: true, path: wr.path, domainRoot, heatmap: wr.snapshot, note: 'fresh_snapshot' };
149
+ }
150
+ console.log(JSON.stringify(out, null, 2));
151
+ process.exit(out.ok ? 0 : 1);
152
+ }
153
+
154
+ if (action === 'bench') {
155
+ const { runSwarmBench } = await import('../core/swarm-bench.mjs');
156
+ const out = runSwarmBench({ domainRoot });
157
+ console.log(JSON.stringify(out, null, 2));
158
+ process.exit(out.ok ? 0 : 1);
159
+ }
160
+
161
+ console.error('KnoSky swarm: unknown action "' + action + '". Try: knosky swarm status | knosky swarm bench');
162
+ process.exit(2);
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // ci subcommand: generate PR-GPS advisory artifacts (advisory, never breaks builds)
167
+ // ---------------------------------------------------------------------------
168
+ if (subcommand === 'ci') {
169
+ const { knoskyCi } = await import('../core/ci.mjs');
170
+
171
+ // Resolve a named flag's value from argv. Supports --flag=value and --flag value.
172
+ const getArgVal = (name) => {
173
+ const prefix = name + '=';
174
+ const eq = argv.find(a => a.startsWith(prefix));
175
+ if (eq !== undefined) return eq.slice(prefix.length);
176
+ const idx = argv.indexOf(name);
177
+ if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) {
178
+ return argv[idx + 1];
179
+ }
180
+ return undefined;
181
+ };
182
+
183
+ const ciBase = getArgVal('--base');
184
+ const ciHead = getArgVal('--head');
185
+ const ciCity = getArgVal('--city');
186
+ const ciFailOnSecret = flags.has('--fail-on-secret');
187
+
188
+ const { exitCode, summaryMd, routeJson, safetyJson } = await knoskyCi({
189
+ root: process.cwd(),
190
+ base: ciBase,
191
+ head: ciHead,
192
+ cityPath: ciCity,
193
+ failOnSecret: ciFailOnSecret,
194
+ });
195
+
196
+ fs.writeFileSync('knosky-pr-summary.md', summaryMd, 'utf8');
197
+ fs.writeFileSync('knosky-pr-route.json', JSON.stringify(routeJson, null, 2) + '\n', 'utf8');
198
+ fs.writeFileSync('knosky-safety-report.json', JSON.stringify(safetyJson, null, 2) + '\n', 'utf8');
199
+
200
+ console.log(summaryMd);
201
+ process.exit(exitCode);
202
+ }
203
+
204
+ // Main path (index -> build -> open -> serve): resolve the target folder only
205
+ // here, after both early-exit subcommands, so a subcommand name is never
206
+ // mistaken for a path.
207
+ const target = path.resolve(subcommand || '.');
208
+
209
+ if (!fs.existsSync(target)) { console.error('KnoSky: path not found: ' + target); process.exit(1); }
210
+
211
+ const outDir = path.join(target, '.knosky');
212
+ fs.mkdirSync(outDir, { recursive: true });
213
+ const cityJson = path.join(outDir, 'city-data.json');
214
+ const cityHtml = path.join(outDir, 'city.html');
215
+
216
+ const run = (script, args) => spawnSync(NODE, [path.join(ROOT, script), ...args], { stdio: 'inherit' });
217
+
218
+ console.log('\nKnoSky -> indexing ' + target);
219
+ let r = run('core/fs-indexer.mjs', ['--root', target, '--out', cityJson, '--share-safe']);
220
+ if (r.status !== 0) { console.error('\nKnoSky: indexing was blocked or failed (see above). Nothing was opened.'); process.exit(r.status || 1); }
221
+
222
+ r = run('renderer/build-rich.mjs', [cityJson, cityHtml]);
223
+ if (r.status !== 0) { console.error('\nKnoSky: building the city failed.'); process.exit(1); }
224
+
225
+ if (!flags.has('--no-open')) {
226
+ const isWin = process.platform === 'win32', isMac = process.platform === 'darwin';
227
+ const cmd = isWin ? 'cmd' : isMac ? 'open' : 'xdg-open';
228
+ const a = isWin ? ['/c', 'start', '', cityHtml] : [cityHtml];
229
+ try { spawn(cmd, a, { detached: true, stdio: 'ignore' }).unref(); } catch (_) {}
230
+ }
231
+
232
+ const mcpServer = path.join(ROOT, 'mcp', 'server.mjs');
233
+ const cfg = JSON.stringify({ mcpServers: { knosky: { command: 'node', args: [mcpServer], env: { KC_CITY: cityJson } } } }, null, 2);
234
+
235
+ console.log('\n City: ' + cityHtml);
236
+ console.log('\nConnect your AI assistant so it answers from THIS repo, with citations.');
237
+ console.log('Claude Code:');
238
+ console.log(' claude mcp add knosky -e KC_CITY="' + cityJson + '" -- node "' + mcpServer + '"');
239
+ console.log('\nClaude Desktop / Cursor / VS Code MCP config:');
240
+ console.log(cfg.split('\n').map(l => ' ' + l).join('\n'));
241
+ console.log('\nThen try one of these to see the loop (grounded, cited answers):');
242
+ console.log(' - "Using KnoSky, where does authentication live in this repo?"');
243
+ console.log(' - "Using KnoSky, what are the entry points of this project?"');
244
+ console.log(' - "Using KnoSky, which files should I read to understand billing?"');
245
+
246
+ if (flags.has('--no-serve')) {
247
+ console.log('\n(--no-serve) Not starting the MCP server. Run it later with:');
248
+ console.log(' KC_CITY="' + cityJson + '" node "' + mcpServer + '"\n');
249
+ process.exit(0);
250
+ }
251
+
252
+ console.log('\nStarting the local MCP server (Ctrl+C to stop)...\n');
253
+ // F0.5: place the evaluator-facing MCP server inside the OS-level network
254
+ // lockdown when the platform supports it (core/net-lockdown.mjs).
255
+ // PR #60 Architect finding: this import must not be allowed to crash the CLI
256
+ // (or worse, silently degrade) if net-lockdown.mjs is ever missing/broken --
257
+ // explicitly caught, always warned, same fallback path as "tool not found".
258
+ let _lockdownPrefix = [];
259
+ try {
260
+ const { wrapArgsForLockdown } = await import('../core/net-lockdown.mjs');
261
+ _lockdownPrefix = wrapArgsForLockdown();
262
+ } catch (err) {
263
+ console.error(
264
+ 'KnoSky: could not load the network lockdown module (' + (err?.message || err) + ') '
265
+ + '-- MCP server will run WITHOUT the network lockdown.'
266
+ );
267
+ }
268
+ if (_lockdownPrefix.length === 0) {
269
+ // PR #60 QA finding: don't silently run unwrapped -- tell the operator.
270
+ console.error(
271
+ 'KnoSky: no OS-level network sandbox available on this platform/environment '
272
+ + '(sandbox-exec/unshare not found) -- MCP server will run WITHOUT the network '
273
+ + 'lockdown. Run `knosky doctor` for details.'
274
+ );
275
+ }
276
+ const mcp = _lockdownPrefix.length
277
+ ? spawn(_lockdownPrefix[0], [..._lockdownPrefix.slice(1), NODE, mcpServer, cityJson], { stdio: 'inherit' })
278
+ : spawn(NODE, [mcpServer, cityJson], { stdio: 'inherit' });
279
+ mcp.on('exit', c => process.exit(c || 0));
106
280
  process.on('SIGINT', () => { try { mcp.kill(); } catch (_) {} process.exit(0); });