@njeybe/nativ 2.4.0 → 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 (140) hide show
  1. package/CHANGELOG.md +31 -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/spec-slices.js +119 -7
  47. package/dist/core/tier1-liaison.d.ts +2 -0
  48. package/dist/core/tier1-liaison.js +64 -3
  49. package/dist/core/verifier/executor.d.ts +5 -0
  50. package/dist/core/verifier/executor.js +62 -0
  51. package/dist/core/verifier/phases.d.ts +5 -0
  52. package/dist/core/verifier/phases.js +71 -0
  53. package/dist/core/verifier/task-verifier.d.ts +37 -0
  54. package/dist/core/verifier/task-verifier.js +233 -0
  55. package/dist/core/verifier/types.d.ts +51 -0
  56. package/dist/core/verifier/types.js +1 -0
  57. package/dist/core/verifier.d.ts +5 -93
  58. package/dist/core/verifier.js +5 -359
  59. package/dist/db/introspector.js +59 -59
  60. package/dist/governor/evaluator.d.ts +4 -64
  61. package/dist/governor/evaluator.js +21 -686
  62. package/dist/governor/patch-applier.d.ts +8 -0
  63. package/dist/governor/patch-applier.js +121 -0
  64. package/dist/governor/self-healing.d.ts +24 -0
  65. package/dist/governor/self-healing.js +351 -0
  66. package/dist/governor/test-integrity.d.ts +41 -0
  67. package/dist/governor/test-integrity.js +237 -0
  68. package/dist/index.js +8 -0
  69. package/dist/mcp/capture.d.ts +4 -0
  70. package/dist/mcp/capture.js +33 -0
  71. package/dist/mcp/resources.d.ts +39 -0
  72. package/dist/mcp/resources.js +44 -0
  73. package/dist/mcp/server.d.ts +11 -0
  74. package/dist/mcp/server.js +14 -89
  75. package/dist/mcp/tools.d.ts +1 -0
  76. package/dist/mcp/tools.js +1 -0
  77. package/dist/runner/agent-supervisor.d.ts +8 -302
  78. package/dist/runner/agent-supervisor.js +41 -1030
  79. package/dist/runner/cli-config.d.ts +84 -0
  80. package/dist/runner/cli-config.js +215 -0
  81. package/dist/runner/native-driver.d.ts +18 -0
  82. package/dist/runner/native-driver.js +208 -0
  83. package/dist/runner/native-engine.d.ts +27 -0
  84. package/dist/runner/native-engine.js +184 -0
  85. package/dist/runner/native-tools.d.ts +31 -0
  86. package/dist/runner/native-tools.js +241 -0
  87. package/dist/runner/process-utils.d.ts +12 -0
  88. package/dist/runner/process-utils.js +96 -0
  89. package/dist/runner/run-lifecycle.d.ts +6 -0
  90. package/dist/runner/run-lifecycle.js +42 -0
  91. package/dist/runner/types.d.ts +215 -0
  92. package/dist/runner/types.js +41 -0
  93. package/dist/scanner/detector.js +42 -663
  94. package/dist/scanner/detectors/ecosystem-detector.d.ts +16 -0
  95. package/dist/scanner/detectors/ecosystem-detector.js +354 -0
  96. package/dist/scanner/detectors/orm-detector.d.ts +5 -0
  97. package/dist/scanner/detectors/orm-detector.js +59 -0
  98. package/dist/scanner/detectors/workspace-detector.d.ts +19 -0
  99. package/dist/scanner/detectors/workspace-detector.js +266 -0
  100. package/dist/server/cli-runner.d.ts +15 -0
  101. package/dist/server/cli-runner.js +43 -0
  102. package/dist/server/event-hub.d.ts +42 -0
  103. package/dist/server/event-hub.js +169 -0
  104. package/dist/server/http-utils.d.ts +15 -0
  105. package/dist/server/http-utils.js +56 -0
  106. package/dist/server/plan-utils.d.ts +9 -0
  107. package/dist/server/plan-utils.js +31 -0
  108. package/dist/server/routes/db-routes.d.ts +57 -0
  109. package/dist/server/routes/db-routes.js +484 -0
  110. package/dist/server/routes/task-routes.d.ts +266 -0
  111. package/dist/server/routes/task-routes.js +285 -0
  112. package/dist/server/routes/telemetry-routes.d.ts +128 -0
  113. package/dist/server/routes/telemetry-routes.js +179 -0
  114. package/dist/server/routes/triage-routes.d.ts +81 -0
  115. package/dist/server/routes/triage-routes.js +244 -0
  116. package/dist/server/routes/worktree-routes.d.ts +31 -0
  117. package/dist/server/routes/worktree-routes.js +144 -0
  118. package/dist/server/studio-server.d.ts +4 -11
  119. package/dist/server/studio-server.js +34 -1578
  120. package/dist/server/studio-ui/shell/pipeline-state.js +2 -0
  121. package/dist/server/studio-ui/shell/triage.js +10 -0
  122. package/package.json +2 -2
  123. package/plugin/.claude-plugin/plugin.json +1 -1
  124. package/plugin/agents/architect.md +4 -2
  125. package/plugin/agents/verifier.md +1 -1
  126. package/plugin/agents/worker.md +7 -4
  127. package/plugin/skills/nativ/SKILL.md +1 -0
  128. package/templates/.claudeignore +62 -45
  129. package/templates/AGENTS.md +1 -0
  130. package/templates/CLAUDE.md +13 -0
  131. package/templates/claude-agents/architect.md +4 -2
  132. package/templates/claude-agents/verifier.md +1 -1
  133. package/templates/claude-agents/worker.md +7 -4
  134. package/templates/dot-ai/api_contracts.json +66 -66
  135. package/templates/dot-ai/context.md +38 -38
  136. package/templates/dot-ai/db_schema.json +39 -39
  137. package/dist/commands/session.d.ts +0 -22
  138. package/dist/commands/session.js +0 -106
  139. package/dist/core/session-memory.d.ts +0 -42
  140. package/dist/core/session-memory.js +0 -121
@@ -0,0 +1,84 @@
1
+ import type { MasterPlanTask } from '../scanner/types.js';
2
+ /** Credentials never reach the model's shell: it could print them. */
3
+ export declare const SECRET_ENV_PATTERN: RegExp;
4
+ export declare const CLAUDE_CODE_AUTH_ENV: RegExp;
5
+ export declare const AWS_ENV: RegExp;
6
+ /**
7
+ * What a dispatched agent may run without asking. `claude -p` cannot prompt, so anything not listed here is
8
+ * denied and the agent blocks or escalates instead of running it.
9
+ */
10
+ export declare const RUNNER_BASE_ALLOWED_TOOLS: string[];
11
+ /** The pieces of a compound command as Claude Code matches them: each must satisfy an allow rule on its own. */
12
+ export declare function splitCompoundCommand(command: string): string[];
13
+ /** Operator-supplied extra rules from NATIV_RUNNER_ALLOW, comma separated, e.g. `Bash(pnpm test *),Bash(cargo test *)`. */
14
+ export declare function parseRunnerAllowList(raw: string | undefined): string[];
15
+ export interface RunnerPermissions {
16
+ permissions: {
17
+ allow: string[];
18
+ deny: string[];
19
+ ask: string[];
20
+ };
21
+ /** Omitted when the project's own settings already run the enforcement hook, so it never runs (and logs) twice. */
22
+ hooks?: {
23
+ PreToolUse: Array<{
24
+ matcher: string;
25
+ hooks: Array<{
26
+ type: 'command';
27
+ command: string;
28
+ timeout: number;
29
+ }>;
30
+ }>;
31
+ };
32
+ }
33
+ /** True when `.claude/settings.json` already registers a nativ `hook check` command. */
34
+ export declare function projectHasEnforcementHook(rootDir: string): boolean;
35
+ /**
36
+ * Settings handed to a dispatched agent with `--settings`. The deny and ask rules and the enforcement hook come
37
+ * from the same source as `nativ setup`, so a dispatched agent is guarded even in a project where setup never ran.
38
+ */
39
+ export declare function buildRunnerPermissions(task: Pick<MasterPlanTask, 'verificationCommand'>, rootDir: string, extraAllowed?: readonly string[]): RunnerPermissions;
40
+ /** Where a task's runner settings live: a subdirectory, because run history parses every top-level .json in runs/. */
41
+ export declare function runnerSettingsPath(runsDir: string, taskId: string): string;
42
+ export declare function writeRunnerSettings(runsDir: string, taskId: string, settings: RunnerPermissions): string;
43
+ /** The instructions text handed to the agent on its standard input. */
44
+ export declare function buildRunnerPrompt(task: MasterPlanTask): string;
45
+ /**
46
+ * Arguments for Claude Code CLI to run headless. The prompt goes on stdin, never in this string:
47
+ * quotes or symbols in task text would break the shell.
48
+ *
49
+ * It runs in `acceptEdits` mode with the rules in `settingsFile` (see buildRunnerPermissions); it does not bypass
50
+ * permissions. Without a settings file only the file-edit tools and read-only commands are pre-approved.
51
+ */
52
+ export declare function buildDefaultClaudeCommand(_task: MasterPlanTask, model?: string | null, settingsFile?: string): string;
53
+ /** True when a runner command asks Claude Code for its line-delimited JSON event stream. */
54
+ export declare function usesClaudeStreamJson(command: string): boolean;
55
+ /** Final `result` event of a Claude Code stream: grounded usage for cli runs. */
56
+ export interface ClaudeStreamResult {
57
+ isError: boolean;
58
+ turns: number;
59
+ costUsd: number | null;
60
+ inputTokens: number;
61
+ outputTokens: number;
62
+ cacheCreationTokens: number;
63
+ cacheReadTokens: number;
64
+ }
65
+ export interface ClaudeStreamLine {
66
+ /** Human-readable log text (newline-terminated), or null when the event is not worth showing. */
67
+ text: string | null;
68
+ model?: string;
69
+ result?: ClaudeStreamResult;
70
+ }
71
+ /**
72
+ * Turns one line of `claude -p --output-format stream-json` into a log line. Anything that is not
73
+ * a recognised event (warnings, custom runners, future event types) passes through or is dropped,
74
+ * never thrown on.
75
+ */
76
+ export declare function formatClaudeStreamLine(line: string): ClaudeStreamLine;
77
+ /**
78
+ * Environment for cli runners. The agent inside can run the commands its allowlist permits (and any custom
79
+ * runner command runs unrestricted), so it can print anything it inherits: credential-looking variables are
80
+ * withheld, except the ones Claude Code itself authenticates with (and AWS credentials when it is configured for Bedrock).
81
+ * NATIV_RUNNER_PASS_ENV="NAME1,NAME2" passes extra variables through deliberately; variables an
82
+ * operator hands to dispatch() explicitly are also passed as-is.
83
+ */
84
+ export declare function buildRunnerEnv(base: NodeJS.ProcessEnv, explicit?: Record<string, string>): NodeJS.ProcessEnv;
@@ -0,0 +1,215 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { buildDesiredConfig, cliString, configuredInvocation, resolveCliInvocation, } from '../core/setup-assets.js';
4
+ /** Credentials never reach the model's shell: it could print them. */
5
+ export const SECRET_ENV_PATTERN = /^ANTHROPIC_|^CLAUDE_CODE_OAUTH|SECRET|TOKEN|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY|DATABASE_URL|_DSN$|CREDENTIAL/i;
6
+ export const CLAUDE_CODE_AUTH_ENV = /^(ANTHROPIC_(API_KEY|AUTH_TOKEN|BASE_URL|MODEL|SMALL_FAST_MODEL|CUSTOM_HEADERS|BETAS|VERTEX_PROJECT_ID)|CLAUDE_CODE_[A-Z0-9_]+|CLAUDE_CONFIG_DIR|CLOUD_ML_REGION|VERTEX_REGION_[A-Z0-9_]+)$/;
7
+ export const AWS_ENV = /^AWS_[A-Z0-9_]+$/;
8
+ /**
9
+ * What a dispatched agent may run without asking. `claude -p` cannot prompt, so anything not listed here is
10
+ * denied and the agent blocks or escalates instead of running it.
11
+ */
12
+ export const RUNNER_BASE_ALLOWED_TOOLS = [
13
+ 'Bash(nativ *)',
14
+ 'Bash(git status *)',
15
+ 'Bash(git diff *)',
16
+ 'Bash(git log *)',
17
+ 'Bash(git add *)',
18
+ 'Bash(git commit *)',
19
+ 'Bash(git checkout -- *)',
20
+ ];
21
+ /** The pieces of a compound command as Claude Code matches them: each must satisfy an allow rule on its own. */
22
+ export function splitCompoundCommand(command) {
23
+ return [...new Set(command.split(/&&|\|\||[;|\n]/).map((part) => part.trim()).filter(Boolean))];
24
+ }
25
+ /** Operator-supplied extra rules from NATIV_RUNNER_ALLOW, comma separated, e.g. `Bash(pnpm test *),Bash(cargo test *)`. */
26
+ export function parseRunnerAllowList(raw) {
27
+ return String(raw ?? '')
28
+ .split(',')
29
+ .map((rule) => rule.trim())
30
+ .filter((rule) => /^[A-Za-z_][A-Za-z0-9_]*(\(.*\))?$/.test(rule));
31
+ }
32
+ /** True when `.claude/settings.json` already registers a nativ `hook check` command. */
33
+ export function projectHasEnforcementHook(rootDir) {
34
+ try {
35
+ const text = fs.readFileSync(path.join(rootDir, '.claude', 'settings.json'), 'utf8');
36
+ return /"command"\s*:\s*"[^"]*\bhook check\b/.test(text);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * Settings handed to a dispatched agent with `--settings`. The deny and ask rules and the enforcement hook come
44
+ * from the same source as `nativ setup`, so a dispatched agent is guarded even in a project where setup never ran.
45
+ */
46
+ export function buildRunnerPermissions(task, rootDir, extraAllowed = []) {
47
+ const invocation = configuredInvocation(rootDir) ?? resolveCliInvocation({});
48
+ const desired = buildDesiredConfig(invocation);
49
+ const cli = cliString(invocation);
50
+ const allow = [
51
+ ...RUNNER_BASE_ALLOWED_TOOLS,
52
+ ...(cli === 'nativ' ? [] : [`Bash(${cli} *)`]),
53
+ ...splitCompoundCommand(task.verificationCommand ?? '').map((command) => `Bash(${command})`),
54
+ ...extraAllowed,
55
+ ];
56
+ return {
57
+ permissions: { allow: [...new Set(allow)], deny: desired.deny, ask: desired.ask },
58
+ ...(projectHasEnforcementHook(rootDir)
59
+ ? {}
60
+ : { hooks: { PreToolUse: [{ matcher: desired.preToolUse.matcher, hooks: [{ type: 'command', command: desired.preToolUse.command, timeout: desired.preToolUse.timeout }] }] } }),
61
+ };
62
+ }
63
+ /** Where a task's runner settings live: a subdirectory, because run history parses every top-level .json in runs/. */
64
+ export function runnerSettingsPath(runsDir, taskId) {
65
+ return path.join(runsDir, 'permissions', `${taskId}.json`).replace(/\\/g, '/');
66
+ }
67
+ export function writeRunnerSettings(runsDir, taskId, settings) {
68
+ const file = runnerSettingsPath(runsDir, taskId);
69
+ fs.mkdirSync(path.dirname(file), { recursive: true });
70
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf8');
71
+ return file;
72
+ }
73
+ /** The instructions text handed to the agent on its standard input. */
74
+ export function buildRunnerPrompt(task) {
75
+ return [
76
+ `Execute task ${task.id} (${task.title}).`,
77
+ task.description ? `Description: ${task.description}.` : '',
78
+ task.verificationCommand ? `Verify your work using: ${task.verificationCommand}.` : '',
79
+ `Start by running: nativ task start ${task.id}. When finished and verified, run: nativ task complete ${task.id}.`,
80
+ `If blocked, follow the Human-Centric Communication Protocol in CLAUDE.md: explain the user experience symptom, root cause in plain English, and clear options without technical jargon.`,
81
+ ]
82
+ .filter(Boolean)
83
+ .join(' ');
84
+ }
85
+ /**
86
+ * Arguments for Claude Code CLI to run headless. The prompt goes on stdin, never in this string:
87
+ * quotes or symbols in task text would break the shell.
88
+ *
89
+ * It runs in `acceptEdits` mode with the rules in `settingsFile` (see buildRunnerPermissions); it does not bypass
90
+ * permissions. Without a settings file only the file-edit tools and read-only commands are pre-approved.
91
+ */
92
+ export function buildDefaultClaudeCommand(_task, model, settingsFile) {
93
+ const modelFlag = model ? ` --model ${model}` : '';
94
+ // Plain `claude -p` prints nothing until it finishes; stream-json emits every message and tool call as it happens.
95
+ const settingsFlag = settingsFile ? ` --settings "${settingsFile}"` : '';
96
+ return `claude -p${modelFlag} --output-format stream-json --verbose --permission-mode acceptEdits${settingsFlag}`;
97
+ }
98
+ /** True when a runner command asks Claude Code for its line-delimited JSON event stream. */
99
+ export function usesClaudeStreamJson(command) {
100
+ return /--output-format(?:=|\s+)stream-json\b/.test(command);
101
+ }
102
+ function firstLine(text, max = 200) {
103
+ const line = String(text ?? '').split('\n').find((l) => l.trim()) ?? '';
104
+ return line.length > max ? `${line.slice(0, max)}...` : line.trim();
105
+ }
106
+ function describeToolInput(input) {
107
+ if (!input || typeof input !== 'object')
108
+ return '';
109
+ const i = input;
110
+ const pick = i.command ?? i.file_path ?? i.path ?? i.pattern ?? i.url ?? i.description;
111
+ return typeof pick === 'string' ? firstLine(pick, 160) : firstLine(JSON.stringify(input), 160);
112
+ }
113
+ const finiteCount = (value) => (typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0);
114
+ /**
115
+ * Turns one line of `claude -p --output-format stream-json` into a log line. Anything that is not
116
+ * a recognised event (warnings, custom runners, future event types) passes through or is dropped,
117
+ * never thrown on.
118
+ */
119
+ export function formatClaudeStreamLine(line) {
120
+ const trimmed = line.trim();
121
+ if (!trimmed)
122
+ return { text: null };
123
+ let event;
124
+ try {
125
+ event = JSON.parse(trimmed);
126
+ }
127
+ catch {
128
+ return { text: `${line}\n` };
129
+ }
130
+ if (!event || typeof event !== 'object' || Array.isArray(event))
131
+ return { text: `${line}\n` };
132
+ switch (event.type) {
133
+ case 'system': {
134
+ if (event.subtype !== 'init')
135
+ return { text: null };
136
+ const model = typeof event.model === 'string' ? event.model : undefined;
137
+ const tools = Array.isArray(event.tools) ? ` · ${event.tools.length} tools` : '';
138
+ return { text: `[claude] session started · model ${model ?? 'default'}${tools}\n`, model };
139
+ }
140
+ case 'assistant': {
141
+ const lines = [];
142
+ for (const block of Array.isArray(event.message?.content) ? event.message.content : []) {
143
+ if (block?.type === 'text' && String(block.text ?? '').trim())
144
+ lines.push(String(block.text).trim());
145
+ else if (block?.type === 'thinking' && String(block.thinking ?? '').trim())
146
+ lines.push(`[claude:thinking] ${firstLine(block.thinking, 300)}`);
147
+ else if (block?.type === 'tool_use')
148
+ lines.push(`[claude] ${block.name}: ${describeToolInput(block.input)}`);
149
+ }
150
+ return { text: lines.length ? `${lines.join('\n')}\n` : null };
151
+ }
152
+ case 'user': {
153
+ const lines = [];
154
+ for (const block of Array.isArray(event.message?.content) ? event.message.content : []) {
155
+ if (block?.type !== 'tool_result')
156
+ continue;
157
+ const body = typeof block.content === 'string'
158
+ ? block.content
159
+ : Array.isArray(block.content) ? block.content.map((c) => c?.text ?? '').join('\n') : '';
160
+ const summary = firstLine(body, 160);
161
+ if (block.is_error)
162
+ lines.push(`[claude] error: ${summary || 'tool failed'}`);
163
+ else if (summary)
164
+ lines.push(`[claude] ${summary}`);
165
+ }
166
+ return { text: lines.length ? `${lines.join('\n')}\n` : null };
167
+ }
168
+ case 'result': {
169
+ const usage = event.usage ?? {};
170
+ const cost = typeof event.total_cost_usd === 'number' && Number.isFinite(event.total_cost_usd) ? event.total_cost_usd : null;
171
+ const turns = finiteCount(event.num_turns);
172
+ const seconds = finiteCount(event.duration_ms) ? ` · ${Math.round(event.duration_ms / 1000)}s` : '';
173
+ const kind = event.subtype && event.subtype !== 'success' ? ` (${event.subtype})` : '';
174
+ const text = `[claude] ${event.is_error ? 'finished with an error' : 'finished'}${kind}` +
175
+ (turns ? ` · ${turns} turns` : '') + (cost !== null ? ` · $${cost.toFixed(4)}` : '') + seconds + '\n';
176
+ return {
177
+ text,
178
+ result: {
179
+ isError: Boolean(event.is_error),
180
+ turns,
181
+ costUsd: cost,
182
+ inputTokens: finiteCount(usage.input_tokens),
183
+ outputTokens: finiteCount(usage.output_tokens),
184
+ cacheCreationTokens: finiteCount(usage.cache_creation_input_tokens),
185
+ cacheReadTokens: finiteCount(usage.cache_read_input_tokens),
186
+ },
187
+ };
188
+ }
189
+ default:
190
+ return { text: null };
191
+ }
192
+ }
193
+ /**
194
+ * Environment for cli runners. The agent inside can run the commands its allowlist permits (and any custom
195
+ * runner command runs unrestricted), so it can print anything it inherits: credential-looking variables are
196
+ * withheld, except the ones Claude Code itself authenticates with (and AWS credentials when it is configured for Bedrock).
197
+ * NATIV_RUNNER_PASS_ENV="NAME1,NAME2" passes extra variables through deliberately; variables an
198
+ * operator hands to dispatch() explicitly are also passed as-is.
199
+ */
200
+ export function buildRunnerEnv(base, explicit) {
201
+ const passThrough = new Set(String(base.NATIV_RUNNER_PASS_ENV ?? '')
202
+ .split(',')
203
+ .map((name) => name.trim())
204
+ .filter(Boolean));
205
+ const bedrock = Boolean(base.CLAUDE_CODE_USE_BEDROCK);
206
+ const env = {};
207
+ for (const [key, value] of Object.entries(base)) {
208
+ if (value === undefined)
209
+ continue;
210
+ const allowed = !SECRET_ENV_PATTERN.test(key) || CLAUDE_CODE_AUTH_ENV.test(key) || passThrough.has(key) || (bedrock && AWS_ENV.test(key));
211
+ if (allowed)
212
+ env[key] = value;
213
+ }
214
+ return { ...env, ...explicit };
215
+ }
@@ -0,0 +1,18 @@
1
+ import type { MasterPlanTask } from '../scanner/types.js';
2
+ import { type ActualTokenUsage } from '../core/telemetry.js';
3
+ import { type ActiveRun, type RunnerRecord } from './types.js';
4
+ export interface NativeDriverContext {
5
+ rootDir: string;
6
+ nativeMaxTurns: number;
7
+ nativeToolTimeoutMs: number;
8
+ nativeAllowedCommands: ReadonlySet<string>;
9
+ toolShell: string | boolean;
10
+ appendLog: (run: ActiveRun, stream: 'stdout' | 'stderr', chunk: string) => void;
11
+ onTurnUsage: (record: RunnerRecord, delta: ActualTokenUsage, model: string) => void;
12
+ }
13
+ export declare function readRoleGuide(rootDir: string, task: MasterPlanTask): string | null;
14
+ /**
15
+ * Manual agentic loop over the Messages API: stream a turn, capture usage,
16
+ * execute the requested local tools, repeat until the model ends its turn.
17
+ */
18
+ export declare function runNativeLoop(ctx: NativeDriverContext, run: ActiveRun, useWorktree: boolean): Promise<void>;
@@ -0,0 +1,208 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { computeActualCostUsd, emptyActualUsage, mergeActualUsage, } from '../core/telemetry.js';
5
+ import { NATIVE_MAX_OUTPUT_TOKENS, } from './types.js';
6
+ import { killProcessTree } from './process-utils.js';
7
+ import { NATIVE_TOOLS, ToolInputError, buildToolEnv, checkNativeBashCommand, clipToolOutput, requireString, runEditorCommand, } from './native-tools.js';
8
+ import { NATIVE_SYSTEM_PROMPT, buildNativeTaskPrompt, supportsServerFallback, } from './native-engine.js';
9
+ const NATIVE_TOOL_CAPTURE_LIMIT = 1024 * 1024;
10
+ const ROLE_GUIDE_LIMIT = 20_000;
11
+ const SERVER_FALLBACK_BETA = 'server-side-fallback-2026-07-01';
12
+ export function readRoleGuide(rootDir, task) {
13
+ if (!/^[\w-]+$/.test(task.assignedSubagent || ''))
14
+ return null;
15
+ try {
16
+ const text = fs.readFileSync(path.join(rootDir, '.ai', 'subagents', `${task.assignedSubagent}.md`), 'utf8');
17
+ return text.length > ROLE_GUIDE_LIMIT ? `${text.slice(0, ROLE_GUIDE_LIMIT)}\n[... truncated ...]` : text;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * Manual agentic loop over the Messages API: stream a turn, capture usage,
25
+ * execute the requested local tools, repeat until the model ends its turn.
26
+ */
27
+ export async function runNativeLoop(ctx, run, useWorktree) {
28
+ const native = run.native;
29
+ const { thinking } = native;
30
+ const fallback = supportsServerFallback(native.model);
31
+ const messages = [
32
+ { role: 'user', content: buildNativeTaskPrompt(run.task, readRoleGuide(ctx.rootDir, run.task), useWorktree, ctx.rootDir) },
33
+ ];
34
+ for (let turn = 1;; turn++) {
35
+ if (run.settled)
36
+ return;
37
+ if (turn > ctx.nativeMaxTurns) {
38
+ throw new Error(`Native engine stopped after the ${ctx.nativeMaxTurns}-turn budget without finishing`);
39
+ }
40
+ const stream = native.client.beta.messages.stream({
41
+ model: native.model,
42
+ max_tokens: NATIVE_MAX_OUTPUT_TOKENS,
43
+ system: [{ type: 'text', text: NATIVE_SYSTEM_PROMPT, cache_control: { type: 'ephemeral' } }],
44
+ cache_control: { type: 'ephemeral' },
45
+ tools: NATIVE_TOOLS,
46
+ messages,
47
+ ...(thinking.budgetTokens
48
+ ? { thinking: { type: 'enabled', budget_tokens: thinking.budgetTokens } }
49
+ : thinking.effort
50
+ ? { thinking: { type: 'adaptive', display: 'summarized' } }
51
+ : {}),
52
+ ...(thinking.effort ? { output_config: { effort: thinking.effort } } : {}),
53
+ ...(fallback ? { betas: [SERVER_FALLBACK_BETA], fallbacks: 'default' } : {}),
54
+ }, { signal: native.controller.signal });
55
+ let streaming = null;
56
+ const switchTo = (kind) => {
57
+ if (streaming === kind)
58
+ return;
59
+ ctx.appendLog(run, 'stdout', `${streaming ? '\n' : ''}${kind === 'thinking' ? '[native:thinking] ' : ''}`);
60
+ streaming = kind;
61
+ };
62
+ stream.on('thinking', (delta) => {
63
+ if (!delta)
64
+ return;
65
+ switchTo('thinking');
66
+ ctx.appendLog(run, 'stdout', delta);
67
+ });
68
+ stream.on('text', (delta) => {
69
+ switchTo('text');
70
+ ctx.appendLog(run, 'stdout', delta);
71
+ });
72
+ const message = await stream.finalMessage();
73
+ if (streaming)
74
+ ctx.appendLog(run, 'stdout', '\n');
75
+ if (run.settled)
76
+ return;
77
+ recordTurnUsage(ctx, run, message);
78
+ if (message.stop_reason === 'refusal') {
79
+ const category = message.stop_details?.category;
80
+ throw new Error(`Model declined the task (refusal${category ? `: ${category}` : ''})`);
81
+ }
82
+ if (message.stop_reason === 'pause_turn') {
83
+ messages.push({ role: 'assistant', content: message.content });
84
+ continue;
85
+ }
86
+ const toolUses = message.content.filter((b) => b.type === 'tool_use');
87
+ if (message.stop_reason === 'max_tokens') {
88
+ throw new Error(`Model output hit max_tokens (${NATIVE_MAX_OUTPUT_TOKENS}) mid-turn`);
89
+ }
90
+ if (toolUses.length === 0)
91
+ return;
92
+ messages.push({ role: 'assistant', content: message.content });
93
+ const results = [];
94
+ for (const block of toolUses) {
95
+ if (run.settled)
96
+ return;
97
+ results.push(await executeNativeTool(ctx, run, block));
98
+ }
99
+ messages.push({ role: 'user', content: results });
100
+ }
101
+ }
102
+ function recordTurnUsage(ctx, run, message) {
103
+ const { record } = run;
104
+ const usage = message.usage;
105
+ const model = message.model || run.native.model;
106
+ const details = usage.output_tokens_details;
107
+ const slice = {
108
+ inputTokens: usage.input_tokens ?? 0,
109
+ outputTokens: usage.output_tokens ?? 0,
110
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
111
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
112
+ };
113
+ const delta = {
114
+ model,
115
+ turns: 1,
116
+ ...slice,
117
+ thinkingTokens: details?.thinking_tokens ?? 0,
118
+ costUsd: computeActualCostUsd(model, slice),
119
+ };
120
+ record.usage = mergeActualUsage(record.usage ?? emptyActualUsage(model), delta);
121
+ ctx.appendLog(run, 'stdout', `[native] turn ${record.usage.turns}: in ${delta.inputTokens} · out ${delta.outputTokens}` +
122
+ ` · cache read ${delta.cacheReadTokens} / write ${delta.cacheCreationTokens} · $${delta.costUsd.toFixed(4)}\n`);
123
+ ctx.onTurnUsage(record, delta, model);
124
+ }
125
+ async function executeNativeTool(ctx, run, block) {
126
+ const input = (block.input && typeof block.input === 'object' ? block.input : {});
127
+ try {
128
+ let content;
129
+ if (block.name === 'bash') {
130
+ content = await runBashTool(ctx, run, input);
131
+ }
132
+ else if (block.name === 'str_replace_based_edit_tool') {
133
+ ctx.appendLog(run, 'stdout', `[native] edit ${String(input.command)} ${String(input.path)}\n`);
134
+ content = runEditorCommand(run.record.worktreeDir, ctx.rootDir, input);
135
+ }
136
+ else {
137
+ throw new ToolInputError(`Unknown tool '${block.name}'.`);
138
+ }
139
+ return { type: 'tool_result', tool_use_id: block.id, content };
140
+ }
141
+ catch (err) {
142
+ const message = err instanceof Error ? err.message : String(err);
143
+ ctx.appendLog(run, 'stderr', `[native] ${block.name} error: ${message.split('\n')[0]}\n`);
144
+ return { type: 'tool_result', tool_use_id: block.id, content: message, is_error: true };
145
+ }
146
+ }
147
+ async function runBashTool(ctx, run, input) {
148
+ if (input.restart === true)
149
+ return 'Shell restarted. Every command already starts in a fresh shell at the workspace root.';
150
+ const command = requireString(input, 'command').trim();
151
+ if (!command)
152
+ throw new ToolInputError('"command" must be a non-empty string.');
153
+ const rejection = checkNativeBashCommand(command, ctx.nativeAllowedCommands);
154
+ if (rejection)
155
+ throw new ToolInputError(`Command rejected: ${rejection}`);
156
+ ctx.appendLog(run, 'stdout', `[native] $ ${command}\n`);
157
+ const child = spawn(command, {
158
+ cwd: run.record.worktreeDir,
159
+ shell: ctx.toolShell,
160
+ detached: process.platform !== 'win32',
161
+ windowsHide: true,
162
+ stdio: ['ignore', 'pipe', 'pipe'],
163
+ env: {
164
+ ...buildToolEnv(process.env, run.native?.env),
165
+ NATIV_RUN_ID: run.record.runId,
166
+ NATIV_TASK_ID: run.record.taskId,
167
+ NATIV_PROJECT_ROOT: ctx.rootDir,
168
+ },
169
+ });
170
+ run.toolPid = child.pid ?? null;
171
+ let output = '';
172
+ let omitted = 0;
173
+ const capture = (chunk) => {
174
+ output += chunk.toString();
175
+ if (output.length > NATIVE_TOOL_CAPTURE_LIMIT) {
176
+ const head = Math.floor(NATIVE_TOOL_CAPTURE_LIMIT * 0.2);
177
+ const excess = output.length - NATIVE_TOOL_CAPTURE_LIMIT;
178
+ output = output.slice(0, head) + output.slice(head + excess);
179
+ omitted += excess;
180
+ }
181
+ };
182
+ child.stdout?.on('data', capture);
183
+ child.stderr?.on('data', capture);
184
+ let timedOut = false;
185
+ const timer = setTimeout(() => {
186
+ timedOut = true;
187
+ if (child.pid)
188
+ killProcessTree(child.pid, 'SIGKILL');
189
+ }, ctx.nativeToolTimeoutMs);
190
+ timer.unref?.();
191
+ const exitCode = await new Promise((resolve) => {
192
+ child.on('error', (err) => {
193
+ capture(Buffer.from(`\n${err.message}\n`));
194
+ resolve(null);
195
+ });
196
+ child.on('close', (code) => resolve(code));
197
+ });
198
+ clearTimeout(timer);
199
+ run.toolPid = null;
200
+ const clipped = clipToolOutput(output, omitted);
201
+ if (clipped)
202
+ ctx.appendLog(run, 'stdout', clipped.endsWith('\n') ? clipped : `${clipped}\n`);
203
+ if (timedOut) {
204
+ throw new ToolInputError(`Command exceeded the ${Math.round(ctx.nativeToolTimeoutMs / 1000)}s tool budget and was terminated.\n${clipped}`);
205
+ }
206
+ const body = clipped || '(no output)';
207
+ return exitCode === 0 ? body : `${body}\n[exit code ${exitCode ?? 'unknown'}]`;
208
+ }
@@ -0,0 +1,27 @@
1
+ import type { MasterPlanTask } from '../scanner/types.js';
2
+ import { type RunnerEngine, type RunnerThinking, type ThinkingEffort } from './types.js';
3
+ /** Models that predate adaptive thinking and still take `thinking.budget_tokens`. */
4
+ export declare function usesLegacyThinkingBudget(model: string): boolean;
5
+ /**
6
+ * Buckets a token budget into an effort level. Current models reject `budget_tokens`, so the
7
+ * contract's numeric `thinkingBudget` maps onto effort. The buckets follow the Studio's budget
8
+ * chips: 0 ("None, fast") is the least thinking a model allows, 2,048 ("Standard") is the
9
+ * standard effort, 4,096 ("Deep") is high, and larger budgets reach xhigh and max.
10
+ */
11
+ export declare function thinkingBudgetToEffort(budget: number): ThinkingEffort;
12
+ export declare function defaultNativeEffort(model: string): ThinkingEffort;
13
+ export declare function resolveNativeThinking(model: string, budget: number | null): RunnerThinking;
14
+ /** Models that opt into server-side refusal fallbacks by default. */
15
+ export declare function supportsServerFallback(model: string): boolean;
16
+ export declare const NATIVE_SYSTEM_PROMPT = "You are an autonomous engineering agent: a worker in the execution tier of a three-tier development pipeline, dispatched by nativ (the project-manager tier). You complete one task from .ai/master_plan.json on your own, with no human watching each step.\n\nYour environment:\n- The working directory is the task's workspace root. Each bash command runs in a fresh POSIX shell started there, so `cd` and exported variables do not persist between commands; chain with && when you need them.\n- Use str_replace_based_edit_tool with paths relative to the workspace root. Files under .ai/, node_modules/ and .git/ are read-only.\n- Commands are checked against an allowlist of executables. Command substitution, background jobs, absolute paths, parent-directory paths, secret files and publishing (git push, npm publish) are rejected with a reason; adjust and continue.\n\nHow to work:\n1. Run `nativ task start <taskId>` before changing code.\n2. Change only the files in the task's targetFiles. Follow the contracts in .ai/ exactly: table and column names, routes and schemas, and design tokens. Match the style of the surrounding code.\n3. Run the task's verificationCommand. If it fails, fix the cause and run it again; you have at most three fix attempts. If it still fails, run `git checkout -- <targetFiles>`, then `nativ task block <taskId> --reason \"Verification failed after 3 attempts: <short error>\"`, and stop.\n4. If a contract in .ai/ lacks something the task needs, do not edit it. Run `nativ task escalate <taskId> --type schema_flaw --details \"<the gap>\"` and stop.\n5. When verification passes, run `nativ task complete <taskId>`.\n\nCode style (the project's own formatter or linter config wins): lines 100 characters or fewer, hard max 120; functions about 40 lines, files about 300; split a growing UI file into components, one per file; comments at most 2 lines, say why not what, plain everyday words, no restating the code, no banner comments, no commented-out code; match the surrounding code.\n\nSecrets: never open .env files (.env.example is fine), *.pem, *.key or .nativ/*.local.json, and never print environment variables. For database structure use `nativ db status|inspect|diff --json`; never read table data.\n\nWhen you finish or get blocked, end with a short plain-English summary: what changed, the verification result, and anything the operator must decide. Do not call a tool in that final message.";
17
+ export declare function buildSpecSlicesBlock(task: MasterPlanTask, rootDir: string | undefined): string;
18
+ export declare function buildPriorEscalationsBlock(task: MasterPlanTask, rootDir: string | undefined): string;
19
+ export declare function buildLearningsBlock(task: MasterPlanTask, rootDir: string | undefined): string;
20
+ /** Keeps stored text from closing the block it sits in and passing as instructions. */
21
+ export declare function untag(text: string, tag: string): string;
22
+ export declare function buildNativeTaskPrompt(task: MasterPlanTask, roleGuide: string | null, useWorktree: boolean, rootDir?: string): string;
23
+ export declare function describeNativeError(err: unknown): string;
24
+ export declare function normalizeEngine(value: unknown, fallback: RunnerEngine): RunnerEngine;
25
+ /** 0 is a valid budget: "as little thinking as the model allows". */
26
+ export declare function normalizeThinkingBudget(value: unknown): number | null;
27
+ export declare function normalizeModel(value: unknown): string | null;