@njeybe/nativ 2.0.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 (140) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/bin/cli.js +31 -0
  5. package/dist/commands/bench.d.ts +8 -0
  6. package/dist/commands/bench.js +52 -0
  7. package/dist/commands/db.d.ts +22 -0
  8. package/dist/commands/db.js +415 -0
  9. package/dist/commands/doctor.d.ts +38 -0
  10. package/dist/commands/doctor.js +169 -0
  11. package/dist/commands/hook.d.ts +31 -0
  12. package/dist/commands/hook.js +105 -0
  13. package/dist/commands/init.d.ts +3 -0
  14. package/dist/commands/init.js +144 -0
  15. package/dist/commands/mcp.d.ts +5 -0
  16. package/dist/commands/mcp.js +16 -0
  17. package/dist/commands/setup.d.ts +17 -0
  18. package/dist/commands/setup.js +51 -0
  19. package/dist/commands/status.d.ts +5 -0
  20. package/dist/commands/status.js +75 -0
  21. package/dist/commands/task.d.ts +83 -0
  22. package/dist/commands/task.js +807 -0
  23. package/dist/commands/test-gen.d.ts +14 -0
  24. package/dist/commands/test-gen.js +52 -0
  25. package/dist/commands/triage.d.ts +70 -0
  26. package/dist/commands/triage.js +343 -0
  27. package/dist/commands/update.d.ts +14 -0
  28. package/dist/commands/update.js +112 -0
  29. package/dist/commands/validate.d.ts +1 -0
  30. package/dist/commands/validate.js +89 -0
  31. package/dist/commands/verify.d.ts +8 -0
  32. package/dist/commands/verify.js +65 -0
  33. package/dist/commands/worktree.d.ts +47 -0
  34. package/dist/commands/worktree.js +396 -0
  35. package/dist/core/benchmark.d.ts +57 -0
  36. package/dist/core/benchmark.js +772 -0
  37. package/dist/core/enforcement.d.ts +80 -0
  38. package/dist/core/enforcement.js +271 -0
  39. package/dist/core/legacy-templates.d.ts +24 -0
  40. package/dist/core/legacy-templates.js +60 -0
  41. package/dist/core/lock-manager.d.ts +54 -0
  42. package/dist/core/lock-manager.js +260 -0
  43. package/dist/core/root-resolver.d.ts +32 -0
  44. package/dist/core/root-resolver.js +149 -0
  45. package/dist/core/setup-assets.d.ts +123 -0
  46. package/dist/core/setup-assets.js +438 -0
  47. package/dist/core/telemetry.d.ts +156 -0
  48. package/dist/core/telemetry.js +416 -0
  49. package/dist/core/test-generator-types.d.ts +227 -0
  50. package/dist/core/test-generator-types.js +179 -0
  51. package/dist/core/test-generator.d.ts +34 -0
  52. package/dist/core/test-generator.js +1690 -0
  53. package/dist/core/tier1-liaison.d.ts +139 -0
  54. package/dist/core/tier1-liaison.js +320 -0
  55. package/dist/core/verifier.d.ts +52 -0
  56. package/dist/core/verifier.js +249 -0
  57. package/dist/core/version.d.ts +5 -0
  58. package/dist/core/version.js +14 -0
  59. package/dist/core/worktree-merge.d.ts +30 -0
  60. package/dist/core/worktree-merge.js +98 -0
  61. package/dist/db/data-engine.d.ts +46 -0
  62. package/dist/db/data-engine.js +802 -0
  63. package/dist/db/diff.d.ts +39 -0
  64. package/dist/db/diff.js +420 -0
  65. package/dist/db/env-parser.d.ts +86 -0
  66. package/dist/db/env-parser.js +549 -0
  67. package/dist/db/introspector.d.ts +21 -0
  68. package/dist/db/introspector.js +835 -0
  69. package/dist/db/types.d.ts +217 -0
  70. package/dist/db/types.js +9 -0
  71. package/dist/governor/circuit-breaker.d.ts +100 -0
  72. package/dist/governor/circuit-breaker.js +231 -0
  73. package/dist/governor/evaluator.d.ts +67 -0
  74. package/dist/governor/evaluator.js +851 -0
  75. package/dist/governor/index.d.ts +6 -0
  76. package/dist/governor/index.js +6 -0
  77. package/dist/governor/rules/api-rules.d.ts +2 -0
  78. package/dist/governor/rules/api-rules.js +140 -0
  79. package/dist/governor/rules/db-rules.d.ts +2 -0
  80. package/dist/governor/rules/db-rules.js +180 -0
  81. package/dist/governor/store.d.ts +33 -0
  82. package/dist/governor/store.js +84 -0
  83. package/dist/governor/types.d.ts +64 -0
  84. package/dist/governor/types.js +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.js +358 -0
  87. package/dist/mcp/server.d.ts +3 -0
  88. package/dist/mcp/server.js +309 -0
  89. package/dist/providers/claude-api.d.ts +5 -0
  90. package/dist/providers/claude-api.js +75 -0
  91. package/dist/providers/claude-cli.d.ts +13 -0
  92. package/dist/providers/claude-cli.js +134 -0
  93. package/dist/providers/gemini.d.ts +8 -0
  94. package/dist/providers/gemini.js +78 -0
  95. package/dist/providers/index.d.ts +5 -0
  96. package/dist/providers/index.js +5 -0
  97. package/dist/providers/registry.d.ts +38 -0
  98. package/dist/providers/registry.js +121 -0
  99. package/dist/providers/types.d.ts +74 -0
  100. package/dist/providers/types.js +25 -0
  101. package/dist/runner/agent-supervisor.d.ts +363 -0
  102. package/dist/runner/agent-supervisor.js +1672 -0
  103. package/dist/scanner/context-builder.d.ts +2 -0
  104. package/dist/scanner/context-builder.js +29 -0
  105. package/dist/scanner/detector.d.ts +2 -0
  106. package/dist/scanner/detector.js +760 -0
  107. package/dist/scanner/types.d.ts +80 -0
  108. package/dist/scanner/types.js +50 -0
  109. package/dist/server/studio-server.d.ts +39 -0
  110. package/dist/server/studio-server.js +1931 -0
  111. package/dist/server/studio-ui.d.ts +15 -0
  112. package/dist/server/studio-ui.js +5321 -0
  113. package/package.json +70 -0
  114. package/plugin/.claude-plugin/plugin.json +18 -0
  115. package/plugin/.mcp.json +10 -0
  116. package/plugin/agents/architect.md +28 -0
  117. package/plugin/agents/verifier.md +28 -0
  118. package/plugin/agents/worker.md +26 -0
  119. package/plugin/hooks/hooks.json +28 -0
  120. package/plugin/skills/nativ/SKILL.md +75 -0
  121. package/templates/.claudeignore +45 -0
  122. package/templates/AGENTS.md +67 -0
  123. package/templates/CLAUDE.md +32 -0
  124. package/templates/GEMINI.md +66 -0
  125. package/templates/claude-agents/architect.md +28 -0
  126. package/templates/claude-agents/verifier.md +28 -0
  127. package/templates/claude-agents/worker.md +26 -0
  128. package/templates/dot-ai/api_contracts.json +66 -0
  129. package/templates/dot-ai/context.md +38 -0
  130. package/templates/dot-ai/db_schema.json +39 -0
  131. package/templates/dot-ai/master_plan.json +28 -0
  132. package/templates/dot-ai/subagents/backend.md +11 -0
  133. package/templates/dot-ai/subagents/database.md +10 -0
  134. package/templates/dot-ai/subagents/db-migration.md +11 -0
  135. package/templates/dot-ai/subagents/devops-agent.md +11 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +11 -0
  137. package/templates/dot-ai/subagents/frontend.md +126 -0
  138. package/templates/dot-ai/subagents/qa-tester.md +13 -0
  139. package/templates/dot-ai/subagents/security-auditor.md +11 -0
  140. package/templates/dot-ai/ui_specs.md +78 -0
@@ -0,0 +1,309 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { runTaskList, runTaskNext, runTaskStart, runTaskComplete, runTaskBlock, runTaskEscalate, runTaskAdd, runTaskProposePatch, } from '../commands/task.js';
7
+ import { SUBAGENT_TYPES } from '../scanner/types.js';
8
+ import { runInit } from '../commands/init.js';
9
+ import { runStatus } from '../commands/status.js';
10
+ import { runDoctor } from '../commands/doctor.js';
11
+ import { runWorktreeCreate, runWorktreeList, runWorktreeMerge, runWorktreeRemove, } from '../commands/worktree.js';
12
+ import { runDbStatus, runDbInspect, runDbDiff } from '../commands/db.js';
13
+ import { runVerify } from '../commands/verify.js';
14
+ import { runTestGen } from '../commands/test-gen.js';
15
+ import { runBench } from '../commands/bench.js';
16
+ import { TEST_FRAMEWORKS } from '../core/test-generator-types.js';
17
+ import { packageVersion } from '../core/version.js';
18
+ /**
19
+ * Native MCP (Model Context Protocol) server for Nativ over stdio.
20
+ *
21
+ * stdout is reserved for JSON-RPC frames, so the existing CLI handlers (which print via console.*)
22
+ * run inside `captureOutput`, which collects their output and returns it as tool content.
23
+ * Air-gap: only masked, structure-only commands are exposed. `db sync`, `db ui` and `init --force`
24
+ * are intentionally absent, and every tool is bound to the server's project root.
25
+ */
26
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
27
+ const ESCALATION_TYPES = [
28
+ 'contract_drift',
29
+ 'schema_flaw',
30
+ 'missing_credential',
31
+ 'dependency_conflict',
32
+ 'architectural_ambiguity',
33
+ ];
34
+ const RESOURCES = [
35
+ { name: 'context', uris: ['nativ://context'], file: 'context.md', mimeType: 'text/markdown', description: 'Project context, tech stack and guardrails (.ai/context.md)' },
36
+ { name: 'master-plan', uris: ['nativ://master-plan'], file: 'master_plan.json', mimeType: 'application/json', description: 'Milestones, tasks and their status (.ai/master_plan.json)' },
37
+ { name: 'db-schema', uris: ['nativ://db-schema'], file: 'db_schema.json', mimeType: 'application/json', description: 'Database schema contract (.ai/db_schema.json)' },
38
+ { name: 'api-contracts', uris: ['nativ://api-contracts'], file: 'api_contracts.json', mimeType: 'application/json', description: 'API route and schema contracts (.ai/api_contracts.json)' },
39
+ { name: 'escalation', uris: ['nativ://escalation'], file: 'escalation.json', mimeType: 'application/json', description: 'Tier-1 escalation records (.ai/escalation.json)' },
40
+ { name: 'telemetry', uris: ['nativ://telemetry'], file: 'telemetry.json', mimeType: 'application/json', description: 'Execution duration, token usage and cost telemetry (.ai/telemetry.json)' },
41
+ ];
42
+ // Handlers share global console/process.exitCode state, so captured runs are serialized.
43
+ let captureQueue = Promise.resolve();
44
+ function captureOutput(fn, options = {}) {
45
+ const run = async () => {
46
+ const lines = [];
47
+ const collect = (...args) => {
48
+ lines.push(args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' '));
49
+ };
50
+ const original = { log: console.log, error: console.error, warn: console.warn, info: console.info };
51
+ const previousExitCode = process.exitCode;
52
+ process.exitCode = undefined;
53
+ console.log = console.error = console.warn = console.info = collect;
54
+ let failed = false;
55
+ try {
56
+ await fn();
57
+ failed = options.errorOnExitCode !== false && process.exitCode !== undefined && process.exitCode !== 0;
58
+ }
59
+ catch (err) {
60
+ failed = true;
61
+ lines.push(`Error: ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ finally {
64
+ Object.assign(console, original);
65
+ process.exitCode = previousExitCode;
66
+ }
67
+ const text = lines.join('\n').replace(ANSI_PATTERN, '').trim() || '(no output)';
68
+ return { content: [{ type: 'text', text }], isError: failed || undefined };
69
+ };
70
+ const result = captureQueue.then(run, run);
71
+ captureQueue = result.catch(() => undefined);
72
+ return result;
73
+ }
74
+ export function createMcpServer(targetDirArg) {
75
+ const targetDir = path.resolve(targetDirArg || process.cwd());
76
+ const server = new McpServer({ name: 'nativ', version: packageVersion() });
77
+ function registerNativTool(baseName, meta, handler) {
78
+ server.registerTool(`nativ_${baseName}`, meta, handler);
79
+ }
80
+ // ─── Task lifecycle tools ──────────────────────────────────────────────────
81
+ registerNativTool('task_next', {
82
+ description: 'Get the next executable task in the active milestone, with its role guide and JIT contract slice (JSON).',
83
+ inputSchema: {},
84
+ }, () => captureOutput(() => runTaskNext(targetDir, { json: true })));
85
+ registerNativTool('task_list', {
86
+ description: 'List project tasks from .ai/master_plan.json with optional filters (JSON).',
87
+ inputSchema: {
88
+ available: z.boolean().optional().describe('Only tasks that are unblocked and ready for execution'),
89
+ status: z.enum(['pending', 'in_progress', 'completed', 'blocked']).optional().describe('Filter by task status'),
90
+ milestone: z.string().optional().describe('Filter by milestone ID or name'),
91
+ fastPath: z.boolean().optional().describe('Only fast-path tasks'),
92
+ },
93
+ }, ({ available, status, milestone, fastPath }) => captureOutput(() => runTaskList(targetDir, { available, status, milestone, fastPath, json: true })));
94
+ registerNativTool('task_add', {
95
+ description: 'Append a task to .ai/master_plan.json with an auto-incremented ID (JSON). Goes to the given milestone, else the active one; ' +
96
+ 'fastPath routes it to the fast-path milestone (created on first use).',
97
+ inputSchema: {
98
+ title: z.string().min(1).describe('Task title'),
99
+ description: z.string().optional().describe('What the task must accomplish'),
100
+ assignedSubagent: z.enum(SUBAGENT_TYPES).optional().describe('Sub-agent role (default backend)'),
101
+ milestone: z.string().optional().describe('Target milestone ID or name'),
102
+ verificationCommand: z.string().optional().describe('Command the task complete gatekeeper runs'),
103
+ targetFiles: z.array(z.string()).optional().describe('Files the task may modify'),
104
+ dependencies: z.array(z.string()).optional().describe('Task IDs that must be completed first'),
105
+ fastPath: z.boolean().optional().describe('Route to the fast-path track'),
106
+ },
107
+ }, ({ title, description, assignedSubagent, milestone, verificationCommand, targetFiles, dependencies, fastPath }) => captureOutput(() => runTaskAdd(title, targetDir, {
108
+ description,
109
+ agent: assignedSubagent,
110
+ milestone,
111
+ verify: verificationCommand,
112
+ files: targetFiles,
113
+ deps: dependencies,
114
+ fastPath,
115
+ json: true,
116
+ })));
117
+ registerNativTool('task_start', {
118
+ description: 'Mark a task as in_progress.',
119
+ inputSchema: { taskId: z.string().min(1).describe('Task ID, e.g. task-12') },
120
+ }, ({ taskId }) => captureOutput(() => runTaskStart(taskId, targetDir)));
121
+ registerNativTool('task_complete', {
122
+ description: "Mark a task as completed (automatically executes its verificationCommand unless skipVerify is true). Advances the milestone when all its tasks are done.",
123
+ inputSchema: {
124
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
125
+ notes: z.string().optional().describe('Completion notes or summary'),
126
+ skipVerify: z.boolean().optional().describe('Skip automated verification command execution'),
127
+ },
128
+ }, ({ taskId, notes, skipVerify }) => captureOutput(() => runTaskComplete(taskId, targetDir, { notes, skipVerify })));
129
+ registerNativTool('verify', {
130
+ description: "Run verification commands on demand for a task, milestone, or all completed tasks without changing task status.",
131
+ inputSchema: {
132
+ taskId: z.string().optional().describe('Task ID to verify. Defaults to active in_progress task.'),
133
+ milestone: z.string().optional().describe('Filter by milestone ID or name'),
134
+ all: z.boolean().optional().describe('Run verification commands for all completed tasks to detect regressions'),
135
+ },
136
+ }, ({ taskId, milestone, all }) => captureOutput(() => runVerify(taskId, targetDir, { milestone, all, json: true })));
137
+ registerNativTool('task_block', {
138
+ description: 'Mark a task as blocked with a required reason (e.g. verification failed after 3 attempts).',
139
+ inputSchema: {
140
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
141
+ reason: z.string().min(1).describe('Why the task is blocked'),
142
+ },
143
+ }, ({ taskId, reason }) => captureOutput(() => runTaskBlock(taskId, reason, targetDir)));
144
+ registerNativTool('task_escalate', {
145
+ description: 'Escalate a contract or architectural blocker to the Architect (writes .ai/escalation.json and blocks the task).',
146
+ inputSchema: {
147
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
148
+ type: z.enum(ESCALATION_TYPES).describe('Escalation type'),
149
+ details: z.string().min(1).describe('Explanation of the contract gap or blocker'),
150
+ affected: z.array(z.string()).optional().describe('Affected contract paths, e.g. [".ai/db_schema.json"]'),
151
+ },
152
+ }, ({ taskId, type, details, affected }) => captureOutput(() => runTaskEscalate(taskId, targetDir, { type, details, affected: affected?.join(',') })));
153
+ registerNativTool('task_propose_patch', {
154
+ description: 'Propose an atomic modification to .ai/db_schema.json or .ai/api_contracts.json. ' +
155
+ 'Evaluated deterministically by the Contract Governor. Additive changes are auto-approved; ' +
156
+ 'destructive changes are rejected and trip the Circuit Breaker on 3 failures.',
157
+ inputSchema: {
158
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
159
+ target: z.enum(['db_schema', 'api_contracts']).describe('Target contract to patch'),
160
+ operation: z.enum(['ADD', 'ALTER', 'DROP', 'RENAME']).describe('Operation type'),
161
+ path: z.string().min(1).describe('Dot-notation path to modify (e.g. users.columns.phone)'),
162
+ value: z.any().optional().describe('Value or schema definition to apply'),
163
+ reason: z.string().min(1).describe('Technical rationale for proposing this patch'),
164
+ baseHash: z.string().optional().describe('Base schema hash to prevent concurrent dirty writes'),
165
+ },
166
+ }, ({ taskId, target, operation, path: targetPath, value, reason, baseHash }) => captureOutput(() => runTaskProposePatch(targetDir, {
167
+ taskId,
168
+ target,
169
+ operation,
170
+ path: targetPath,
171
+ value,
172
+ reason,
173
+ baseHash,
174
+ json: true,
175
+ })));
176
+ // ─── Worktree lifecycle tools ──────────────────────────────────────────────
177
+ registerNativTool('worktree_create', {
178
+ description: 'Create an isolated Git worktree branch for a task with mounted .ai/ contracts (JSON).',
179
+ inputSchema: {
180
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
181
+ },
182
+ }, ({ taskId }) => captureOutput(() => runWorktreeCreate(taskId, targetDir, { json: true })));
183
+ registerNativTool('worktree_list', {
184
+ description: 'List active agent Git worktrees and their branch mapping (JSON).',
185
+ inputSchema: {},
186
+ }, () => captureOutput(() => runWorktreeList(targetDir, { json: true })));
187
+ registerNativTool('worktree_merge', {
188
+ description: 'Merge an agent worktree branch into the base branch (enforces Safe Merge Gatekeeper unless force is true).',
189
+ inputSchema: {
190
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
191
+ force: z.boolean().optional().describe('Bypass Safe Merge Gatekeeper checks'),
192
+ },
193
+ }, ({ taskId, force }) => captureOutput(() => runWorktreeMerge(taskId, targetDir, { force, json: true })));
194
+ registerNativTool('worktree_remove', {
195
+ description: 'Safely remove an agent worktree and discard its branch without merging.',
196
+ inputSchema: {
197
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
198
+ force: z.boolean().optional().describe('Force removal'),
199
+ },
200
+ }, ({ taskId, force }) => captureOutput(() => runWorktreeRemove(taskId, targetDir, { force, json: true })));
201
+ // ─── Workspace tools ───────────────────────────────────────────────────────
202
+ registerNativTool('init', {
203
+ description: 'Scaffold the .ai/ workflow (contracts, master plan, sub-agents, directives). Never overwrites existing files.',
204
+ inputSchema: {},
205
+ }, () => captureOutput(() => runInit(targetDir, { force: false })));
206
+ registerNativTool('status', {
207
+ description: 'Summarize execution progress across all milestones and tasks.',
208
+ inputSchema: {},
209
+ }, () => captureOutput(() => runStatus(targetDir)));
210
+ registerNativTool('doctor', {
211
+ description: 'Read-only health check of the Claude Code integration: MCP server, hooks, permissions, agents, enforcement mode and provider availability (JSON). ' +
212
+ 'Reports drift but never repairs it; repairing is `nativ doctor --fix` in a terminal.',
213
+ inputSchema: {},
214
+ },
215
+ // A report that lists problems is still a successful report, not a tool failure.
216
+ () => captureOutput(async () => runDoctor(targetDir, { json: true }), { errorOnExitCode: false }));
217
+ // ─── Database telemetry tools (masked, structure-only) ─────────────────────
218
+ registerNativTool('db_status', {
219
+ description: 'Dev/Prod database connection health, engine, latency and table count. URLs are masked; no credentials are returned.',
220
+ inputSchema: {},
221
+ },
222
+ // An offline database is a valid status report, not a tool failure.
223
+ () => captureOutput(() => runDbStatus(targetDir, { json: true }), { errorOnExitCode: false }));
224
+ registerNativTool('db_inspect', {
225
+ description: 'Introspect live database structure (tables, columns, types, keys, indexes). Structure only; never reads row data.',
226
+ inputSchema: {
227
+ env: z.enum(['dev', 'prod']).optional().describe('Environment to inspect (default dev)'),
228
+ table: z.string().optional().describe('Only return a single table or collection'),
229
+ },
230
+ }, ({ env, table }) => captureOutput(() => runDbInspect(targetDir, { env: env ?? 'dev', table, json: true })));
231
+ registerNativTool('db_diff', {
232
+ description: 'Compute schema drift from the Dev database to .ai/db_schema.json (target "contract", default) or to Prod.',
233
+ inputSchema: {
234
+ target: z.enum(['contract', 'prod']).optional().describe('Comparison target (default contract)'),
235
+ },
236
+ }, ({ target }) => captureOutput(() => runDbDiff(targetDir, { target: target ?? 'contract', json: true })));
237
+ // ─── Test generation ─────────────────────────────────────────────────────
238
+ registerNativTool('test_gen', {
239
+ description: 'Generate API contract and DB integrity test suites from .ai/api_contracts.json and .ai/db_schema.json (JSON result). ' +
240
+ 'Writes into the project unless dryRun is true; never overwrites files it did not generate.',
241
+ inputSchema: {
242
+ output: z.string().optional().describe('Output directory relative to the project (default tests/contract)'),
243
+ framework: z.enum(TEST_FRAMEWORKS).optional().describe('Test framework (default vitest)'),
244
+ baseUrl: z.string().optional().describe('Base URL the API suites call (default http://localhost:3000)'),
245
+ dryRun: z.boolean().optional().describe('Return the generated files without writing them'),
246
+ },
247
+ }, async ({ output, framework, baseUrl, dryRun }) => {
248
+ if (output) {
249
+ const rel = path.relative(targetDir, path.resolve(targetDir, output));
250
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
251
+ return { content: [{ type: 'text', text: 'Error: output must be a subdirectory of the project.' }], isError: true };
252
+ }
253
+ }
254
+ return captureOutput(() => runTestGen(targetDir, { output, framework, baseUrl, dryRun, json: true }));
255
+ });
256
+ // ─── Synthetic Benchmark Matrix ──────────────────────────────────────────
257
+ registerNativTool('bench', {
258
+ description: 'Run synthetic evaluation benchmarks across concurrency, invariants, verifications, and telemetry (JSON report).',
259
+ inputSchema: {
260
+ scenario: z
261
+ .enum(['all', 'concurrency', 'governor', 'verification', 'telemetry', 'e2e'])
262
+ .optional()
263
+ .describe('Target scenario to benchmark (default all)'),
264
+ concurrency: z.number().optional().describe('Number of simulated concurrent agent workers (default 6)'),
265
+ },
266
+ }, async ({ scenario, concurrency }) => captureOutput(() => runBench(targetDir, { scenario, concurrency, json: true })));
267
+ // ─── Contract resources ────────────────────────────────────────────────────
268
+ for (const res of RESOURCES) {
269
+ for (const uriStr of res.uris) {
270
+ server.registerResource(res.name, uriStr, { mimeType: res.mimeType, description: res.description }, async (uri) => {
271
+ const filePath = path.join(targetDir, '.ai', res.file);
272
+ let text;
273
+ if (fs.existsSync(filePath)) {
274
+ text = fs.readFileSync(filePath, 'utf8');
275
+ }
276
+ else if (res.name === 'escalation') {
277
+ text = JSON.stringify({ escalations: [] }, null, 2);
278
+ }
279
+ else if (res.name === 'telemetry') {
280
+ text = JSON.stringify({
281
+ version: '1.0.0',
282
+ summary: {
283
+ totalTasksCompleted: 0,
284
+ totalDurationMs: 0,
285
+ estimatedTotalTokens: 0,
286
+ estimatedTotalCostUsd: 0,
287
+ verificationPassRate: 1.0,
288
+ circuitBreakerTrips: 0,
289
+ },
290
+ tasks: [],
291
+ }, null, 2);
292
+ }
293
+ else {
294
+ throw new Error(`.ai/${res.file} not found in ${targetDir}. Run nativ_init first.`);
295
+ }
296
+ return { contents: [{ uri: uri.href, mimeType: res.mimeType, text }] };
297
+ });
298
+ }
299
+ }
300
+ return server;
301
+ }
302
+ export async function startMcpServer(targetDirArg) {
303
+ // Keep stray console.log calls off stdout, which carries the JSON-RPC stream.
304
+ console.log = console.info = console.error;
305
+ const server = createMcpServer(targetDirArg);
306
+ const transport = new StdioServerTransport();
307
+ await server.connect(transport);
308
+ console.error(`Nativ MCP server running on stdio (project: ${path.resolve(targetDirArg || process.cwd())})`);
309
+ }
@@ -0,0 +1,5 @@
1
+ import { type Provider, type ProviderDeps } from './types.js';
2
+ export declare const CLAUDE_API_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
3
+ /** Resolves the key from .env files or the environment. The value never leaves this module. */
4
+ export declare function resolveAnthropicApiKey(root: string, env?: NodeJS.ProcessEnv): string | null;
5
+ export declare function createClaudeApiProvider(deps?: ProviderDeps): Provider;
@@ -0,0 +1,75 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { loadEnvFiles } from '../db/env-parser.js';
3
+ import { ProviderError, stripJsonFence, } from './types.js';
4
+ export const CLAUDE_API_DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
6
+ /** Resolves the key from .env files or the environment. The value never leaves this module. */
7
+ export function resolveAnthropicApiKey(root, env = process.env) {
8
+ try {
9
+ const key = loadEnvFiles(root).ANTHROPIC_API_KEY || env.ANTHROPIC_API_KEY;
10
+ if (typeof key === 'string' && key.trim().length > 0)
11
+ return key.trim();
12
+ }
13
+ catch {
14
+ // Air-gap guard: an unreadable env file means "no key", never an error that could echo its contents.
15
+ }
16
+ return null;
17
+ }
18
+ function retryAfterMs(err) {
19
+ const headers = err?.headers;
20
+ const raw = headers instanceof Headers ? headers.get('retry-after') : headers?.['retry-after'];
21
+ const seconds = Number(raw);
22
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : undefined;
23
+ }
24
+ export function createClaudeApiProvider(deps = {}) {
25
+ const env = deps.env ?? process.env;
26
+ let client = null;
27
+ const getClient = (root) => {
28
+ if (deps.anthropicClient)
29
+ return deps.anthropicClient();
30
+ if (!client) {
31
+ const apiKey = resolveAnthropicApiKey(root, env);
32
+ if (!apiKey)
33
+ throw new ProviderError('NO_CREDENTIALS', 'ANTHROPIC_API_KEY is not set.');
34
+ client = new Anthropic({ apiKey });
35
+ }
36
+ return client;
37
+ };
38
+ return {
39
+ id: 'claude-api',
40
+ defaultModel: CLAUDE_API_DEFAULT_MODEL,
41
+ hasCredentials(root) {
42
+ return deps.anthropicClient !== undefined || resolveAnthropicApiKey(root, env) !== null;
43
+ },
44
+ async complete(request, root) {
45
+ const sdk = getClient(root);
46
+ const timeout = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
47
+ try {
48
+ const reply = await sdk.messages.create({
49
+ model: request.model ?? CLAUDE_API_DEFAULT_MODEL,
50
+ max_tokens: 2048,
51
+ ...(request.system ? { system: request.system } : {}),
52
+ messages: [{ role: 'user', content: request.prompt }],
53
+ }, { timeout });
54
+ const text = reply.content.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('');
55
+ if (!text.trim())
56
+ throw new ProviderError('BAD_RESPONSE', 'Claude returned an empty result.');
57
+ return request.json ? stripJsonFence(text) : text;
58
+ }
59
+ catch (err) {
60
+ if (err instanceof ProviderError)
61
+ throw err;
62
+ const status = err?.status;
63
+ const name = err?.name ?? '';
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ if (status === 429 || status === 529)
66
+ throw new ProviderError('RATE_LIMITED', message.slice(0, 300), retryAfterMs(err));
67
+ if (status === 401 || status === 403)
68
+ throw new ProviderError('NO_CREDENTIALS', 'The Anthropic API key was rejected.');
69
+ if (/timeout|timed out/i.test(name + message))
70
+ throw new ProviderError('TIMEOUT', `Claude did not answer within ${timeout}ms.`);
71
+ throw new ProviderError('BAD_RESPONSE', message.slice(0, 300));
72
+ }
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,13 @@
1
+ import { type Provider, type ProviderDeps } from './types.js';
2
+ /** Alias understood by `claude --model`; cheap and fast enough for background triage. */
3
+ export declare const CLAUDE_CLI_DEFAULT_MODEL = "haiku";
4
+ /** Set on every child so a nested nativ hook, MCP server or provider never recurses into another call. */
5
+ export declare const PROVIDER_CHILD_ENV = "NATIV_PROVIDER_CHILD";
6
+ /** Default wait when the CLI reports a usage limit without saying when it resets. */
7
+ export declare const DEFAULT_LIMIT_COOLDOWN_MS: number;
8
+ /**
9
+ * Pulls a reset time out of a limit message when there is one: an epoch after a pipe
10
+ * ("Claude usage limit reached|1760000000"), the format Claude Code has used for subscription limits.
11
+ */
12
+ export declare function parseLimitRetryAfter(text: string, now: number): number | undefined;
13
+ export declare function createClaudeCliProvider(deps?: ProviderDeps): Provider;
@@ -0,0 +1,134 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { ProviderError, stripJsonFence, } from './types.js';
6
+ /** Alias understood by `claude --model`; cheap and fast enough for background triage. */
7
+ export const CLAUDE_CLI_DEFAULT_MODEL = 'haiku';
8
+ const DEFAULT_TIMEOUT_MS = 60_000;
9
+ /** Set on every child so a nested nativ hook, MCP server or provider never recurses into another call. */
10
+ export const PROVIDER_CHILD_ENV = 'NATIV_PROVIDER_CHILD';
11
+ let neutralCwd = null;
12
+ /**
13
+ * A scratch directory with no project files. Running from here keeps the child from loading this
14
+ * project's CLAUDE.md, hooks, settings and MCP servers, which would slow the call and could loop back into nativ.
15
+ */
16
+ function neutralWorkingDirectory() {
17
+ if (neutralCwd && fs.existsSync(neutralCwd))
18
+ return neutralCwd;
19
+ neutralCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'nativ-provider-'));
20
+ return neutralCwd;
21
+ }
22
+ function defaultHasBinary(name) {
23
+ const probe = process.platform === 'win32' ? 'where' : 'which';
24
+ try {
25
+ return spawnSync(probe, [name], { stdio: 'ignore' }).status === 0;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function defaultSpawn(command, args, request) {
32
+ return new Promise((resolve) => {
33
+ let stdout = '';
34
+ let stderr = '';
35
+ let timedOut = false;
36
+ let settled = false;
37
+ const finish = (result) => {
38
+ if (settled)
39
+ return;
40
+ settled = true;
41
+ clearTimeout(timer);
42
+ resolve(result);
43
+ };
44
+ // No shell: the prompt travels over stdin and arguments are passed verbatim, so nothing is re-quoted or interpreted.
45
+ const child = spawn(command, args, { cwd: request.cwd, env: request.env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
46
+ const timer = setTimeout(() => {
47
+ timedOut = true;
48
+ child.kill();
49
+ }, request.timeoutMs);
50
+ child.stdout.setEncoding('utf8').on('data', (chunk) => (stdout += chunk));
51
+ child.stderr.setEncoding('utf8').on('data', (chunk) => (stderr += chunk));
52
+ child.on('error', (err) => finish({ code: null, stdout, stderr: err.message, timedOut: false, notFound: err.code === 'ENOENT' }));
53
+ child.on('close', (code) => finish({ code, stdout, stderr, timedOut }));
54
+ child.stdin.on('error', () => undefined);
55
+ child.stdin.end(request.input);
56
+ });
57
+ }
58
+ /** Default wait when the CLI reports a usage limit without saying when it resets. */
59
+ export const DEFAULT_LIMIT_COOLDOWN_MS = 20 * 60_000;
60
+ const LIMIT_PATTERN = /usage limit|rate.?limit|limit reached|too many requests|overloaded|quota/i;
61
+ /**
62
+ * Pulls a reset time out of a limit message when there is one: an epoch after a pipe
63
+ * ("Claude usage limit reached|1760000000"), the format Claude Code has used for subscription limits.
64
+ */
65
+ export function parseLimitRetryAfter(text, now) {
66
+ const epoch = /\|(\d{10})\b/.exec(text);
67
+ if (!epoch)
68
+ return undefined;
69
+ const wait = Number(epoch[1]) * 1000 - now;
70
+ return wait > 0 ? wait : undefined;
71
+ }
72
+ export function createClaudeCliProvider(deps = {}) {
73
+ const run = deps.spawn ?? defaultSpawn;
74
+ const hasBinary = deps.hasBinary ?? defaultHasBinary;
75
+ const now = deps.now ?? Date.now;
76
+ const baseEnv = deps.env ?? process.env;
77
+ return {
78
+ id: 'claude-cli',
79
+ defaultModel: CLAUDE_CLI_DEFAULT_MODEL,
80
+ // A Claude Code login lives in the CLI's own store; the binary being present is the best offline signal we have.
81
+ hasCredentials() {
82
+ if (baseEnv[PROVIDER_CHILD_ENV] === '1')
83
+ return false;
84
+ return hasBinary('claude');
85
+ },
86
+ async complete(request) {
87
+ if (baseEnv[PROVIDER_CHILD_ENV] === '1') {
88
+ throw new ProviderError('NO_CREDENTIALS', 'Refusing to start Claude from inside a nativ provider call.');
89
+ }
90
+ const args = [
91
+ '-p',
92
+ '--output-format', 'json',
93
+ '--model', request.model ?? CLAUDE_CLI_DEFAULT_MODEL,
94
+ '--tools', '',
95
+ '--strict-mcp-config',
96
+ '--no-session-persistence',
97
+ ];
98
+ if (request.system)
99
+ args.push('--system-prompt', request.system);
100
+ const result = await run('claude', args, {
101
+ cwd: neutralWorkingDirectory(),
102
+ env: { ...baseEnv, [PROVIDER_CHILD_ENV]: '1' },
103
+ input: request.prompt,
104
+ timeoutMs: request.timeoutMs ?? DEFAULT_TIMEOUT_MS,
105
+ });
106
+ if (result.notFound)
107
+ throw new ProviderError('NO_CREDENTIALS', 'The claude executable was not found on PATH.');
108
+ if (result.timedOut)
109
+ throw new ProviderError('TIMEOUT', `Claude did not answer within ${request.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`);
110
+ let parsed = null;
111
+ try {
112
+ parsed = JSON.parse(result.stdout);
113
+ }
114
+ catch {
115
+ parsed = null;
116
+ }
117
+ const text = typeof parsed?.result === 'string' ? parsed.result : '';
118
+ const failed = result.code !== 0 || parsed === null || parsed.is_error === true;
119
+ if (failed) {
120
+ const detail = `${text} ${result.stderr}`.trim();
121
+ if (parsed?.api_error_status === 429 || LIMIT_PATTERN.test(detail)) {
122
+ throw new ProviderError('RATE_LIMITED', detail.slice(0, 300) || 'Claude usage limit reached.', parseLimitRetryAfter(detail, now()) ?? DEFAULT_LIMIT_COOLDOWN_MS);
123
+ }
124
+ if (/not logged in|please run .*login|invalid api key|authentication/i.test(detail)) {
125
+ throw new ProviderError('NO_CREDENTIALS', 'Claude Code is not logged in. Run `claude` once to sign in.');
126
+ }
127
+ throw new ProviderError('BAD_RESPONSE', (detail || `claude exited with code ${result.code}`).slice(0, 300));
128
+ }
129
+ if (!text.trim())
130
+ throw new ProviderError('BAD_RESPONSE', 'Claude returned an empty result.');
131
+ return request.json ? stripJsonFence(text) : text;
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,8 @@
1
+ import { type Provider, type ProviderDeps } from './types.js';
2
+ export declare const GEMINI_DEFAULT_MODEL = "gemini-3.8-flash";
3
+ /**
4
+ * Resolves the Google/Gemini API key from .env files or the environment.
5
+ * Zero-credential air-gap: the returned key is never stored in contracts or responses.
6
+ */
7
+ export declare function resolveGeminiApiKey(root: string, env?: NodeJS.ProcessEnv): string | null;
8
+ export declare function createGeminiProvider(deps?: ProviderDeps): Provider;
@@ -0,0 +1,78 @@
1
+ import { loadEnvFiles } from '../db/env-parser.js';
2
+ import { ProviderError, stripJsonFence } from './types.js';
3
+ export const GEMINI_DEFAULT_MODEL = 'gemini-3.8-flash';
4
+ /** A stalled call aborts here and the caller falls through to the next provider. */
5
+ const DEFAULT_TIMEOUT_MS = 15_000;
6
+ /**
7
+ * Resolves the Google/Gemini API key from .env files or the environment.
8
+ * Zero-credential air-gap: the returned key is never stored in contracts or responses.
9
+ */
10
+ export function resolveGeminiApiKey(root, env = process.env) {
11
+ try {
12
+ const envVars = loadEnvFiles(root);
13
+ const key = envVars.GEMINI_API_KEY || envVars.GOOGLE_API_KEY || env.GEMINI_API_KEY || env.GOOGLE_API_KEY;
14
+ if (typeof key === 'string' && key.trim().length > 0)
15
+ return key.trim();
16
+ }
17
+ catch {
18
+ // Air-gap guard: suppress file read errors
19
+ }
20
+ return null;
21
+ }
22
+ export function createGeminiProvider(deps = {}) {
23
+ const env = deps.env ?? process.env;
24
+ const doFetch = deps.fetch ?? globalThis.fetch;
25
+ return {
26
+ id: 'gemini',
27
+ defaultModel: GEMINI_DEFAULT_MODEL,
28
+ hasCredentials(root) {
29
+ return resolveGeminiApiKey(root, env) !== null;
30
+ },
31
+ async complete(request, root) {
32
+ const apiKey = resolveGeminiApiKey(root, env);
33
+ if (!apiKey)
34
+ throw new ProviderError('NO_CREDENTIALS', 'GEMINI_API_KEY is not set.');
35
+ const model = request.model ?? GEMINI_DEFAULT_MODEL;
36
+ const timeout = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
37
+ const text = request.system ? `${request.system}\n\n${request.prompt}` : request.prompt;
38
+ let res;
39
+ try {
40
+ // The key travels in a header, never the URL, so it cannot leak through logged or proxied URLs.
41
+ res = await doFetch(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },
44
+ body: JSON.stringify({
45
+ contents: [{ parts: [{ text }] }],
46
+ generationConfig: { ...(request.json ? { responseMimeType: 'application/json' } : {}), temperature: 0.2 },
47
+ }),
48
+ signal: AbortSignal.timeout(timeout),
49
+ });
50
+ }
51
+ catch (err) {
52
+ const name = err?.name ?? '';
53
+ if (name === 'TimeoutError' || name === 'AbortError')
54
+ throw new ProviderError('TIMEOUT', `Gemini did not answer within ${timeout}ms.`);
55
+ throw new ProviderError('BAD_RESPONSE', err instanceof Error ? err.message : String(err));
56
+ }
57
+ if (res.status === 429) {
58
+ const seconds = Number(res.headers.get('retry-after'));
59
+ throw new ProviderError('RATE_LIMITED', 'Gemini rate limit reached.', Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : undefined);
60
+ }
61
+ if (res.status === 401 || res.status === 403)
62
+ throw new ProviderError('NO_CREDENTIALS', 'The Gemini API key was rejected.');
63
+ if (!res.ok)
64
+ throw new ProviderError('BAD_RESPONSE', `Gemini responded with HTTP ${res.status}.`);
65
+ let body;
66
+ try {
67
+ body = (await res.json());
68
+ }
69
+ catch {
70
+ throw new ProviderError('BAD_RESPONSE', 'Gemini returned a body that is not JSON.');
71
+ }
72
+ const out = body.candidates?.[0]?.content?.parts?.[0]?.text;
73
+ if (!out)
74
+ throw new ProviderError('BAD_RESPONSE', 'Gemini returned no text.');
75
+ return request.json ? stripJsonFence(out) : out;
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './claude-cli.js';
3
+ export * from './claude-api.js';
4
+ export * from './gemini.js';
5
+ export * from './registry.js';
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './claude-cli.js';
3
+ export * from './claude-api.js';
4
+ export * from './gemini.js';
5
+ export * from './registry.js';