@njeybe/nativ 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +26 -0
  3. package/dist/commands/db/db-formatter.d.ts +5 -0
  4. package/dist/commands/db/db-formatter.js +116 -0
  5. package/dist/commands/db/db-introspect.d.ts +11 -0
  6. package/dist/commands/db/db-introspect.js +84 -0
  7. package/dist/commands/db.js +4 -206
  8. package/dist/commands/profile.d.ts +3 -0
  9. package/dist/commands/profile.js +39 -0
  10. package/dist/commands/task/task-add.d.ts +11 -0
  11. package/dist/commands/task/task-add.js +169 -0
  12. package/dist/commands/task/task-common.d.ts +57 -0
  13. package/dist/commands/task/task-common.js +45 -0
  14. package/dist/commands/task/task-escalate.d.ts +7 -0
  15. package/dist/commands/task/task-escalate.js +119 -0
  16. package/dist/commands/task/task-lifecycle.d.ts +5 -0
  17. package/dist/commands/task/task-lifecycle.js +252 -0
  18. package/dist/commands/task/task-query.d.ts +10 -0
  19. package/dist/commands/task/task-query.js +269 -0
  20. package/dist/commands/task.d.ts +9 -87
  21. package/dist/commands/task.js +8 -850
  22. package/dist/commands/triage/triage-store.d.ts +56 -0
  23. package/dist/commands/triage/triage-store.js +108 -0
  24. package/dist/commands/triage/triage-ui.d.ts +14 -0
  25. package/dist/commands/triage/triage-ui.js +104 -0
  26. package/dist/commands/triage.d.ts +3 -52
  27. package/dist/commands/triage.js +8 -216
  28. package/dist/core/enforcement.d.ts +1 -0
  29. package/dist/core/enforcement.js +19 -0
  30. package/dist/core/human-gate.d.ts +2 -1
  31. package/dist/core/human-gate.js +9 -2
  32. package/dist/core/profiles.d.ts +14 -0
  33. package/dist/core/profiles.js +82 -0
  34. package/dist/core/setup/config-merger.d.ts +59 -0
  35. package/dist/core/setup/config-merger.js +107 -0
  36. package/dist/core/setup/session-context.d.ts +4 -0
  37. package/dist/core/setup/session-context.js +65 -0
  38. package/dist/core/setup/setup-planner.d.ts +14 -0
  39. package/dist/core/setup/setup-planner.js +180 -0
  40. package/dist/core/setup/template-planner.d.ts +8 -0
  41. package/dist/core/setup/template-planner.js +32 -0
  42. package/dist/core/setup/types.d.ts +65 -0
  43. package/dist/core/setup/types.js +55 -0
  44. package/dist/core/setup-assets.d.ts +8 -119
  45. package/dist/core/setup-assets.js +7 -437
  46. package/dist/core/tier1-liaison.d.ts +2 -0
  47. package/dist/core/tier1-liaison.js +64 -3
  48. package/dist/core/verifier/executor.d.ts +5 -0
  49. package/dist/core/verifier/executor.js +62 -0
  50. package/dist/core/verifier/phases.d.ts +5 -0
  51. package/dist/core/verifier/phases.js +71 -0
  52. package/dist/core/verifier/task-verifier.d.ts +37 -0
  53. package/dist/core/verifier/task-verifier.js +233 -0
  54. package/dist/core/verifier/types.d.ts +51 -0
  55. package/dist/core/verifier/types.js +1 -0
  56. package/dist/core/verifier.d.ts +5 -93
  57. package/dist/core/verifier.js +5 -359
  58. package/dist/db/introspector.js +59 -59
  59. package/dist/governor/evaluator.d.ts +4 -64
  60. package/dist/governor/evaluator.js +21 -686
  61. package/dist/governor/patch-applier.d.ts +8 -0
  62. package/dist/governor/patch-applier.js +121 -0
  63. package/dist/governor/self-healing.d.ts +24 -0
  64. package/dist/governor/self-healing.js +351 -0
  65. package/dist/governor/test-integrity.d.ts +41 -0
  66. package/dist/governor/test-integrity.js +237 -0
  67. package/dist/index.js +8 -0
  68. package/dist/mcp/capture.d.ts +4 -0
  69. package/dist/mcp/capture.js +33 -0
  70. package/dist/mcp/resources.d.ts +39 -0
  71. package/dist/mcp/resources.js +44 -0
  72. package/dist/mcp/server.d.ts +11 -0
  73. package/dist/mcp/server.js +14 -89
  74. package/dist/mcp/tools.d.ts +1 -0
  75. package/dist/mcp/tools.js +1 -0
  76. package/dist/runner/agent-supervisor.d.ts +8 -302
  77. package/dist/runner/agent-supervisor.js +41 -1030
  78. package/dist/runner/cli-config.d.ts +84 -0
  79. package/dist/runner/cli-config.js +215 -0
  80. package/dist/runner/native-driver.d.ts +18 -0
  81. package/dist/runner/native-driver.js +208 -0
  82. package/dist/runner/native-engine.d.ts +27 -0
  83. package/dist/runner/native-engine.js +184 -0
  84. package/dist/runner/native-tools.d.ts +31 -0
  85. package/dist/runner/native-tools.js +241 -0
  86. package/dist/runner/process-utils.d.ts +12 -0
  87. package/dist/runner/process-utils.js +96 -0
  88. package/dist/runner/run-lifecycle.d.ts +6 -0
  89. package/dist/runner/run-lifecycle.js +42 -0
  90. package/dist/runner/types.d.ts +215 -0
  91. package/dist/runner/types.js +41 -0
  92. package/dist/scanner/detector.js +42 -663
  93. package/dist/scanner/detectors/ecosystem-detector.d.ts +16 -0
  94. package/dist/scanner/detectors/ecosystem-detector.js +354 -0
  95. package/dist/scanner/detectors/orm-detector.d.ts +5 -0
  96. package/dist/scanner/detectors/orm-detector.js +59 -0
  97. package/dist/scanner/detectors/workspace-detector.d.ts +19 -0
  98. package/dist/scanner/detectors/workspace-detector.js +266 -0
  99. package/dist/server/cli-runner.d.ts +15 -0
  100. package/dist/server/cli-runner.js +43 -0
  101. package/dist/server/event-hub.d.ts +42 -0
  102. package/dist/server/event-hub.js +169 -0
  103. package/dist/server/http-utils.d.ts +15 -0
  104. package/dist/server/http-utils.js +56 -0
  105. package/dist/server/plan-utils.d.ts +9 -0
  106. package/dist/server/plan-utils.js +31 -0
  107. package/dist/server/routes/db-routes.d.ts +57 -0
  108. package/dist/server/routes/db-routes.js +484 -0
  109. package/dist/server/routes/task-routes.d.ts +266 -0
  110. package/dist/server/routes/task-routes.js +285 -0
  111. package/dist/server/routes/telemetry-routes.d.ts +128 -0
  112. package/dist/server/routes/telemetry-routes.js +179 -0
  113. package/dist/server/routes/triage-routes.d.ts +81 -0
  114. package/dist/server/routes/triage-routes.js +244 -0
  115. package/dist/server/routes/worktree-routes.d.ts +31 -0
  116. package/dist/server/routes/worktree-routes.js +144 -0
  117. package/dist/server/studio-server.d.ts +4 -11
  118. package/dist/server/studio-server.js +34 -1578
  119. package/dist/server/studio-ui/shell/pipeline-state.js +2 -0
  120. package/dist/server/studio-ui/shell/triage.js +10 -0
  121. package/package.json +2 -2
  122. package/plugin/.claude-plugin/plugin.json +1 -1
  123. package/plugin/agents/architect.md +4 -2
  124. package/plugin/agents/verifier.md +1 -1
  125. package/plugin/agents/worker.md +7 -4
  126. package/plugin/skills/nativ/SKILL.md +1 -0
  127. package/templates/.claudeignore +62 -45
  128. package/templates/AGENTS.md +1 -0
  129. package/templates/CLAUDE.md +13 -0
  130. package/templates/claude-agents/architect.md +4 -2
  131. package/templates/claude-agents/verifier.md +1 -1
  132. package/templates/claude-agents/worker.md +7 -4
  133. package/templates/dot-ai/api_contracts.json +66 -66
  134. package/templates/dot-ai/context.md +38 -38
  135. package/templates/dot-ai/db_schema.json +39 -39
  136. package/dist/commands/session.d.ts +0 -22
  137. package/dist/commands/session.js +0 -106
  138. package/dist/core/session-memory.d.ts +0 -42
  139. package/dist/core/session-memory.js +0 -121
@@ -0,0 +1,237 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { CircuitBreaker, } from './circuit-breaker.js';
6
+ const TEST_CODE_EXTENSION = /\.(?:[cm]?[jt]sx?|py|go|rb|java|kt|cs|php)$/i;
7
+ export function isTestFile(file) {
8
+ const p = file.replace(/\\/g, '/');
9
+ if (!TEST_CODE_EXTENSION.test(p) || /(^|\/)node_modules\//.test(p))
10
+ return false;
11
+ return (/(^|\/)(?:__tests__|tests?|specs?)\//i.test(p) ||
12
+ /[._-](?:test|spec)\.[a-z]+$/i.test(p) ||
13
+ /(^|\/)test[_-][^/]+$/i.test(p));
14
+ }
15
+ /** Heuristic, language-agnostic counts; only deltas against the baseline matter. */
16
+ export function measureTestSuite(source) {
17
+ const count = (re) => (source.match(re) || []).length;
18
+ return {
19
+ assertions: count(/(?<![.\w])(?:assert(?:\.[A-Za-z]+)*|expect|self\.assert[A-Za-z]*|t\.(?:is|ok|not|truthy|falsy|equal|deepEqual|true|false|throws|throwsAsync|notThrows|regex|snapshot|like))\s*\(/g) +
20
+ count(/^\s*assert\s+(?!\()/gm),
21
+ testCases: count(/(?<![.\w])(?:it|test|describe|suite|context)(?:\.(?:each|concurrent|serial))?\s*\(/g) +
22
+ count(/^\s*(?:async\s+)?def\s+test_/gm) +
23
+ count(/^func\s+Test[A-Z_]/gm),
24
+ skipped: count(/(?<![.\w])(?:it|test|describe|suite|context)\.(?:skip|todo)\s*\(|(?<![.\w])x(?:it|test|describe)\s*\(|@pytest\.mark\.skip|\bt\.Skip(?:Now|f)?\(/g),
25
+ focused: count(/(?<![.\w])(?:it|test|describe|suite|context)\.only\s*\(|(?<![.\w])f(?:it|describe)\s*\(/g),
26
+ };
27
+ }
28
+ function git(targetDir, args) {
29
+ const res = spawnSync('git', args, {
30
+ cwd: targetDir,
31
+ encoding: 'utf8',
32
+ windowsHide: true,
33
+ maxBuffer: 64 * 1024 * 1024,
34
+ });
35
+ return { ok: res.status === 0, stdout: res.stdout ?? '' };
36
+ }
37
+ /** Current commit of the checkout at `targetDir`, or null outside git / before the first commit. */
38
+ export function resolveGitHead(targetDir) {
39
+ const res = git(targetDir, ['rev-parse', '--verify', 'HEAD']);
40
+ return res.ok ? res.stdout.trim() || null : null;
41
+ }
42
+ /** Test files and test-named scripts referenced by `package.json` test scripts. */
43
+ function testScriptReferences(packageJson) {
44
+ const scripts = new Set();
45
+ const files = new Set();
46
+ try {
47
+ const pkg = JSON.parse(packageJson);
48
+ for (const [name, command] of Object.entries(pkg?.scripts ?? {})) {
49
+ if (!/^(?:pre|post)?test(?:[:_-]|$)/.test(name) || typeof command !== 'string')
50
+ continue;
51
+ scripts.add(name);
52
+ for (const token of command.split(/[\s;&|'"]+/)) {
53
+ if (token && isTestFile(token))
54
+ files.add(token.replace(/\\/g, '/').replace(/^\.\//, ''));
55
+ }
56
+ }
57
+ }
58
+ catch {
59
+ // Unparseable package.json: nothing to compare.
60
+ }
61
+ return { scripts, files };
62
+ }
63
+ function quoteArg(value) {
64
+ return /^[\w./@:-]+$/.test(value) ? value : `"${value.replace(/"/g, '\\"')}"`;
65
+ }
66
+ /**
67
+ * Enforces that a task never deletes or weakens the test suites that existed
68
+ * when it started: no deleted test files, no fewer assertions or cases, no new
69
+ * skip/only markers, and no test files dropped from package.json test scripts.
70
+ */
71
+ export class TestIntegrityGuard {
72
+ static evaluate(targetDir, taskId) {
73
+ const pass = (skippedReason, baselineRef = null, baselineRecorded = false) => ({
74
+ approved: true,
75
+ blastRadius: 'LOW_ADDITIVE',
76
+ ruleId: skippedReason ? 'TEST_INTEGRITY_SKIPPED' : 'TEST_INTEGRITY_OK',
77
+ message: skippedReason ?? 'Existing test suites are intact.',
78
+ violations: [],
79
+ baselineRef,
80
+ baselineRecorded,
81
+ findings: [],
82
+ ...(skippedReason ? { skippedReason } : {}),
83
+ });
84
+ try {
85
+ const cfgPath = path.join(targetDir, '.nativ', 'config.json');
86
+ if (fs.existsSync(cfgPath)) {
87
+ const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
88
+ if (raw.testIntegrity === 'off' || raw.profile === 'prototype') {
89
+ return pass('Test integrity check disabled by profile or testIntegrity: off');
90
+ }
91
+ }
92
+ }
93
+ catch {
94
+ // Non-fatal
95
+ }
96
+ if (!git(targetDir, ['rev-parse', '--is-inside-work-tree']).ok)
97
+ return pass('Not a git repository; test integrity cannot be checked.');
98
+ const recorded = CircuitBreaker.getBaseline(targetDir, taskId);
99
+ const recordedValid = recorded ? git(targetDir, ['cat-file', '-e', `${recorded}^{commit}`]).ok : false;
100
+ const baseline = recordedValid ? recorded : resolveGitHead(targetDir);
101
+ if (!baseline)
102
+ return pass('Repository has no commits yet; nothing to protect.');
103
+ const diff = git(targetDir, ['diff', '--name-status', '-M', '--relative', baseline, '--']);
104
+ if (!diff.ok)
105
+ return pass(`Could not diff against ${baseline}.`, baseline, recordedValid);
106
+ const findings = [];
107
+ const baselineSource = (file) => {
108
+ const res = git(targetDir, ['show', `${baseline}:./${file}`]);
109
+ return res.ok ? res.stdout : null;
110
+ };
111
+ for (const line of diff.stdout.split('\n')) {
112
+ const [status, ...paths] = line.trim().split('\t');
113
+ if (!status || !paths.length)
114
+ continue;
115
+ const kind = status[0];
116
+ const from = paths[0];
117
+ const to = paths[paths.length - 1];
118
+ if (kind === 'D' && isTestFile(from)) {
119
+ findings.push({ rule: 'TEST_SUITE_DELETED', path: from, detail: `Test file '${from}' was deleted.` });
120
+ continue;
121
+ }
122
+ if (kind === 'R' && isTestFile(from) && !isTestFile(to)) {
123
+ findings.push({ rule: 'TEST_SUITE_DELETED', path: from, detail: `Test file '${from}' was moved out of the suite to '${to}'.` });
124
+ continue;
125
+ }
126
+ if ((kind === 'M' || kind === 'R') && isTestFile(to)) {
127
+ const before = baselineSource(from);
128
+ let after = null;
129
+ try {
130
+ after = fs.readFileSync(path.join(targetDir, to), 'utf8');
131
+ }
132
+ catch {
133
+ // Unreadable now: treat like a deletion.
134
+ }
135
+ if (before === null)
136
+ continue;
137
+ if (after === null) {
138
+ findings.push({ rule: 'TEST_SUITE_DELETED', path: to, detail: `Test file '${to}' can no longer be read.` });
139
+ continue;
140
+ }
141
+ const was = measureTestSuite(before);
142
+ const now = measureTestSuite(after);
143
+ if (now.assertions < was.assertions) {
144
+ findings.push({ rule: 'TEST_ASSERTIONS_REMOVED', path: to, detail: `'${to}' went from ${was.assertions} to ${now.assertions} assertions.` });
145
+ }
146
+ if (now.testCases < was.testCases) {
147
+ findings.push({ rule: 'TEST_CASES_REMOVED', path: to, detail: `'${to}' went from ${was.testCases} to ${now.testCases} test cases.` });
148
+ }
149
+ if (now.skipped > was.skipped) {
150
+ findings.push({ rule: 'TESTS_SKIPPED', path: to, detail: `'${to}' added ${now.skipped - was.skipped} skip/todo marker(s).` });
151
+ }
152
+ if (now.focused > was.focused) {
153
+ findings.push({ rule: 'TESTS_FOCUSED', path: to, detail: `'${to}' added ${now.focused - was.focused} .only marker(s), silencing the rest of the suite.` });
154
+ }
155
+ }
156
+ if (kind === 'M' && to === 'package.json') {
157
+ const before = baselineSource('package.json');
158
+ let after = '';
159
+ try {
160
+ after = fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8');
161
+ }
162
+ catch {
163
+ // Treated as an empty package.json below.
164
+ }
165
+ if (before !== null) {
166
+ const was = testScriptReferences(before);
167
+ const now = testScriptReferences(after);
168
+ const droppedScripts = [...was.scripts].filter((s) => !now.scripts.has(s));
169
+ const droppedFiles = [...was.files].filter((f) => !now.files.has(f));
170
+ if (droppedScripts.length || droppedFiles.length) {
171
+ findings.push({
172
+ rule: 'TEST_SCRIPT_WEAKENED',
173
+ path: 'package.json',
174
+ detail: `package.json test scripts dropped ${[...droppedScripts.map((s) => `script '${s}'`), ...droppedFiles].join(', ')}.`,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ if (!findings.length)
181
+ return pass(undefined, baseline, recordedValid);
182
+ return {
183
+ approved: false,
184
+ blastRadius: 'HIGH_DESTRUCTIVE',
185
+ ruleId: 'TEST_INTEGRITY_VIOLATION',
186
+ message: `Task changes would delete or weaken ${findings.length === 1 ? 'an existing test suite' : `existing test suites (${findings.length} findings)`}.`,
187
+ violations: findings.map((f) => `${f.rule}: ${f.detail}`),
188
+ baselineRef: baseline,
189
+ baselineRecorded: recordedValid,
190
+ findings,
191
+ };
192
+ }
193
+ /**
194
+ * Proposal to restore every flagged file from the baseline, with proof that
195
+ * each baseline blob exists and what the restored suite measures.
196
+ */
197
+ static proposeRestoration(targetDir, report) {
198
+ if (report.approved || !report.baselineRef)
199
+ return null;
200
+ const started = Date.now();
201
+ const files = [...new Set(report.findings.map((f) => f.path))];
202
+ const baseline = report.baselineRef;
203
+ const checks = files.map((file) => {
204
+ const blob = git(targetDir, ['show', `${baseline}:./${file}`]);
205
+ if (!blob.ok)
206
+ return { name: `baseline:${file}`, passed: false, detail: `No copy of '${file}' at ${baseline.slice(0, 12)}` };
207
+ if (!isTestFile(file))
208
+ return { name: `baseline:${file}`, passed: true, detail: `Baseline copy available (${blob.stdout.length} bytes)` };
209
+ const m = measureTestSuite(blob.stdout);
210
+ return {
211
+ name: `baseline:${file}`,
212
+ passed: true,
213
+ detail: `Restores ${m.assertions} assertions and ${m.testCases} test cases (${m.skipped} skipped, ${m.focused} focused)`,
214
+ };
215
+ });
216
+ return {
217
+ kind: 'restore_tests',
218
+ proposalId: `heal-${crypto.randomUUID().slice(0, 8)}`,
219
+ strategy: 'restore_test_suite',
220
+ rationale: 'Restore the flagged test files from the task baseline, then make the implementation pass the original suite.',
221
+ requiresHumanApproval: true,
222
+ candidatesEvaluated: 1,
223
+ verificationProof: {
224
+ isolated: true,
225
+ method: 'baseline_blob_check',
226
+ passed: checks.length > 0 && checks.every((c) => c.passed),
227
+ checks,
228
+ verifiedAt: new Date().toISOString(),
229
+ durationMs: Date.now() - started,
230
+ },
231
+ generatedAt: new Date().toISOString(),
232
+ baselineRef: baseline,
233
+ files,
234
+ commands: [`git checkout ${baseline} -- ${files.map(quoteArg).join(' ')}`],
235
+ };
236
+ }
237
+ }
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { runTestGen, TEST_GEN_FRAMEWORK_HELP } from './commands/test-gen.js';
16
16
  import { runBench } from './commands/bench.js';
17
17
  import { runTriage } from './commands/triage.js';
18
18
  import { runLearnPropose, runLearnDecide, runLearnList } from './commands/learn.js';
19
+ import { runProfileCommand } from './commands/profile.js';
19
20
  import { packageVersion } from './core/version.js';
20
21
  export function createProgram() {
21
22
  const program = new Command();
@@ -103,6 +104,13 @@ export function createProgram() {
103
104
  .action(async (targetDir, options) => {
104
105
  await runBench(targetDir, options);
105
106
  });
107
+ program
108
+ .command('profile [profile] [targetDir]')
109
+ .description('Inspect or switch adaptive ceremony profiles (prototype, solo, enterprise)')
110
+ .option('--json', 'Output profile configuration as JSON')
111
+ .action(async (profile, targetDir, options) => {
112
+ await runProfileCommand(profile, targetDir, options);
113
+ });
106
114
  const task = program
107
115
  .command('task')
108
116
  .description('Manage task lifecycle and JIT context slicing in .ai/master_plan.json');
@@ -0,0 +1,4 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare function captureOutput(fn: () => Promise<unknown>, options?: {
3
+ errorOnExitCode?: boolean;
4
+ }): Promise<CallToolResult>;
@@ -0,0 +1,33 @@
1
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
2
+ // Handlers share global console/process.exitCode state, so captured runs are serialized.
3
+ let captureQueue = Promise.resolve();
4
+ export function captureOutput(fn, options = {}) {
5
+ const run = async () => {
6
+ const lines = [];
7
+ const collect = (...args) => {
8
+ lines.push(args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' '));
9
+ };
10
+ const original = { log: console.log, error: console.error, warn: console.warn, info: console.info };
11
+ const previousExitCode = process.exitCode;
12
+ process.exitCode = undefined;
13
+ console.log = console.error = console.warn = console.info = collect;
14
+ let failed = false;
15
+ try {
16
+ await fn();
17
+ failed = options.errorOnExitCode !== false && process.exitCode !== undefined && process.exitCode !== 0;
18
+ }
19
+ catch (err) {
20
+ failed = true;
21
+ lines.push(`Error: ${err instanceof Error ? err.message : String(err)}`);
22
+ }
23
+ finally {
24
+ Object.assign(console, original);
25
+ process.exitCode = previousExitCode;
26
+ }
27
+ const text = lines.join('\n').replace(ANSI_PATTERN, '').trim() || '(no output)';
28
+ return { content: [{ type: 'text', text }], isError: failed || undefined };
29
+ };
30
+ const result = captureQueue.then(run, run);
31
+ captureQueue = result.catch(() => undefined);
32
+ return result;
33
+ }
@@ -0,0 +1,39 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const MCP_RESOURCES: readonly [{
3
+ readonly name: "context";
4
+ readonly uris: readonly ["nativ://context"];
5
+ readonly file: "context.md";
6
+ readonly mimeType: "text/markdown";
7
+ readonly description: "Project context, tech stack and guardrails (.ai/context.md)";
8
+ }, {
9
+ readonly name: "master-plan";
10
+ readonly uris: readonly ["nativ://master-plan"];
11
+ readonly file: "master_plan.json";
12
+ readonly mimeType: "application/json";
13
+ readonly description: "Milestones, tasks and their status (.ai/master_plan.json)";
14
+ }, {
15
+ readonly name: "db-schema";
16
+ readonly uris: readonly ["nativ://db-schema"];
17
+ readonly file: "db_schema.json";
18
+ readonly mimeType: "application/json";
19
+ readonly description: "Database schema contract (.ai/db_schema.json)";
20
+ }, {
21
+ readonly name: "api-contracts";
22
+ readonly uris: readonly ["nativ://api-contracts"];
23
+ readonly file: "api_contracts.json";
24
+ readonly mimeType: "application/json";
25
+ readonly description: "API route and schema contracts (.ai/api_contracts.json)";
26
+ }, {
27
+ readonly name: "escalation";
28
+ readonly uris: readonly ["nativ://escalation"];
29
+ readonly file: "escalation.json";
30
+ readonly mimeType: "application/json";
31
+ readonly description: "Tier-1 escalation records (.ai/escalation.json)";
32
+ }, {
33
+ readonly name: "telemetry";
34
+ readonly uris: readonly ["nativ://telemetry"];
35
+ readonly file: "telemetry.json";
36
+ readonly mimeType: "application/json";
37
+ readonly description: "Execution duration, token usage and cost telemetry (.ai/telemetry.json)";
38
+ }];
39
+ export declare function registerMcpResources(server: McpServer, targetDir: string): void;
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const MCP_RESOURCES = [
4
+ { name: 'context', uris: ['nativ://context'], file: 'context.md', mimeType: 'text/markdown', description: 'Project context, tech stack and guardrails (.ai/context.md)' },
5
+ { name: 'master-plan', uris: ['nativ://master-plan'], file: 'master_plan.json', mimeType: 'application/json', description: 'Milestones, tasks and their status (.ai/master_plan.json)' },
6
+ { name: 'db-schema', uris: ['nativ://db-schema'], file: 'db_schema.json', mimeType: 'application/json', description: 'Database schema contract (.ai/db_schema.json)' },
7
+ { name: 'api-contracts', uris: ['nativ://api-contracts'], file: 'api_contracts.json', mimeType: 'application/json', description: 'API route and schema contracts (.ai/api_contracts.json)' },
8
+ { name: 'escalation', uris: ['nativ://escalation'], file: 'escalation.json', mimeType: 'application/json', description: 'Tier-1 escalation records (.ai/escalation.json)' },
9
+ { name: 'telemetry', uris: ['nativ://telemetry'], file: 'telemetry.json', mimeType: 'application/json', description: 'Execution duration, token usage and cost telemetry (.ai/telemetry.json)' },
10
+ ];
11
+ export function registerMcpResources(server, targetDir) {
12
+ for (const res of MCP_RESOURCES) {
13
+ for (const uriStr of res.uris) {
14
+ server.registerResource(res.name, uriStr, { mimeType: res.mimeType, description: res.description }, async (uri) => {
15
+ const filePath = path.join(targetDir, '.ai', res.file);
16
+ let text;
17
+ if (fs.existsSync(filePath)) {
18
+ text = fs.readFileSync(filePath, 'utf8');
19
+ }
20
+ else if (res.name === 'escalation') {
21
+ text = JSON.stringify({ escalations: [] }, null, 2);
22
+ }
23
+ else if (res.name === 'telemetry') {
24
+ text = JSON.stringify({
25
+ version: '1.0.0',
26
+ summary: {
27
+ totalTasksCompleted: 0,
28
+ totalDurationMs: 0,
29
+ estimatedTotalTokens: 0,
30
+ estimatedTotalCostUsd: 0,
31
+ verificationPassRate: 1.0,
32
+ circuitBreakerTrips: 0,
33
+ },
34
+ tasks: [],
35
+ }, null, 2);
36
+ }
37
+ else {
38
+ throw new Error(`.ai/${res.file} not found in ${targetDir}. Run nativ_init first.`);
39
+ }
40
+ return { contents: [{ uri: uri.href, mimeType: res.mimeType, text }] };
41
+ });
42
+ }
43
+ }
44
+ }
@@ -1,3 +1,14 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export { captureOutput } from './capture.js';
3
+ export { registerMcpResources, MCP_RESOURCES } from './resources.js';
4
+ export declare const ESCALATION_TYPES: readonly ["contract_drift", "schema_flaw", "missing_credential", "dependency_conflict", "architectural_ambiguity"];
5
+ /**
6
+ * Native MCP (Model Context Protocol) server for Nativ over stdio.
7
+ *
8
+ * stdout is reserved for JSON-RPC frames, so the existing CLI handlers (which print via console.*)
9
+ * run inside `captureOutput`, which collects their output and returns it as tool content.
10
+ * Air-gap: only masked, structure-only commands are exposed. `db sync`, `db ui` and `init --force`
11
+ * are intentionally absent, and every tool is bound to the server's project root.
12
+ */
2
13
  export declare function createMcpServer(targetDirArg?: string): McpServer;
3
14
  export declare function startMcpServer(targetDirArg?: string): Promise<void>;
@@ -1,4 +1,3 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -16,6 +15,17 @@ import { runTestGen } from '../commands/test-gen.js';
16
15
  import { runBench } from '../commands/bench.js';
17
16
  import { TEST_FRAMEWORKS } from '../core/test-generator-types.js';
18
17
  import { packageVersion } from '../core/version.js';
18
+ import { captureOutput } from './capture.js';
19
+ import { registerMcpResources } from './resources.js';
20
+ export { captureOutput } from './capture.js';
21
+ export { registerMcpResources, MCP_RESOURCES } from './resources.js';
22
+ export const ESCALATION_TYPES = [
23
+ 'contract_drift',
24
+ 'schema_flaw',
25
+ 'missing_credential',
26
+ 'dependency_conflict',
27
+ 'architectural_ambiguity',
28
+ ];
19
29
  /**
20
30
  * Native MCP (Model Context Protocol) server for Nativ over stdio.
21
31
  *
@@ -24,54 +34,6 @@ import { packageVersion } from '../core/version.js';
24
34
  * Air-gap: only masked, structure-only commands are exposed. `db sync`, `db ui` and `init --force`
25
35
  * are intentionally absent, and every tool is bound to the server's project root.
26
36
  */
27
- const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
28
- const ESCALATION_TYPES = [
29
- 'contract_drift',
30
- 'schema_flaw',
31
- 'missing_credential',
32
- 'dependency_conflict',
33
- 'architectural_ambiguity',
34
- ];
35
- const RESOURCES = [
36
- { name: 'context', uris: ['nativ://context'], file: 'context.md', mimeType: 'text/markdown', description: 'Project context, tech stack and guardrails (.ai/context.md)' },
37
- { name: 'master-plan', uris: ['nativ://master-plan'], file: 'master_plan.json', mimeType: 'application/json', description: 'Milestones, tasks and their status (.ai/master_plan.json)' },
38
- { name: 'db-schema', uris: ['nativ://db-schema'], file: 'db_schema.json', mimeType: 'application/json', description: 'Database schema contract (.ai/db_schema.json)' },
39
- { name: 'api-contracts', uris: ['nativ://api-contracts'], file: 'api_contracts.json', mimeType: 'application/json', description: 'API route and schema contracts (.ai/api_contracts.json)' },
40
- { name: 'escalation', uris: ['nativ://escalation'], file: 'escalation.json', mimeType: 'application/json', description: 'Tier-1 escalation records (.ai/escalation.json)' },
41
- { name: 'telemetry', uris: ['nativ://telemetry'], file: 'telemetry.json', mimeType: 'application/json', description: 'Execution duration, token usage and cost telemetry (.ai/telemetry.json)' },
42
- ];
43
- // Handlers share global console/process.exitCode state, so captured runs are serialized.
44
- let captureQueue = Promise.resolve();
45
- function captureOutput(fn, options = {}) {
46
- const run = async () => {
47
- const lines = [];
48
- const collect = (...args) => {
49
- lines.push(args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' '));
50
- };
51
- const original = { log: console.log, error: console.error, warn: console.warn, info: console.info };
52
- const previousExitCode = process.exitCode;
53
- process.exitCode = undefined;
54
- console.log = console.error = console.warn = console.info = collect;
55
- let failed = false;
56
- try {
57
- await fn();
58
- failed = options.errorOnExitCode !== false && process.exitCode !== undefined && process.exitCode !== 0;
59
- }
60
- catch (err) {
61
- failed = true;
62
- lines.push(`Error: ${err instanceof Error ? err.message : String(err)}`);
63
- }
64
- finally {
65
- Object.assign(console, original);
66
- process.exitCode = previousExitCode;
67
- }
68
- const text = lines.join('\n').replace(ANSI_PATTERN, '').trim() || '(no output)';
69
- return { content: [{ type: 'text', text }], isError: failed || undefined };
70
- };
71
- const result = captureQueue.then(run, run);
72
- captureQueue = result.catch(() => undefined);
73
- return result;
74
- }
75
37
  export function createMcpServer(targetDirArg) {
76
38
  const targetDir = path.resolve(targetDirArg || process.cwd());
77
39
  const server = new McpServer({ name: 'nativ', version: packageVersion() });
@@ -133,7 +95,6 @@ export function createMcpServer(targetDirArg) {
133
95
  skipVerify: z.unknown().optional().describe('Not accepted: only a human at a terminal may skip'),
134
96
  },
135
97
  }, ({ taskId, notes, skipVerify }) => captureOutput(async () => {
136
- // Kept in the schema so a stray value is refused instead of silently dropped.
137
98
  if (skipVerify !== undefined) {
138
99
  throw new Error('skipVerify is not allowed over MCP. Only a human at a terminal can skip verification.');
139
100
  }
@@ -235,16 +196,12 @@ export function createMcpServer(targetDirArg) {
235
196
  description: 'Read-only health check of the Claude Code integration: MCP server, hooks, permissions, agents, enforcement mode and provider availability (JSON). ' +
236
197
  'Reports drift but never repairs it; repairing is `nativ doctor --fix` in a terminal.',
237
198
  inputSchema: {},
238
- },
239
- // A report that lists problems is still a successful report, not a tool failure.
240
- () => captureOutput(async () => runDoctor(targetDir, { json: true }), { errorOnExitCode: false }));
199
+ }, () => captureOutput(async () => runDoctor(targetDir, { json: true }), { errorOnExitCode: false }));
241
200
  // ─── Database telemetry tools (masked, structure-only) ─────────────────────
242
201
  registerNativTool('db_status', {
243
202
  description: 'Dev/Prod database connection health, engine, latency and table count. URLs are masked; no credentials are returned.',
244
203
  inputSchema: {},
245
- },
246
- // An offline database is a valid status report, not a tool failure.
247
- () => captureOutput(() => runDbStatus(targetDir, { json: true }), { errorOnExitCode: false }));
204
+ }, () => captureOutput(() => runDbStatus(targetDir, { json: true }), { errorOnExitCode: false }));
248
205
  registerNativTool('db_inspect', {
249
206
  description: 'Introspect live database structure (tables, columns, types, keys, indexes). Structure only; never reads row data.',
250
207
  inputSchema: {
@@ -288,39 +245,7 @@ export function createMcpServer(targetDirArg) {
288
245
  concurrency: z.number().optional().describe('Number of simulated concurrent agent workers (default 6)'),
289
246
  },
290
247
  }, async ({ scenario, concurrency }) => captureOutput(() => runBench(targetDir, { scenario, concurrency, json: true })));
291
- // ─── Contract resources ────────────────────────────────────────────────────
292
- for (const res of RESOURCES) {
293
- for (const uriStr of res.uris) {
294
- server.registerResource(res.name, uriStr, { mimeType: res.mimeType, description: res.description }, async (uri) => {
295
- const filePath = path.join(targetDir, '.ai', res.file);
296
- let text;
297
- if (fs.existsSync(filePath)) {
298
- text = fs.readFileSync(filePath, 'utf8');
299
- }
300
- else if (res.name === 'escalation') {
301
- text = JSON.stringify({ escalations: [] }, null, 2);
302
- }
303
- else if (res.name === 'telemetry') {
304
- text = JSON.stringify({
305
- version: '1.0.0',
306
- summary: {
307
- totalTasksCompleted: 0,
308
- totalDurationMs: 0,
309
- estimatedTotalTokens: 0,
310
- estimatedTotalCostUsd: 0,
311
- verificationPassRate: 1.0,
312
- circuitBreakerTrips: 0,
313
- },
314
- tasks: [],
315
- }, null, 2);
316
- }
317
- else {
318
- throw new Error(`.ai/${res.file} not found in ${targetDir}. Run nativ_init first.`);
319
- }
320
- return { contents: [{ uri: uri.href, mimeType: res.mimeType, text }] };
321
- });
322
- }
323
- }
248
+ registerMcpResources(server, targetDir);
324
249
  return server;
325
250
  }
326
251
  export async function startMcpServer(targetDirArg) {
@@ -0,0 +1 @@
1
+ export { ESCALATION_TYPES, createMcpServer, startMcpServer } from './server.js';
@@ -0,0 +1 @@
1
+ export { ESCALATION_TYPES, createMcpServer, startMcpServer } from './server.js';