@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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +334 -0
- package/bin/cli.js +31 -0
- package/dist/commands/bench.d.ts +8 -0
- package/dist/commands/bench.js +52 -0
- package/dist/commands/db.d.ts +22 -0
- package/dist/commands/db.js +415 -0
- package/dist/commands/doctor.d.ts +38 -0
- package/dist/commands/doctor.js +169 -0
- package/dist/commands/hook.d.ts +31 -0
- package/dist/commands/hook.js +105 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +144 -0
- package/dist/commands/mcp.d.ts +5 -0
- package/dist/commands/mcp.js +16 -0
- package/dist/commands/setup.d.ts +17 -0
- package/dist/commands/setup.js +51 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +75 -0
- package/dist/commands/task.d.ts +83 -0
- package/dist/commands/task.js +807 -0
- package/dist/commands/test-gen.d.ts +14 -0
- package/dist/commands/test-gen.js +52 -0
- package/dist/commands/triage.d.ts +70 -0
- package/dist/commands/triage.js +343 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +112 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +89 -0
- package/dist/commands/verify.d.ts +8 -0
- package/dist/commands/verify.js +65 -0
- package/dist/commands/worktree.d.ts +47 -0
- package/dist/commands/worktree.js +396 -0
- package/dist/core/benchmark.d.ts +57 -0
- package/dist/core/benchmark.js +772 -0
- package/dist/core/enforcement.d.ts +80 -0
- package/dist/core/enforcement.js +271 -0
- package/dist/core/legacy-templates.d.ts +24 -0
- package/dist/core/legacy-templates.js +60 -0
- package/dist/core/lock-manager.d.ts +54 -0
- package/dist/core/lock-manager.js +260 -0
- package/dist/core/root-resolver.d.ts +32 -0
- package/dist/core/root-resolver.js +149 -0
- package/dist/core/setup-assets.d.ts +123 -0
- package/dist/core/setup-assets.js +438 -0
- package/dist/core/telemetry.d.ts +156 -0
- package/dist/core/telemetry.js +416 -0
- package/dist/core/test-generator-types.d.ts +227 -0
- package/dist/core/test-generator-types.js +179 -0
- package/dist/core/test-generator.d.ts +34 -0
- package/dist/core/test-generator.js +1690 -0
- package/dist/core/tier1-liaison.d.ts +139 -0
- package/dist/core/tier1-liaison.js +320 -0
- package/dist/core/verifier.d.ts +52 -0
- package/dist/core/verifier.js +249 -0
- package/dist/core/version.d.ts +5 -0
- package/dist/core/version.js +14 -0
- package/dist/core/worktree-merge.d.ts +30 -0
- package/dist/core/worktree-merge.js +98 -0
- package/dist/db/data-engine.d.ts +46 -0
- package/dist/db/data-engine.js +802 -0
- package/dist/db/diff.d.ts +39 -0
- package/dist/db/diff.js +420 -0
- package/dist/db/env-parser.d.ts +86 -0
- package/dist/db/env-parser.js +549 -0
- package/dist/db/introspector.d.ts +21 -0
- package/dist/db/introspector.js +835 -0
- package/dist/db/types.d.ts +217 -0
- package/dist/db/types.js +9 -0
- package/dist/governor/circuit-breaker.d.ts +100 -0
- package/dist/governor/circuit-breaker.js +231 -0
- package/dist/governor/evaluator.d.ts +67 -0
- package/dist/governor/evaluator.js +851 -0
- package/dist/governor/index.d.ts +6 -0
- package/dist/governor/index.js +6 -0
- package/dist/governor/rules/api-rules.d.ts +2 -0
- package/dist/governor/rules/api-rules.js +140 -0
- package/dist/governor/rules/db-rules.d.ts +2 -0
- package/dist/governor/rules/db-rules.js +180 -0
- package/dist/governor/store.d.ts +33 -0
- package/dist/governor/store.js +84 -0
- package/dist/governor/types.d.ts +64 -0
- package/dist/governor/types.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +358 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +309 -0
- package/dist/providers/claude-api.d.ts +5 -0
- package/dist/providers/claude-api.js +75 -0
- package/dist/providers/claude-cli.d.ts +13 -0
- package/dist/providers/claude-cli.js +134 -0
- package/dist/providers/gemini.d.ts +8 -0
- package/dist/providers/gemini.js +78 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +5 -0
- package/dist/providers/registry.d.ts +38 -0
- package/dist/providers/registry.js +121 -0
- package/dist/providers/types.d.ts +74 -0
- package/dist/providers/types.js +25 -0
- package/dist/runner/agent-supervisor.d.ts +363 -0
- package/dist/runner/agent-supervisor.js +1672 -0
- package/dist/scanner/context-builder.d.ts +2 -0
- package/dist/scanner/context-builder.js +29 -0
- package/dist/scanner/detector.d.ts +2 -0
- package/dist/scanner/detector.js +760 -0
- package/dist/scanner/types.d.ts +80 -0
- package/dist/scanner/types.js +50 -0
- package/dist/server/studio-server.d.ts +39 -0
- package/dist/server/studio-server.js +1931 -0
- package/dist/server/studio-ui.d.ts +15 -0
- package/dist/server/studio-ui.js +5321 -0
- package/package.json +70 -0
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/.mcp.json +10 -0
- package/plugin/agents/architect.md +28 -0
- package/plugin/agents/verifier.md +28 -0
- package/plugin/agents/worker.md +26 -0
- package/plugin/hooks/hooks.json +28 -0
- package/plugin/skills/nativ/SKILL.md +75 -0
- package/templates/.claudeignore +45 -0
- package/templates/AGENTS.md +67 -0
- package/templates/CLAUDE.md +32 -0
- package/templates/GEMINI.md +66 -0
- package/templates/claude-agents/architect.md +28 -0
- package/templates/claude-agents/verifier.md +28 -0
- package/templates/claude-agents/worker.md +26 -0
- package/templates/dot-ai/api_contracts.json +66 -0
- package/templates/dot-ai/context.md +38 -0
- package/templates/dot-ai/db_schema.json +39 -0
- package/templates/dot-ai/master_plan.json +28 -0
- package/templates/dot-ai/subagents/backend.md +11 -0
- package/templates/dot-ai/subagents/database.md +10 -0
- package/templates/dot-ai/subagents/db-migration.md +11 -0
- package/templates/dot-ai/subagents/devops-agent.md +11 -0
- package/templates/dot-ai/subagents/flutter-developer.md +11 -0
- package/templates/dot-ai/subagents/frontend.md +126 -0
- package/templates/dot-ai/subagents/qa-tester.md +13 -0
- package/templates/dot-ai/subagents/security-auditor.md +11 -0
- package/templates/dot-ai/ui_specs.md +78 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type CompleteRequest, type Provider, type ProviderDeps, type ProviderId, type ProviderRole } from './types.js';
|
|
2
|
+
/** Subscription login first (no key needed), then API keys. Claude is the default for every role. */
|
|
3
|
+
export declare const DEFAULT_CHAIN: readonly ProviderId[];
|
|
4
|
+
export interface ProviderConfig {
|
|
5
|
+
/** Per-role provider id, or an ordered list. Unknown ids are ignored. */
|
|
6
|
+
providers?: Partial<Record<ProviderRole, ProviderId | ProviderId[]>>;
|
|
7
|
+
/** Per-provider model override, e.g. { "claude-cli": "sonnet" }. */
|
|
8
|
+
models?: Partial<Record<ProviderId, string>>;
|
|
9
|
+
}
|
|
10
|
+
export interface RegistryOptions {
|
|
11
|
+
/** Explicit provider id(s); wins over config and the default chain. */
|
|
12
|
+
provider?: ProviderId | ProviderId[];
|
|
13
|
+
deps?: ProviderDeps;
|
|
14
|
+
/** Inline config, used instead of reading .nativ/config.json (tests, programmatic callers). */
|
|
15
|
+
config?: ProviderConfig;
|
|
16
|
+
}
|
|
17
|
+
export interface ChainResult {
|
|
18
|
+
provider: ProviderId;
|
|
19
|
+
model: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function createProviders(deps?: ProviderDeps): Record<ProviderId, Provider>;
|
|
23
|
+
export declare function loadProviderConfig(root: string): ProviderConfig;
|
|
24
|
+
export declare function setProviderCooldown(root: string, id: ProviderId, waitMs: number, now?: number): void;
|
|
25
|
+
/** Milliseconds left on a provider's cooldown, or 0. Expired entries are pruned. */
|
|
26
|
+
export declare function providerCooldownRemaining(root: string, id: ProviderId, now?: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Providers to try for a role, in order: those with credentials that are not cooling down.
|
|
29
|
+
* An explicit or configured provider that is unavailable is skipped, not an error.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveProviderChain(role: ProviderRole, root: string, options?: RegistryOptions): Provider[];
|
|
32
|
+
export declare function resolveProvider(role: ProviderRole, root: string, options?: RegistryOptions): Provider | null;
|
|
33
|
+
/**
|
|
34
|
+
* Runs the request against each provider in turn. A rate limit puts that provider on cooldown, and any
|
|
35
|
+
* ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
|
|
36
|
+
* last error when every provider failed, so the caller can drop to its deterministic path.
|
|
37
|
+
*/
|
|
38
|
+
export declare function completeWithChain(role: ProviderRole, root: string, request: CompleteRequest, options?: RegistryOptions): Promise<ChainResult>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createClaudeApiProvider } from './claude-api.js';
|
|
4
|
+
import { createClaudeCliProvider, DEFAULT_LIMIT_COOLDOWN_MS } from './claude-cli.js';
|
|
5
|
+
import { createGeminiProvider } from './gemini.js';
|
|
6
|
+
import { ProviderError, } from './types.js';
|
|
7
|
+
/** Subscription login first (no key needed), then API keys. Claude is the default for every role. */
|
|
8
|
+
export const DEFAULT_CHAIN = ['claude-cli', 'claude-api', 'gemini'];
|
|
9
|
+
const CONFIG_FILE = path.join('.nativ', 'config.json');
|
|
10
|
+
const STATE_FILE = path.join('.nativ', 'provider-state.json');
|
|
11
|
+
export function createProviders(deps = {}) {
|
|
12
|
+
return {
|
|
13
|
+
'claude-cli': createClaudeCliProvider(deps),
|
|
14
|
+
'claude-api': createClaudeApiProvider(deps),
|
|
15
|
+
gemini: createGeminiProvider(deps),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function isProviderId(value) {
|
|
19
|
+
return typeof value === 'string' && DEFAULT_CHAIN.includes(value);
|
|
20
|
+
}
|
|
21
|
+
function toIds(value) {
|
|
22
|
+
const list = Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
23
|
+
return list.filter(isProviderId);
|
|
24
|
+
}
|
|
25
|
+
export function loadProviderConfig(root) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(fs.readFileSync(path.join(root, CONFIG_FILE), 'utf8'));
|
|
28
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function readCooldowns(root) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(fs.readFileSync(path.join(root, STATE_FILE), 'utf8'));
|
|
37
|
+
return raw.cooldownUntil && typeof raw.cooldownUntil === 'object' ? raw.cooldownUntil : {};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeCooldowns(root, cooldownUntil) {
|
|
44
|
+
try {
|
|
45
|
+
const file = path.join(root, STATE_FILE);
|
|
46
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
48
|
+
fs.writeFileSync(tmp, JSON.stringify({ cooldownUntil }, null, 2), 'utf8');
|
|
49
|
+
fs.renameSync(tmp, file);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Cooldown is an optimisation; failing to persist it must never fail the call.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function setProviderCooldown(root, id, waitMs, now = Date.now()) {
|
|
56
|
+
const state = readCooldowns(root);
|
|
57
|
+
state[id] = now + waitMs;
|
|
58
|
+
writeCooldowns(root, state);
|
|
59
|
+
}
|
|
60
|
+
/** Milliseconds left on a provider's cooldown, or 0. Expired entries are pruned. */
|
|
61
|
+
export function providerCooldownRemaining(root, id, now = Date.now()) {
|
|
62
|
+
const state = readCooldowns(root);
|
|
63
|
+
const until = state[id];
|
|
64
|
+
if (typeof until !== 'number')
|
|
65
|
+
return 0;
|
|
66
|
+
if (until <= now) {
|
|
67
|
+
delete state[id];
|
|
68
|
+
writeCooldowns(root, state);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
return until - now;
|
|
72
|
+
}
|
|
73
|
+
// ─── Resolution ────────────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
function orderedIds(role, root, options) {
|
|
75
|
+
const explicit = toIds(options.provider);
|
|
76
|
+
if (explicit.length)
|
|
77
|
+
return explicit;
|
|
78
|
+
const configured = toIds((options.config ?? loadProviderConfig(root)).providers?.[role]);
|
|
79
|
+
return configured.length ? configured : [...DEFAULT_CHAIN];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Providers to try for a role, in order: those with credentials that are not cooling down.
|
|
83
|
+
* An explicit or configured provider that is unavailable is skipped, not an error.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveProviderChain(role, root, options = {}) {
|
|
86
|
+
const providers = createProviders(options.deps);
|
|
87
|
+
const now = options.deps?.now?.() ?? Date.now();
|
|
88
|
+
return orderedIds(role, root, options)
|
|
89
|
+
.map((id) => providers[id])
|
|
90
|
+
.filter((provider) => provider.hasCredentials(root) && providerCooldownRemaining(root, provider.id, now) === 0);
|
|
91
|
+
}
|
|
92
|
+
export function resolveProvider(role, root, options = {}) {
|
|
93
|
+
return resolveProviderChain(role, root, options)[0] ?? null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Runs the request against each provider in turn. A rate limit puts that provider on cooldown, and any
|
|
97
|
+
* ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
|
|
98
|
+
* last error when every provider failed, so the caller can drop to its deterministic path.
|
|
99
|
+
*/
|
|
100
|
+
export async function completeWithChain(role, root, request, options = {}) {
|
|
101
|
+
const chain = resolveProviderChain(role, root, options);
|
|
102
|
+
if (!chain.length)
|
|
103
|
+
throw new ProviderError('NO_CREDENTIALS', 'No provider is available (no login, key or all cooling down).');
|
|
104
|
+
const models = (options.config ?? loadProviderConfig(root)).models ?? {};
|
|
105
|
+
const now = options.deps?.now ?? Date.now;
|
|
106
|
+
let lastError = null;
|
|
107
|
+
for (const provider of chain) {
|
|
108
|
+
const model = request.model ?? models[provider.id] ?? provider.defaultModel;
|
|
109
|
+
try {
|
|
110
|
+
const text = await provider.complete({ ...request, model }, root);
|
|
111
|
+
return { provider: provider.id, model, text };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const failure = err instanceof ProviderError ? err : new ProviderError('BAD_RESPONSE', err instanceof Error ? err.message : String(err));
|
|
115
|
+
if (failure.code === 'RATE_LIMITED')
|
|
116
|
+
setProviderCooldown(root, provider.id, failure.retryAfterMs ?? DEFAULT_LIMIT_COOLDOWN_MS, now());
|
|
117
|
+
lastError = failure;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw lastError ?? new ProviderError('BAD_RESPONSE', 'Every provider failed.');
|
|
121
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nativ · Provider adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* Every LLM call made by nativ itself goes through a `Provider`. Adding a vendor means adding one
|
|
5
|
+
* file in this directory and one entry in registry.ts; nothing outside src/providers/ changes.
|
|
6
|
+
*/
|
|
7
|
+
export type ProviderId = 'claude-cli' | 'claude-api' | 'gemini';
|
|
8
|
+
/** Roles a provider can be assigned to in `.nativ/config.json`. */
|
|
9
|
+
export type ProviderRole = 'architect' | 'pm' | 'worker' | 'verifier' | 'triage';
|
|
10
|
+
export type ProviderErrorCode = 'NO_CREDENTIALS' | 'TIMEOUT' | 'RATE_LIMITED' | 'BAD_RESPONSE';
|
|
11
|
+
/** Typed failure so callers can fall through the provider chain without string matching. */
|
|
12
|
+
export declare class ProviderError extends Error {
|
|
13
|
+
readonly code: ProviderErrorCode;
|
|
14
|
+
/** For RATE_LIMITED: how long the provider asked us to wait, when it said. */
|
|
15
|
+
readonly retryAfterMs?: number | undefined;
|
|
16
|
+
constructor(code: ProviderErrorCode, message: string,
|
|
17
|
+
/** For RATE_LIMITED: how long the provider asked us to wait, when it said. */
|
|
18
|
+
retryAfterMs?: number | undefined);
|
|
19
|
+
}
|
|
20
|
+
export interface CompleteRequest {
|
|
21
|
+
system?: string;
|
|
22
|
+
prompt: string;
|
|
23
|
+
/** Ask for a single JSON document; providers strip code fences from the reply. */
|
|
24
|
+
json?: boolean;
|
|
25
|
+
/** Overrides the provider's default model. */
|
|
26
|
+
model?: string;
|
|
27
|
+
/** Hard ceiling for the call. Defaults are provider-specific. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface Provider {
|
|
31
|
+
readonly id: ProviderId;
|
|
32
|
+
readonly defaultModel: string;
|
|
33
|
+
/** True when a call could plausibly succeed: a login, a binary on PATH, or an API key. Never reads the secret out. */
|
|
34
|
+
hasCredentials(root: string): boolean;
|
|
35
|
+
complete(request: CompleteRequest, root: string): Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
export interface SpawnRequest {
|
|
38
|
+
cwd: string;
|
|
39
|
+
env: NodeJS.ProcessEnv;
|
|
40
|
+
input: string;
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
}
|
|
43
|
+
export interface SpawnResult {
|
|
44
|
+
code: number | null;
|
|
45
|
+
stdout: string;
|
|
46
|
+
stderr: string;
|
|
47
|
+
timedOut: boolean;
|
|
48
|
+
/** True when the executable could not be started at all (ENOENT). */
|
|
49
|
+
notFound?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Everything a provider touches outside the process, injectable so tests never hit a network or a real binary. */
|
|
52
|
+
export interface ProviderDeps {
|
|
53
|
+
spawn?: (command: string, args: string[], request: SpawnRequest) => Promise<SpawnResult>;
|
|
54
|
+
hasBinary?: (name: string) => boolean;
|
|
55
|
+
fetch?: typeof fetch;
|
|
56
|
+
/** Minimal shape of the Anthropic SDK client used by claude-api. */
|
|
57
|
+
anthropicClient?: () => AnthropicLike;
|
|
58
|
+
env?: NodeJS.ProcessEnv;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
export interface AnthropicLike {
|
|
62
|
+
messages: {
|
|
63
|
+
create(body: Record<string, unknown>, options?: {
|
|
64
|
+
timeout?: number;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
content: Array<{
|
|
67
|
+
type: string;
|
|
68
|
+
text?: string;
|
|
69
|
+
}>;
|
|
70
|
+
}>;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Strips a surrounding ```json fence, which models add even when told not to. */
|
|
74
|
+
export declare function stripJsonFence(text: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nativ · Provider adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* Every LLM call made by nativ itself goes through a `Provider`. Adding a vendor means adding one
|
|
5
|
+
* file in this directory and one entry in registry.ts; nothing outside src/providers/ changes.
|
|
6
|
+
*/
|
|
7
|
+
/** Typed failure so callers can fall through the provider chain without string matching. */
|
|
8
|
+
export class ProviderError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
retryAfterMs;
|
|
11
|
+
constructor(code, message,
|
|
12
|
+
/** For RATE_LIMITED: how long the provider asked us to wait, when it said. */
|
|
13
|
+
retryAfterMs) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.retryAfterMs = retryAfterMs;
|
|
17
|
+
this.name = 'ProviderError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Strips a surrounding ```json fence, which models add even when told not to. */
|
|
21
|
+
export function stripJsonFence(text) {
|
|
22
|
+
const trimmed = text.trim();
|
|
23
|
+
const fenced = /^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i.exec(trimmed);
|
|
24
|
+
return (fenced ? fenced[1] : trimmed).trim();
|
|
25
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
+
import { type ActualTokenUsage } from '../core/telemetry.js';
|
|
4
|
+
import type { MasterPlanTask } from '../scanner/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Lifecycle phases broadcast on the `runner_status` channel. These mirror the
|
|
7
|
+
* phases declared in .ai/api_contracts.json (sseChannels → runner_status).
|
|
8
|
+
*/
|
|
9
|
+
export type RunnerStatus = 'spawning_worktree' | 'running' | 'verifying' | 'merging' | 'completed' | 'failed' | 'aborted';
|
|
10
|
+
export declare const ACTIVE_RUNNER_STATUSES: readonly RunnerStatus[];
|
|
11
|
+
export declare function isActiveStatus(status: RunnerStatus): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* `native` drives the Messages API in-process with local bash/editor tools;
|
|
14
|
+
* `cli` spawns a runner command (Claude Code by default).
|
|
15
|
+
*/
|
|
16
|
+
export type RunnerEngine = 'native' | 'cli';
|
|
17
|
+
export type ThinkingEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
18
|
+
export interface RunnerThinking {
|
|
19
|
+
/** The dispatch's `thinkingBudget`, echoed back; null when unset. */
|
|
20
|
+
budget: number | null;
|
|
21
|
+
/** `output_config.effort` sent to adaptive-thinking models; null otherwise. */
|
|
22
|
+
effort: ThinkingEffort | null;
|
|
23
|
+
/** Literal `budget_tokens` (legacy models) or `MAX_THINKING_TOKENS` (cli); null otherwise. */
|
|
24
|
+
budgetTokens: number | null;
|
|
25
|
+
}
|
|
26
|
+
/** Payload of the `runner_token_usage` channel, emitted after every native turn. */
|
|
27
|
+
export interface RunnerTokenUsageEvent {
|
|
28
|
+
runId: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
/** Model that served this turn (differs from the requested one after a refusal fallback). */
|
|
31
|
+
model: string;
|
|
32
|
+
turn: number;
|
|
33
|
+
delta: ActualTokenUsage;
|
|
34
|
+
total: ActualTokenUsage & {
|
|
35
|
+
cacheHitRate: number;
|
|
36
|
+
};
|
|
37
|
+
at: string;
|
|
38
|
+
}
|
|
39
|
+
/** Persisted, serializable state of a single agent execution. */
|
|
40
|
+
export interface RunnerRecord {
|
|
41
|
+
runId: string;
|
|
42
|
+
taskId: string;
|
|
43
|
+
status: RunnerStatus;
|
|
44
|
+
engine: RunnerEngine;
|
|
45
|
+
/** Requested model; null for cli runs that keep the runner's own default. */
|
|
46
|
+
model: string | null;
|
|
47
|
+
thinking: RunnerThinking | null;
|
|
48
|
+
/** API-reported usage accumulated across turns; null for cli runs. */
|
|
49
|
+
usage: ActualTokenUsage | null;
|
|
50
|
+
/** PID of the spawned process-group leader; null before spawn and after exit. */
|
|
51
|
+
pid: number | null;
|
|
52
|
+
/** Branch backing the isolated worktree, or null when running in place. */
|
|
53
|
+
branch: string | null;
|
|
54
|
+
/** Absolute worktree path, or the project root when `useWorktree` is false. */
|
|
55
|
+
worktreeDir: string;
|
|
56
|
+
command: string;
|
|
57
|
+
startedAt: string;
|
|
58
|
+
endedAt: string | null;
|
|
59
|
+
durationMs: number | null;
|
|
60
|
+
exitCode: number | null;
|
|
61
|
+
signal: string | null;
|
|
62
|
+
timeoutSeconds: number;
|
|
63
|
+
timedOut: boolean;
|
|
64
|
+
logBytes: number;
|
|
65
|
+
logFile: string;
|
|
66
|
+
error: string | null;
|
|
67
|
+
abortReason: string | null;
|
|
68
|
+
verification: {
|
|
69
|
+
command: string;
|
|
70
|
+
success: boolean;
|
|
71
|
+
exitCode: number;
|
|
72
|
+
durationMs: number;
|
|
73
|
+
skipped: boolean;
|
|
74
|
+
} | null;
|
|
75
|
+
}
|
|
76
|
+
export interface DispatchOptions {
|
|
77
|
+
taskId: string;
|
|
78
|
+
/** Defaults to the supervisor's `defaultEngine` (`cli` unless configured). */
|
|
79
|
+
runnerEngine?: RunnerEngine;
|
|
80
|
+
/** Shell command that launches the agent (cli engine only). Defaults to `defaultRunnerCommand`. */
|
|
81
|
+
runnerCommand?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Reasoning budget in tokens. Adaptive-thinking models receive it as an
|
|
84
|
+
* `effort` level (they reject `budget_tokens`); legacy models receive it
|
|
85
|
+
* literally; the Claude Code CLI receives it as `MAX_THINKING_TOKENS`.
|
|
86
|
+
*/
|
|
87
|
+
thinkingBudget?: number;
|
|
88
|
+
/** Model ID. Native runs default to `defaultNativeModel`; cli runs pass it as `--model`. */
|
|
89
|
+
model?: string;
|
|
90
|
+
/** Killswitch budget in seconds. Defaults to the supervisor's `defaultTimeoutSeconds`. */
|
|
91
|
+
timeoutSeconds?: number;
|
|
92
|
+
/** Run inside `.worktrees/task-<id>` on branch `agent/task-<id>`. Default true. */
|
|
93
|
+
useWorktree?: boolean;
|
|
94
|
+
/** Run the task's `verificationCommand` after a clean exit. Default false. */
|
|
95
|
+
verify?: boolean;
|
|
96
|
+
/** Merge the agent branch back after a passing verification. Default false. */
|
|
97
|
+
autoMerge?: boolean;
|
|
98
|
+
/** Extra environment variables handed to the child (never logged or persisted). */
|
|
99
|
+
env?: Record<string, string>;
|
|
100
|
+
/** Extra permission rules for the default cli runner, e.g. `Bash(pnpm test *)`. Added to the per-task allowlist. */
|
|
101
|
+
allowedTools?: string[];
|
|
102
|
+
}
|
|
103
|
+
export interface AbortResult {
|
|
104
|
+
taskId: string;
|
|
105
|
+
runId: string;
|
|
106
|
+
status: 'aborted';
|
|
107
|
+
message: string;
|
|
108
|
+
}
|
|
109
|
+
export interface LogSlice {
|
|
110
|
+
taskId: string;
|
|
111
|
+
runId: string | null;
|
|
112
|
+
status: RunnerStatus | null;
|
|
113
|
+
totalBytes: number;
|
|
114
|
+
log: string;
|
|
115
|
+
truncated: boolean;
|
|
116
|
+
}
|
|
117
|
+
export interface ListRunsQuery {
|
|
118
|
+
taskId?: string;
|
|
119
|
+
activeOnly?: boolean;
|
|
120
|
+
}
|
|
121
|
+
export interface AgentSupervisorOptions {
|
|
122
|
+
/** Directory used to resolve the project root holding `.ai/`. Defaults to `process.cwd()`. */
|
|
123
|
+
cwd?: string;
|
|
124
|
+
defaultRunnerCommand?: string;
|
|
125
|
+
defaultTimeoutSeconds?: number;
|
|
126
|
+
/** Hard cap on the in-memory log ring buffer per run. */
|
|
127
|
+
maxLogBufferBytes?: number;
|
|
128
|
+
/** Grace period between SIGTERM and the forced kill during abort/timeout. */
|
|
129
|
+
killGraceMs?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Inactivity watchdog: terminate a run that emits no output for this many seconds. Defaults to
|
|
132
|
+
* `NATIV_RUNNER_IDLE_TIMEOUT`, then `DEFAULT_IDLE_TIMEOUT_SECONDS`; 0 disables it.
|
|
133
|
+
*/
|
|
134
|
+
idleTimeoutSeconds?: number;
|
|
135
|
+
/** How many finished runs to keep in memory. */
|
|
136
|
+
maxHistory?: number;
|
|
137
|
+
/** Engine used when a dispatch omits `runnerEngine`. Defaults to `NATIV_RUNNER_ENGINE`, then `cli`. */
|
|
138
|
+
defaultEngine?: RunnerEngine;
|
|
139
|
+
/** Model used by native runs that omit `model`. Defaults to `NATIV_NATIVE_MODEL`, then `DEFAULT_NATIVE_MODEL`. */
|
|
140
|
+
defaultNativeModel?: string;
|
|
141
|
+
/** Hard cap on API round-trips per native run. */
|
|
142
|
+
nativeMaxTurns?: number;
|
|
143
|
+
/** Per-command budget for the native bash tool. */
|
|
144
|
+
nativeToolTimeoutMs?: number;
|
|
145
|
+
/** Executables the native bash tool may invoke. */
|
|
146
|
+
nativeAllowedCommands?: readonly string[];
|
|
147
|
+
/** Builds the Claude API client for native runs; injectable for tests. */
|
|
148
|
+
anthropicClientFactory?: () => Anthropic;
|
|
149
|
+
/**
|
|
150
|
+
* Shell for the native bash tool: a path to bash, or `true` for the platform default shell.
|
|
151
|
+
* Defaults to NATIV_BASH_PATH, then Git Bash on Windows, then /bin/bash.
|
|
152
|
+
*/
|
|
153
|
+
nativeShell?: string | boolean;
|
|
154
|
+
}
|
|
155
|
+
/** Structured failure carrying the `{ code, message }` shape used by the pipeline API. */
|
|
156
|
+
export declare class SupervisorError extends Error {
|
|
157
|
+
readonly code: string;
|
|
158
|
+
readonly status: number;
|
|
159
|
+
constructor(code: string, message: string, status?: number);
|
|
160
|
+
}
|
|
161
|
+
export declare const DEFAULT_RUNNER_COMMAND = "claude";
|
|
162
|
+
/** Hard cap per run. Real agent tasks routinely take tens of minutes; hung runs are caught by the idle watchdog. */
|
|
163
|
+
export declare const DEFAULT_TIMEOUT_SECONDS = 3600;
|
|
164
|
+
/** A run that produces no output (log lines, tool activity, API turns) for this long is treated as hung. */
|
|
165
|
+
export declare const DEFAULT_IDLE_TIMEOUT_SECONDS = 900;
|
|
166
|
+
export declare const DEFAULT_MAX_LOG_BUFFER_BYTES: number;
|
|
167
|
+
export declare const DEFAULT_KILL_GRACE_MS = 5000;
|
|
168
|
+
export declare const DEFAULT_MAX_HISTORY = 50;
|
|
169
|
+
export declare const DEFAULT_RUNNER_ENGINE: RunnerEngine;
|
|
170
|
+
export declare const DEFAULT_NATIVE_MODEL = "claude-opus-5-5";
|
|
171
|
+
export declare const DEFAULT_NATIVE_EFFORT: ThinkingEffort;
|
|
172
|
+
export declare const DEFAULT_NATIVE_MAX_TURNS = 150;
|
|
173
|
+
export declare const DEFAULT_NATIVE_TOOL_TIMEOUT_MS: number;
|
|
174
|
+
export declare const NATIVE_MAX_OUTPUT_TOKENS = 64000;
|
|
175
|
+
/** Characters of tool output handed back to the model per call. */
|
|
176
|
+
export declare const NATIVE_TOOL_OUTPUT_LIMIT = 30000;
|
|
177
|
+
export declare const DEFAULT_NATIVE_ALLOWED_COMMANDS: readonly string[];
|
|
178
|
+
/** Extracts the executable binary or command name, handling quoted Windows paths. */
|
|
179
|
+
export declare function extractBinary(cmd: string): string;
|
|
180
|
+
/** Pre-flight check verifying whether a command binary exists in PATH or on the filesystem. */
|
|
181
|
+
export declare function isExecutableInPath(cmd: string): boolean;
|
|
182
|
+
/** The pieces of a compound command as Claude Code matches them: each must satisfy an allow rule on its own. */
|
|
183
|
+
export declare function splitCompoundCommand(command: string): string[];
|
|
184
|
+
/** Operator-supplied extra rules from NATIV_RUNNER_ALLOW, comma separated, e.g. `Bash(pnpm test *),Bash(cargo test *)`. */
|
|
185
|
+
export declare function parseRunnerAllowList(raw: string | undefined): string[];
|
|
186
|
+
export interface RunnerPermissions {
|
|
187
|
+
permissions: {
|
|
188
|
+
allow: string[];
|
|
189
|
+
deny: string[];
|
|
190
|
+
ask: string[];
|
|
191
|
+
};
|
|
192
|
+
/** Omitted when the project's own settings already run the enforcement hook, so it never runs (and logs) twice. */
|
|
193
|
+
hooks?: {
|
|
194
|
+
PreToolUse: Array<{
|
|
195
|
+
matcher: string;
|
|
196
|
+
hooks: Array<{
|
|
197
|
+
type: 'command';
|
|
198
|
+
command: string;
|
|
199
|
+
timeout: number;
|
|
200
|
+
}>;
|
|
201
|
+
}>;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Settings handed to a dispatched agent with `--settings`. The deny and ask rules and the enforcement hook come
|
|
206
|
+
* from the same source as `nativ setup`, so a dispatched agent is guarded even in a project where setup never ran.
|
|
207
|
+
*/
|
|
208
|
+
export declare function buildRunnerPermissions(task: Pick<MasterPlanTask, 'verificationCommand'>, rootDir: string, extraAllowed?: readonly string[]): RunnerPermissions;
|
|
209
|
+
/** Where a task's runner settings live: a subdirectory, because run history parses every top-level .json in runs/. */
|
|
210
|
+
export declare function runnerSettingsPath(runsDir: string, taskId: string): string;
|
|
211
|
+
export declare function writeRunnerSettings(runsDir: string, taskId: string, settings: RunnerPermissions): string;
|
|
212
|
+
/**
|
|
213
|
+
* Builds the autonomous prompt and arguments required by Claude Code CLI
|
|
214
|
+
* to execute non-interactively in a headless child process.
|
|
215
|
+
*
|
|
216
|
+
* It runs in `acceptEdits` mode with the rules in `settingsFile` (see buildRunnerPermissions); it does not bypass
|
|
217
|
+
* permissions. Without a settings file only the file-edit tools and read-only commands are pre-approved.
|
|
218
|
+
*/
|
|
219
|
+
export declare function buildDefaultClaudeCommand(task: MasterPlanTask, model?: string | null, settingsFile?: string): string;
|
|
220
|
+
/** True when a runner command asks Claude Code for its line-delimited JSON event stream. */
|
|
221
|
+
export declare function usesClaudeStreamJson(command: string): boolean;
|
|
222
|
+
/** Final `result` event of a Claude Code stream: grounded usage for cli runs. */
|
|
223
|
+
export interface ClaudeStreamResult {
|
|
224
|
+
isError: boolean;
|
|
225
|
+
turns: number;
|
|
226
|
+
costUsd: number | null;
|
|
227
|
+
inputTokens: number;
|
|
228
|
+
outputTokens: number;
|
|
229
|
+
cacheCreationTokens: number;
|
|
230
|
+
cacheReadTokens: number;
|
|
231
|
+
}
|
|
232
|
+
export interface ClaudeStreamLine {
|
|
233
|
+
/** Human-readable log text (newline-terminated), or null when the event is not worth showing. */
|
|
234
|
+
text: string | null;
|
|
235
|
+
model?: string;
|
|
236
|
+
result?: ClaudeStreamResult;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Turns one line of `claude -p --output-format stream-json` into a log line. Anything that is not
|
|
240
|
+
* a recognised event (warnings, custom runners, future event types) passes through or is dropped,
|
|
241
|
+
* never thrown on.
|
|
242
|
+
*/
|
|
243
|
+
export declare function formatClaudeStreamLine(line: string): ClaudeStreamLine;
|
|
244
|
+
/** Models that predate adaptive thinking and still take `thinking.budget_tokens`. */
|
|
245
|
+
export declare function usesLegacyThinkingBudget(model: string): boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Buckets a token budget into an effort level. Current models reject `budget_tokens`, so the
|
|
248
|
+
* contract's numeric `thinkingBudget` maps onto effort. The buckets follow the Studio's budget
|
|
249
|
+
* chips: 0 ("None, fast") is the least thinking a model allows, 2,048 ("Standard") is the
|
|
250
|
+
* standard effort, 4,096 ("Deep") is high, and larger budgets reach xhigh and max.
|
|
251
|
+
*/
|
|
252
|
+
export declare function thinkingBudgetToEffort(budget: number): ThinkingEffort;
|
|
253
|
+
export declare function resolveNativeThinking(model: string, budget: number | null): RunnerThinking;
|
|
254
|
+
/**
|
|
255
|
+
* Effort used when a dispatch sets no `thinkingBudget`. Claude Opus 5.5 at
|
|
256
|
+
* `medium` outperforms Claude Opus 5 at `high` on coding work, so it starts lower.
|
|
257
|
+
*/
|
|
258
|
+
export declare function defaultNativeEffort(model: string): ThinkingEffort;
|
|
259
|
+
/** Models that opt into server-side refusal fallbacks by default. */
|
|
260
|
+
export declare function supportsServerFallback(model: string): boolean;
|
|
261
|
+
/**
|
|
262
|
+
* Environment for cli runners. The agent inside can run the commands its allowlist permits (and any custom
|
|
263
|
+
* runner command runs unrestricted), so it can print anything it inherits: credential-looking variables are
|
|
264
|
+
* withheld, except the ones Claude
|
|
265
|
+
* Code itself authenticates with (and AWS credentials when it is configured for Bedrock).
|
|
266
|
+
* NATIV_RUNNER_PASS_ENV="NAME1,NAME2" passes extra variables through deliberately; variables an
|
|
267
|
+
* operator hands to dispatch() explicitly are also passed as-is.
|
|
268
|
+
*/
|
|
269
|
+
export declare function buildRunnerEnv(base: NodeJS.ProcessEnv, explicit?: Record<string, string>): NodeJS.ProcessEnv;
|
|
270
|
+
/**
|
|
271
|
+
* Best-effort policy for model-authored shell commands. The worktree is the
|
|
272
|
+
* real boundary (as with the cli engine); this blocks the obvious escapes and
|
|
273
|
+
* keeps every executable on an allowlist. Returns the rejection reason, or null.
|
|
274
|
+
*/
|
|
275
|
+
export declare function checkNativeBashCommand(command: string, allowed: ReadonlySet<string>): string | null;
|
|
276
|
+
/**
|
|
277
|
+
* Supervises autonomous agent runs: detached spawn inside an isolated git
|
|
278
|
+
* worktree, a timeout killswitch, a bounded streaming log buffer, and
|
|
279
|
+
* local-first persistence under `.nativ/runs/` (structure only, never secrets).
|
|
280
|
+
*
|
|
281
|
+
* Emits `runner_status` and `runner_log` events for SSE fan-out.
|
|
282
|
+
*/
|
|
283
|
+
export declare class AgentSupervisor extends EventEmitter {
|
|
284
|
+
private readonly rootDir;
|
|
285
|
+
private readonly runsDir;
|
|
286
|
+
private readonly defaultRunnerCommand;
|
|
287
|
+
private readonly defaultTimeoutSeconds;
|
|
288
|
+
private readonly maxLogBufferBytes;
|
|
289
|
+
private readonly killGraceMs;
|
|
290
|
+
private readonly idleTimeoutMs;
|
|
291
|
+
private readonly maxHistory;
|
|
292
|
+
private readonly defaultEngine;
|
|
293
|
+
private readonly defaultNativeModel;
|
|
294
|
+
private readonly nativeMaxTurns;
|
|
295
|
+
private readonly nativeToolTimeoutMs;
|
|
296
|
+
private readonly nativeAllowedCommands;
|
|
297
|
+
private readonly anthropicClientFactory;
|
|
298
|
+
private readonly toolShell;
|
|
299
|
+
private readonly active;
|
|
300
|
+
private history;
|
|
301
|
+
private closed;
|
|
302
|
+
constructor(options?: AgentSupervisorOptions);
|
|
303
|
+
get projectRoot(): string;
|
|
304
|
+
dispatch(options: DispatchOptions): Promise<RunnerRecord>;
|
|
305
|
+
abort(taskId: string, reason?: string): Promise<AbortResult>;
|
|
306
|
+
getRun(taskId: string): RunnerRecord | null;
|
|
307
|
+
isRunning(taskId: string): boolean;
|
|
308
|
+
listRuns(query?: ListRunsQuery): RunnerRecord[];
|
|
309
|
+
/**
|
|
310
|
+
* Returns the tail of a run's log. Live runs are served from the in-memory
|
|
311
|
+
* ring buffer; finished runs fall back to `.nativ/runs/<taskId>.log`.
|
|
312
|
+
*/
|
|
313
|
+
getLogs(taskId: string, tail?: number): LogSlice;
|
|
314
|
+
/** Kills every in-flight runner and releases timers. Safe to call twice. */
|
|
315
|
+
shutdown(reason?: string): void;
|
|
316
|
+
private normalizeTimeout;
|
|
317
|
+
private requireTask;
|
|
318
|
+
private assertDependenciesMet;
|
|
319
|
+
/** Creates `.worktrees/task-<id>` on `agent/task-<id>`, reusing an existing one. */
|
|
320
|
+
private provisionWorktree;
|
|
321
|
+
private spawnRunner;
|
|
322
|
+
/** Terminates runs that go silent: a hung agent is caught long before the hard cap. */
|
|
323
|
+
private startIdleWatchdog;
|
|
324
|
+
private handleTimeout;
|
|
325
|
+
private handleExit;
|
|
326
|
+
/** Shared tail of a clean runner exit: optional verification, optional merge, then completion. */
|
|
327
|
+
private completeSuccessfulRun;
|
|
328
|
+
private consumeStreamJson;
|
|
329
|
+
private handleStreamLine;
|
|
330
|
+
/** Claude Code reports the run's total cost and tokens once, in its final result event. */
|
|
331
|
+
private recordStreamUsage;
|
|
332
|
+
private createAnthropicClient;
|
|
333
|
+
/** Cancels the in-flight API stream and reaps the running bash tool command, if any. */
|
|
334
|
+
private interruptNative;
|
|
335
|
+
private startNativeRun;
|
|
336
|
+
private driveNativeRun;
|
|
337
|
+
/**
|
|
338
|
+
* Manual agentic loop over the Messages API: stream a turn, capture usage,
|
|
339
|
+
* execute the requested local tools, repeat until the model ends its turn.
|
|
340
|
+
*/
|
|
341
|
+
private runNativeLoop;
|
|
342
|
+
private recordTurnUsage;
|
|
343
|
+
private executeNativeTool;
|
|
344
|
+
/** Runs one model-authored command in a fresh shell at the workspace root. */
|
|
345
|
+
private runBashTool;
|
|
346
|
+
private readRoleGuide;
|
|
347
|
+
private runVerification;
|
|
348
|
+
/** Quiet counterpart to `nativ worktree merge`: same safe merge, no stdout noise inside a server. */
|
|
349
|
+
private mergeWorktree;
|
|
350
|
+
private resetLogFile;
|
|
351
|
+
private appendLog;
|
|
352
|
+
private clearTimers;
|
|
353
|
+
private finalize;
|
|
354
|
+
private emitStatus;
|
|
355
|
+
private persist;
|
|
356
|
+
/**
|
|
357
|
+
* Rebuilds the recent-run list from `.nativ/runs/*.json` so a restarted
|
|
358
|
+
* supervisor can still answer `/tasks/runs` and `/tasks/logs`. Records left
|
|
359
|
+
* in an active phase by a crashed supervisor are reconciled to `failed`.
|
|
360
|
+
*/
|
|
361
|
+
private restoreHistory;
|
|
362
|
+
}
|
|
363
|
+
export default AgentSupervisor;
|