@njeybe/nativ 2.7.0 → 2.9.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 +36 -0
- package/README.md +26 -1
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.js +36 -0
- package/dist/commands/doctor.js +7 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +1 -0
- package/dist/commands/task/task-add.js +49 -6
- package/dist/commands/task/task-common.d.ts +6 -0
- package/dist/commands/task/task-lifecycle.d.ts +3 -1
- package/dist/commands/task/task-lifecycle.js +40 -2
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/verify.d.ts +7 -0
- package/dist/commands/verify.js +26 -2
- package/dist/core/checks/docs-check.d.ts +18 -0
- package/dist/core/checks/docs-check.js +186 -0
- package/dist/core/code-shape.d.ts +7 -2
- package/dist/core/code-shape.js +46 -16
- package/dist/core/env-reads.d.ts +15 -0
- package/dist/core/env-reads.js +126 -0
- package/dist/core/human-gate.js +6 -3
- package/dist/core/profiles.d.ts +8 -0
- package/dist/core/profiles.js +43 -2
- package/dist/core/root-resolver.d.ts +4 -0
- package/dist/core/root-resolver.js +32 -0
- package/dist/core/setup/gitignore-block.d.ts +17 -0
- package/dist/core/setup/gitignore-block.js +182 -0
- package/dist/core/setup/pm-playbook.d.ts +1 -1
- package/dist/core/setup/pm-playbook.js +1 -1
- package/dist/core/setup/setup-planner.js +36 -3
- package/dist/core/setup/types.d.ts +6 -0
- package/dist/core/verifier/clean-tree.d.ts +14 -0
- package/dist/core/verifier/clean-tree.js +151 -0
- package/dist/core/verifier/executor.d.ts +3 -1
- package/dist/core/verifier/executor.js +54 -31
- package/dist/core/verifier/isolation.d.ts +25 -0
- package/dist/core/verifier/isolation.js +77 -0
- package/dist/core/verifier/portability-lint.d.ts +2 -0
- package/dist/core/verifier/portability-lint.js +64 -0
- package/dist/core/verifier/skip-policy.d.ts +8 -0
- package/dist/core/verifier/skip-policy.js +13 -0
- package/dist/core/verifier/task-verifier.d.ts +6 -8
- package/dist/core/verifier/task-verifier.js +68 -39
- package/dist/core/verifier/types.d.ts +4 -0
- package/dist/core/verifier/verify-env.d.ts +2 -0
- package/dist/core/verifier/verify-env.js +19 -0
- package/dist/index.js +33 -4
- package/dist/mcp/server.js +9 -2
- package/dist/scanner/types.d.ts +4 -0
- package/dist/scanner/types.js +9 -0
- package/dist/server/routes/task-routes.d.ts +1 -0
- package/dist/server/studio-ui/pages/benchmarks/script.js +9 -8
- package/dist/server/studio-ui/pages/benchmarks/styles.js +3 -1
- package/dist/server/studio-ui/pages/database/browser.js +1 -1
- package/dist/server/studio-ui/pages/database/connection.js +5 -4
- package/dist/server/studio-ui/pages/database/data-shell.js +8 -13
- package/dist/server/studio-ui/pages/database/drift.js +1 -1
- package/dist/server/studio-ui/pages/database/events.js +1 -2
- package/dist/server/studio-ui/pages/database/explorer.js +4 -4
- package/dist/server/studio-ui/pages/flow/layout.js +1 -1
- package/dist/server/studio-ui/pages/flow/styles.js +1 -1
- package/dist/server/studio-ui/pages/flow/timeline-view.js +1 -1
- package/dist/server/studio-ui/pages/home/script.js +26 -29
- package/dist/server/studio-ui/pages/home/styles.js +2 -6
- package/dist/server/studio-ui/pages/tasks/script.js +25 -40
- package/dist/server/studio-ui/pages/tasks/styles.js +9 -9
- package/dist/server/studio-ui/pages/team/script.js +27 -8
- package/dist/server/studio-ui/pages/team/styles.js +0 -1
- package/dist/server/studio-ui/shell/console.js +13 -7
- package/dist/server/studio-ui/shell/detail-view.js +3 -3
- package/dist/server/studio-ui/shell/dispatch.js +9 -8
- package/dist/server/studio-ui/shell/events-tools.js +11 -0
- package/dist/server/studio-ui/shell/format.js +31 -2
- package/dist/server/studio-ui/shell/markup-head.js +1 -1
- package/dist/server/studio-ui/shell/markup-tools.js +13 -8
- package/dist/server/studio-ui/shell/model.js +8 -2
- package/dist/server/studio-ui/shell/paint.js +1 -3
- package/dist/server/studio-ui/shell/pipeline-state.js +2 -11
- package/dist/server/studio-ui/shell/pulse.js +8 -7
- package/dist/server/studio-ui/shell/sidebar-view.js +2 -3
- package/dist/server/studio-ui/shell/triage.js +1 -1
- package/dist/server/studio-ui/styles/console.js +3 -0
- package/dist/server/studio-ui/styles/shell.js +4 -6
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +15 -8
- package/plugin/agents/verifier.md +16 -11
- package/plugin/skills/nativ/SKILL.md +18 -15
- package/templates/AGENTS.md +18 -15
- package/templates/GEMINI.md +1 -1
- package/templates/claude-agents/architect.md +15 -8
- package/templates/claude-agents/verifier.md +16 -11
- package/templates/dot-ai/subagents/architect.md +2 -2
- package/templates/dot-ai/subagents/qa-tester.md +14 -13
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { buildDesiredConfig, isObject } from './config-merger.js';
|
|
5
|
+
import { AGENT_FILES, MCP_SERVER_NAME, managedState } from './types.js';
|
|
6
|
+
export const BLOCK_START = '# >>> nativ (managed) >>>';
|
|
7
|
+
export const BLOCK_END = '# <<< nativ (managed) <<<';
|
|
8
|
+
/** Secret and machine-local paths: ignored even when the team shares nativ's files. */
|
|
9
|
+
export const SHARED_PATHS = [
|
|
10
|
+
'/.nativ/*.local.json',
|
|
11
|
+
'/.nativ/logs/',
|
|
12
|
+
'/.nativ/*.log',
|
|
13
|
+
'/.nativ/notify-state.json',
|
|
14
|
+
'/.worktrees/',
|
|
15
|
+
];
|
|
16
|
+
const read = (root, rel) => {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync(path.join(root, rel), 'utf8');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** Exactly what older nativ versions wrote for CLAUDE.md / GEMINI.md. */
|
|
25
|
+
function isOldPointer(text) {
|
|
26
|
+
const lf = text.replace(/\r\n/g, '\n');
|
|
27
|
+
return ['Claude Code', 'Gemini / Antigravity'].some((t) => lf === `# ${t} directive\n\n@AGENTS.md\n`);
|
|
28
|
+
}
|
|
29
|
+
function isNativOwnedFile(text, allowPointer) {
|
|
30
|
+
if (text === null)
|
|
31
|
+
return false;
|
|
32
|
+
if (managedState(text) !== 'unmanaged')
|
|
33
|
+
return true;
|
|
34
|
+
return allowPointer && isOldPointer(text);
|
|
35
|
+
}
|
|
36
|
+
const parseObject = (text) => {
|
|
37
|
+
if (text === null)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const value = JSON.parse(text);
|
|
41
|
+
return isObject(value) ? value : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
48
|
+
/** nativ's own server entry, for whatever command setup was configured to use. */
|
|
49
|
+
function ownedServer(entry) {
|
|
50
|
+
if (!isObject(entry) || typeof entry.command !== 'string')
|
|
51
|
+
return false;
|
|
52
|
+
if (Object.keys(entry).some((k) => k !== 'command' && k !== 'args'))
|
|
53
|
+
return false;
|
|
54
|
+
const args = entry.args;
|
|
55
|
+
if (!Array.isArray(args) || args.length === 0 || !args.every((a) => typeof a === 'string'))
|
|
56
|
+
return false;
|
|
57
|
+
const invocation = { command: entry.command, prefixArgs: args.slice(0, -1), portable: true };
|
|
58
|
+
const want = buildDesiredConfig(invocation).mcpServer;
|
|
59
|
+
return sameJson(entry, { command: want.command, args: want.args });
|
|
60
|
+
}
|
|
61
|
+
function onlyNativMcp(root) {
|
|
62
|
+
const json = parseObject(read(root, '.mcp.json'));
|
|
63
|
+
if (!json)
|
|
64
|
+
return false;
|
|
65
|
+
const keys = Object.keys(json).filter((k) => k !== 'mcpServers');
|
|
66
|
+
if (keys.length || !isObject(json.mcpServers))
|
|
67
|
+
return false;
|
|
68
|
+
const servers = json.mcpServers;
|
|
69
|
+
const names = Object.keys(servers);
|
|
70
|
+
return names.length > 0 && names.every((n) => n === MCP_SERVER_NAME && ownedServer(servers[n]));
|
|
71
|
+
}
|
|
72
|
+
/** CLI strings that setup wrote into hook commands; `nativ` is always a candidate. */
|
|
73
|
+
function clisFromHooks(hooks) {
|
|
74
|
+
const found = new Set(['nativ']);
|
|
75
|
+
if (!isObject(hooks))
|
|
76
|
+
return [...found];
|
|
77
|
+
for (const groups of Object.values(hooks)) {
|
|
78
|
+
for (const g of Array.isArray(groups) ? groups : []) {
|
|
79
|
+
for (const h of isObject(g) && Array.isArray(g.hooks) ? g.hooks : []) {
|
|
80
|
+
const m = isObject(h) ? /^(.+) hook (check|context)$/.exec(String(h.command)) : null;
|
|
81
|
+
if (m)
|
|
82
|
+
found.add(m[1]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...found];
|
|
87
|
+
}
|
|
88
|
+
/** The generated config for an already-rendered CLI string (it may hold spaces or quotes). */
|
|
89
|
+
function desiredFor(cli) {
|
|
90
|
+
const mark = 'CLIMARK';
|
|
91
|
+
const text = JSON.stringify(buildDesiredConfig({ command: mark, prefixArgs: [], portable: true }));
|
|
92
|
+
return JSON.parse(text.split(mark).join(JSON.stringify(cli).slice(1, -1)));
|
|
93
|
+
}
|
|
94
|
+
function ownedHooks(hooks, wants) {
|
|
95
|
+
if (!isObject(hooks))
|
|
96
|
+
return false;
|
|
97
|
+
const groupOk = (event, g) => wants.some((want) => {
|
|
98
|
+
const w = event === 'PreToolUse' ? want.preToolUse : event === 'SessionStart' ? want.sessionStart : null;
|
|
99
|
+
const hook = w && { type: 'command', command: w.command, timeout: w.timeout };
|
|
100
|
+
return w !== null && sameJson(g, { matcher: w.matcher, hooks: [hook] });
|
|
101
|
+
});
|
|
102
|
+
return Object.entries(hooks).every(([event, groups]) => Array.isArray(groups) && groups.every((g) => groupOk(event, g)));
|
|
103
|
+
}
|
|
104
|
+
// Setup unions entries, so a file may hold those of an older invocation too.
|
|
105
|
+
function ownedPermissions(perms, wants) {
|
|
106
|
+
if (!isObject(perms))
|
|
107
|
+
return false;
|
|
108
|
+
const keys = ['allow', 'deny', 'ask'];
|
|
109
|
+
return Object.entries(perms).every(([k, v]) => {
|
|
110
|
+
const key = keys.find((x) => x === k);
|
|
111
|
+
return key !== undefined && Array.isArray(v) &&
|
|
112
|
+
v.every((e) => typeof e === 'string' && wants.some((w) => w[key].includes(e)));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function onlyNativSettings(root) {
|
|
116
|
+
const json = parseObject(read(root, '.claude/settings.json'));
|
|
117
|
+
if (!json)
|
|
118
|
+
return false;
|
|
119
|
+
const allowed = new Set(['permissions', 'hooks', 'enabledMcpjsonServers']);
|
|
120
|
+
if (Object.keys(json).some((k) => !allowed.has(k)))
|
|
121
|
+
return false;
|
|
122
|
+
const wants = clisFromHooks(json.hooks).map(desiredFor);
|
|
123
|
+
if (json.permissions !== undefined && !ownedPermissions(json.permissions, wants))
|
|
124
|
+
return false;
|
|
125
|
+
if (json.hooks !== undefined && !ownedHooks(json.hooks, wants))
|
|
126
|
+
return false;
|
|
127
|
+
const enabled = json.enabledMcpjsonServers;
|
|
128
|
+
return enabled === undefined || (Array.isArray(enabled) && enabled.every((s) => s === MCP_SERVER_NAME));
|
|
129
|
+
}
|
|
130
|
+
/** Root-anchored paths nativ generated in this project, per the git-visibility choice. */
|
|
131
|
+
export function listGeneratedPaths(root, visibility = 'hidden') {
|
|
132
|
+
if (visibility === 'shared')
|
|
133
|
+
return [...SHARED_PATHS];
|
|
134
|
+
const paths = ['/.ai/', '/.nativ/', '/.worktrees/'];
|
|
135
|
+
for (const file of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) {
|
|
136
|
+
if (isNativOwnedFile(read(root, file), true))
|
|
137
|
+
paths.push(`/${file}`);
|
|
138
|
+
}
|
|
139
|
+
for (const file of AGENT_FILES) {
|
|
140
|
+
const rel = `.claude/agents/${file}`;
|
|
141
|
+
if (isNativOwnedFile(read(root, rel), false))
|
|
142
|
+
paths.push(`/${rel}`);
|
|
143
|
+
}
|
|
144
|
+
if (onlyNativMcp(root))
|
|
145
|
+
paths.push('/.mcp.json');
|
|
146
|
+
if (onlyNativSettings(root))
|
|
147
|
+
paths.push('/.claude/settings.json');
|
|
148
|
+
return paths;
|
|
149
|
+
}
|
|
150
|
+
export function renderBlock(paths, eol = '\n') {
|
|
151
|
+
return [BLOCK_START, ...paths, BLOCK_END].join(eol);
|
|
152
|
+
}
|
|
153
|
+
/** Inserts or replaces the managed block; text outside it and its line endings stay as they were. */
|
|
154
|
+
export function applyBlock(existing, paths) {
|
|
155
|
+
const text = existing ?? '';
|
|
156
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
157
|
+
const block = renderBlock(paths, eol);
|
|
158
|
+
const start = text.indexOf(BLOCK_START);
|
|
159
|
+
const endAt = start === -1 ? -1 : text.indexOf(BLOCK_END, start);
|
|
160
|
+
if (start !== -1 && endAt !== -1) {
|
|
161
|
+
return text.slice(0, start) + block + text.slice(endAt + BLOCK_END.length);
|
|
162
|
+
}
|
|
163
|
+
if (text.trim() === '')
|
|
164
|
+
return block + eol;
|
|
165
|
+
const sep = text.endsWith('\n') ? eol : eol + eol;
|
|
166
|
+
return text + sep + block + eol;
|
|
167
|
+
}
|
|
168
|
+
/** Lists generated paths that git already tracks. Non-git folders give an empty list. */
|
|
169
|
+
export function trackedNativPaths(root, paths) {
|
|
170
|
+
const none = { tracked: [], command: '' };
|
|
171
|
+
const specs = paths.map((p) => p.replace(/^\//, '')).filter((p) => !p.includes('*'));
|
|
172
|
+
if (!specs.length)
|
|
173
|
+
return none;
|
|
174
|
+
const out = spawnSync('git', ['ls-files', '-z', '--', ...specs], { cwd: root, encoding: 'utf8' });
|
|
175
|
+
if (out.status !== 0 || !out.stdout)
|
|
176
|
+
return none;
|
|
177
|
+
const files = out.stdout.split('\0').filter(Boolean);
|
|
178
|
+
const tracked = specs.filter((s) => files.some((f) => f === s || f.startsWith(s.endsWith('/') ? s : `${s}/`)));
|
|
179
|
+
if (!tracked.length)
|
|
180
|
+
return none;
|
|
181
|
+
return { tracked, command: `git rm --cached -r -- ${tracked.join(' ')}` };
|
|
182
|
+
}
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* The project manager's playbook. The SessionStart hook prints it into the main session only:
|
|
3
3
|
* subagents load CLAUDE.md and AGENTS.md on every turn, so anything kept there is paid for by every worker.
|
|
4
4
|
*/
|
|
5
|
-
export declare const PM_PLAYBOOK = "## Working as Project Manager\n\n- Start each session with `nativ task next --json` (or the `nativ_task_next` MCP tool).\n- Delegate, do not implement. Hand each task to the `worker` agent by task id, passing the task's `recommendedModel` (haiku, sonnet or opus) as the Agent tool's `model`; with none, the worker's default applies. Ask the `verifier` agent for an independent check before you mark a milestone done. Ask the `architect` agent for anything that changes a contract.\n- Before the architect designs on existing code, run the `explorer` when `.ai/codebase_map.md` is missing or its commit is well behind `HEAD`, and pass its report to the architect. Give the explorer the current commit (`git rev-parse --short HEAD`); it cannot look it up. Pass `model: haiku` for a single lookup such as \"where is the auth middleware?\".\n- Keep usage low. Run this session on Sonnet. Agent defaults: architect Opus, worker Sonnet, verifier Haiku. Pass `haiku` for mechanical tasks (wording, docs, renames) and `sonnet` for the verifier at the end of a milestone. Never pass a model the human did not choose.\n- Independent tasks can run in parallel in worktrees (`nativ worktree create <taskId>`). On a Claude subscription every agent draws from the same usage limit, so run two or three at a time, not ten.\n- You may not edit `.ai/`. If the enforcement hook warns about a file, that is a scope or contract gap: escalate with `nativ task escalate`, do not edit around it.\n- `nativ doctor` confirms the integration is intact. Run it after updating Claude Code or nativ, and whenever hooks or MCP tools seem to be missing.\n\n## Autonomous execution\n\nWhen told to run autonomously (\"run the queue\", \"execute milestone\", AFK mode):\n1. `nativ_task_next`, then `nativ_task_start`.\n2. Delegate to the `worker` agent with the task id and recommended model.\n3. The worker completes the task itself; `nativ task complete` already ran the check. Do not run `nativ_verify` or `nativ_task_complete` again for it.\n4. Loop to the next task immediately, without asking the human.\n5. Stop only when the milestone is done (ask the `verifier` for a milestone check, then tell the human) or when a task is blocked or escalated (present the four-part summary).\n\n## Talking to the human\n\nThe human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked `(Recommended)`, then one direct question. Say \"the app tried to link a record to an item that does not exist yet\", not \"foreign key constraint violation\". Never paste stack traces; put raw output in a collapsed `<details>` block.";
|
|
5
|
+
export declare const PM_PLAYBOOK = "## Working as Project Manager\n\n- Start each session with `nativ task next --json` (or the `nativ_task_next` MCP tool).\n- Delegate, do not implement. Hand each task to the `worker` agent by task id, passing the task's `recommendedModel` (haiku, sonnet or opus) as the Agent tool's `model`; with none, the worker's default applies. Ask the `verifier` agent for an independent check before you mark a milestone done. Ask the `architect` agent for anything that changes a contract.\n- Before the architect designs on existing code, run the `explorer` when `.ai/codebase_map.md` is missing or its commit is well behind `HEAD`, and pass its report to the architect. Give the explorer the current commit (`git rev-parse --short HEAD`); it cannot look it up. Pass `model: haiku` for a single lookup such as \"where is the auth middleware?\".\n- Keep usage low. Run this session on Sonnet. Agent defaults: architect Opus, worker Sonnet, verifier Haiku. Pass `haiku` for mechanical tasks (wording, docs, renames) and `sonnet` for the verifier at the end of a milestone. Never pass a model the human did not choose.\n- Independent tasks can run in parallel in worktrees (`nativ worktree create <taskId>`). On a Claude subscription every agent draws from the same usage limit, so run two or three at a time, not ten.\n- You may not edit `.ai/`. If the enforcement hook warns about a file, that is a scope or contract gap: escalate with `nativ task escalate`, do not edit around it.\n- `nativ doctor` confirms the integration is intact. Run it after updating Claude Code or nativ, and whenever hooks or MCP tools seem to be missing.\n\n## Autonomous execution\n\nWhen told to run autonomously (\"run the queue\", \"execute milestone\", AFK mode):\n1. `nativ_task_next`, then `nativ_task_start`.\n2. Delegate to the `worker` agent with the task id and recommended model.\n3. The worker completes the task itself; `nativ task complete` already ran the check. Do not run `nativ_verify` or `nativ_task_complete` again for it.\n4. Loop to the next task immediately, without asking the human.\n5. Stop only when the milestone is done (ask the `verifier` for a milestone check, then tell the human) or when a task is blocked or escalated (present the four-part summary).\n\n## Talking to the human\n\nThe human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked `(Recommended)`, plus one line on when the other option would be the better choice, then one direct question. Say \"the app tried to link a record to an item that does not exist yet\", not \"foreign key constraint violation\". Never paste stack traces; put raw output in a collapsed `<details>` block.";
|
|
@@ -23,4 +23,4 @@ When told to run autonomously ("run the queue", "execute milestone", AFK mode):
|
|
|
23
23
|
|
|
24
24
|
## Talking to the human
|
|
25
25
|
|
|
26
|
-
The human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked \`(Recommended)\`, then one direct question. Say "the app tried to link a record to an item that does not exist yet", not "foreign key constraint violation". Never paste stack traces; put raw output in a collapsed \`<details>\` block.`;
|
|
26
|
+
The human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked \`(Recommended)\`, plus one line on when the other option would be the better choice, then one direct question. Say "the app tried to link a record to an item that does not exist yet", not "foreign key constraint violation". Never paste stack traces; put raw output in a collapsed \`<details>\` block.`;
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { MCP_SERVER_NAME, AGENT_FILES, resolveCliInvocation, isMachineBoundPath, stampManaged, managedState, } from './types.js';
|
|
5
5
|
import { buildDesiredConfig, mergeClaudeSettings, mergeMcpConfig, isObject, } from './config-merger.js';
|
|
6
|
+
import { applyBlock, listGeneratedPaths, trackedNativPaths } from './gitignore-block.js';
|
|
6
7
|
import { loadEnforcementMode, parseJsonLoose } from '../enforcement.js';
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
export function getTemplatesDir() {
|
|
@@ -44,6 +45,18 @@ export function configuredInvocation(root) {
|
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
/** Disk-derived paths plus files this run creates, in a fixed order so reruns match. */
|
|
49
|
+
function blockPaths(root, visibility, changes) {
|
|
50
|
+
const disk = listGeneratedPaths(root, visibility);
|
|
51
|
+
if (visibility === 'shared')
|
|
52
|
+
return disk;
|
|
53
|
+
const created = new Set(changes.filter((c) => c.action === 'created').map((c) => `/${c.path}`));
|
|
54
|
+
const canonical = [
|
|
55
|
+
'/.ai/', '/.nativ/', '/.worktrees/', '/AGENTS.md', '/CLAUDE.md', '/GEMINI.md',
|
|
56
|
+
...AGENT_FILES.map((f) => `/.claude/agents/${f}`), '/.mcp.json', '/.claude/settings.json',
|
|
57
|
+
];
|
|
58
|
+
return canonical.filter((p) => disk.includes(p) || created.has(p));
|
|
59
|
+
}
|
|
47
60
|
/** Computes what `nativ setup` would do, without touching disk. */
|
|
48
61
|
export function planSetup(rootArg, options = {}) {
|
|
49
62
|
const root = path.resolve(rootArg);
|
|
@@ -55,6 +68,7 @@ export function planSetup(rootArg, options = {}) {
|
|
|
55
68
|
const changes = [];
|
|
56
69
|
const warnings = [];
|
|
57
70
|
const writes = new Map();
|
|
71
|
+
let visibility = options.gitVisibility ?? 'hidden';
|
|
58
72
|
if (!invocation.portable) {
|
|
59
73
|
warnings.push('nativ is not on PATH, so the generated config points at this machine\'s install. ' +
|
|
60
74
|
'Install it globally (npm i -g @njeybe/nativ) or pass --command "npx -y @njeybe/nativ" so the config works for your team.');
|
|
@@ -143,7 +157,7 @@ export function planSetup(rootArg, options = {}) {
|
|
|
143
157
|
for (const [file, title] of [['CLAUDE.md', 'Claude Code'], ['GEMINI.md', 'Gemini / Antigravity']]) {
|
|
144
158
|
const existing = readText(path.join(root, file));
|
|
145
159
|
if (existing === null)
|
|
146
|
-
record(file, null, `# ${title} directive\n\n@AGENTS.md\n`);
|
|
160
|
+
record(file, null, stampManaged(`# ${title} directive\n\n@AGENTS.md\n`));
|
|
147
161
|
else
|
|
148
162
|
changes.push({ path: file, action: 'unchanged', detail: /AGENTS\.md/.test(existing) ? undefined : 'exists; does not reference AGENTS.md' });
|
|
149
163
|
}
|
|
@@ -160,9 +174,21 @@ export function planSetup(rootArg, options = {}) {
|
|
|
160
174
|
next.enforcement = loadEnforcementMode(root);
|
|
161
175
|
detail = `enforcement=${String(next.enforcement)}`;
|
|
162
176
|
}
|
|
177
|
+
if (options.gitVisibility && next.gitVisibility !== options.gitVisibility) {
|
|
178
|
+
next.gitVisibility = options.gitVisibility;
|
|
179
|
+
detail = [detail, `gitVisibility=${options.gitVisibility}`].filter(Boolean).join(', ');
|
|
180
|
+
}
|
|
163
181
|
record('.nativ/config.json', config.text, detail || config.text === null ? serializeJson(next) : (config.text ?? ''), detail);
|
|
182
|
+
visibility = options.gitVisibility ?? (next.gitVisibility === 'shared' ? 'shared' : 'hidden');
|
|
164
183
|
}
|
|
165
|
-
|
|
184
|
+
// .gitignore: the managed block, from what is on disk plus what this run creates
|
|
185
|
+
const paths = blockPaths(root, visibility, changes);
|
|
186
|
+
const ignore = readText(path.join(root, '.gitignore'));
|
|
187
|
+
record('.gitignore', ignore, applyBlock(ignore, paths), 'nativ block');
|
|
188
|
+
const gitRmCommand = trackedNativPaths(root, paths).command;
|
|
189
|
+
if (gitRmCommand)
|
|
190
|
+
warnings.push(`nativ files are already tracked by git. To stop tracking them, run: ${gitRmCommand}`);
|
|
191
|
+
return { root, invocation, changes, warnings, writes, gitRmCommand: gitRmCommand || undefined };
|
|
166
192
|
}
|
|
167
193
|
/** Writes the planned differences. Idempotent: applying twice changes nothing the second time. */
|
|
168
194
|
export function applySetup(rootArg, options = {}) {
|
|
@@ -176,5 +202,12 @@ export function applySetup(rootArg, options = {}) {
|
|
|
176
202
|
fs.renameSync(tmp, file);
|
|
177
203
|
}
|
|
178
204
|
}
|
|
179
|
-
return {
|
|
205
|
+
return {
|
|
206
|
+
root: plan.root,
|
|
207
|
+
dryRun: options.dryRun === true,
|
|
208
|
+
invocation: plan.invocation,
|
|
209
|
+
changes: plan.changes,
|
|
210
|
+
warnings: plan.warnings,
|
|
211
|
+
gitRmCommand: plan.gitRmCommand,
|
|
212
|
+
};
|
|
180
213
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EnforcementMode } from '../enforcement.js';
|
|
2
|
+
import type { GitVisibility } from './gitignore-block.js';
|
|
2
3
|
export declare const MCP_SERVER_NAME = "nativ";
|
|
3
4
|
export declare const AGENT_FILES: readonly ["architect.md", "worker.md", "verifier.md", "explorer.md"];
|
|
4
5
|
export declare const MANAGED_PREFIX = "<!-- nativ:managed sha256=";
|
|
@@ -27,6 +28,8 @@ export interface SetupOptions {
|
|
|
27
28
|
enforcement?: EnforcementMode;
|
|
28
29
|
/** Also overwrite agent files that were edited by hand or are not nativ's. */
|
|
29
30
|
force?: boolean;
|
|
31
|
+
/** Whether nativ's files stay out of git (hidden, the default) or are shared with the team. */
|
|
32
|
+
gitVisibility?: GitVisibility;
|
|
30
33
|
hasBinary?: (name: string) => boolean;
|
|
31
34
|
templatesDir?: string;
|
|
32
35
|
}
|
|
@@ -37,6 +40,8 @@ export interface SetupPlan {
|
|
|
37
40
|
warnings: string[];
|
|
38
41
|
/** Full new content per changed path, keyed by the same relative path. */
|
|
39
42
|
writes: Map<string, string>;
|
|
43
|
+
/** `git rm --cached` command for nativ files git already tracks; never run by nativ. */
|
|
44
|
+
gitRmCommand?: string;
|
|
40
45
|
}
|
|
41
46
|
export interface SetupResult {
|
|
42
47
|
root: string;
|
|
@@ -44,6 +49,7 @@ export interface SetupResult {
|
|
|
44
49
|
invocation: CliInvocation;
|
|
45
50
|
changes: AssetChange[];
|
|
46
51
|
warnings: string[];
|
|
52
|
+
gitRmCommand?: string;
|
|
47
53
|
}
|
|
48
54
|
export interface TemplateFilePlan {
|
|
49
55
|
action: AssetAction;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const CLEAN_TREE_PREFIX = "nativ-verify-";
|
|
2
|
+
export type CleanTree = {
|
|
3
|
+
ok: true;
|
|
4
|
+
dir: string;
|
|
5
|
+
dispose: () => Promise<void>;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function disposeTree(root: string, dir: string, names: string[], unlink?: (dir: string, name: string) => boolean): Promise<void>;
|
|
11
|
+
export declare function createCleanTree(root: string, opts?: {
|
|
12
|
+
links?: string[];
|
|
13
|
+
}): Promise<CleanTree>;
|
|
14
|
+
export declare function sweepStaleCleanTrees(root: string): Promise<number>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { linkWorktreeDir, safeUnlinkWorktreeDir } from '../root-resolver.js';
|
|
6
|
+
export const CLEAN_TREE_PREFIX = 'nativ-verify-';
|
|
7
|
+
const STALE_MS = 60 * 60 * 1000;
|
|
8
|
+
function git(cwd, args) {
|
|
9
|
+
const opts = { cwd, maxBuffer: 64 * 1024 * 1024, windowsHide: true };
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
execFile('git', args, opts, (err, stdout) => {
|
|
12
|
+
const code = err ? (typeof err.code === 'number' ? err.code : 1) : 0;
|
|
13
|
+
resolve({ code, out: String(stdout ?? '') });
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
18
|
+
async function removeDir(dir) {
|
|
19
|
+
for (let i = 0; i < 6; i++) {
|
|
20
|
+
try {
|
|
21
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
if (!fs.existsSync(dir))
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
const code = e.code;
|
|
27
|
+
if (code !== 'EBUSY' && code !== 'EPERM' && code !== 'ENOTEMPTY')
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await sleep(150 * (i + 1));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Links go first: removing a tree can follow a junction into the real folder.
|
|
34
|
+
function linkRemains(dir, names) {
|
|
35
|
+
return names.some((n) => {
|
|
36
|
+
try {
|
|
37
|
+
return fs.lstatSync(path.join(dir, n)).isSymbolicLink();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// unlink is injectable so tests can simulate a link that refuses to go away.
|
|
45
|
+
export async function disposeTree(root, dir, names, unlink = safeUnlinkWorktreeDir) {
|
|
46
|
+
try {
|
|
47
|
+
for (const n of names)
|
|
48
|
+
unlink(dir, n);
|
|
49
|
+
// A surviving link means a recursive delete could reach the real folder: leave it for the sweep.
|
|
50
|
+
if (linkRemains(dir, names))
|
|
51
|
+
return;
|
|
52
|
+
for (let i = 0; i < 4; i++) {
|
|
53
|
+
const r = await git(root, ['worktree', 'remove', '--force', dir]);
|
|
54
|
+
if (r.code === 0 || !fs.existsSync(dir))
|
|
55
|
+
break;
|
|
56
|
+
await sleep(150 * (i + 1));
|
|
57
|
+
}
|
|
58
|
+
await removeDir(dir);
|
|
59
|
+
await git(root, ['worktree', 'prune']);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Cleanup never throws.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function copyUntracked(root, dir) {
|
|
66
|
+
const r = await git(root, ['ls-files', '--others', '--exclude-standard', '-z']);
|
|
67
|
+
for (const rel of r.out.split('\0').filter(Boolean)) {
|
|
68
|
+
const from = path.join(root, rel);
|
|
69
|
+
const to = path.join(dir, rel);
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.statSync(from).isFile())
|
|
72
|
+
continue;
|
|
73
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
74
|
+
fs.copyFileSync(from, to);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// A file that vanished mid-copy is not worth failing the tree.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function createCleanTree(root, opts = {}) {
|
|
82
|
+
const inRepo = await git(root, ['rev-parse', '--is-inside-work-tree']);
|
|
83
|
+
if (inRepo.code !== 0)
|
|
84
|
+
return { ok: false, reason: 'clean isolation needs a git repository' };
|
|
85
|
+
const head = await git(root, ['rev-parse', '--verify', 'HEAD']);
|
|
86
|
+
if (head.code !== 0)
|
|
87
|
+
return { ok: false, reason: 'clean isolation needs at least one commit' };
|
|
88
|
+
const stash = await git(root, ['stash', 'create']);
|
|
89
|
+
const commit = stash.code === 0 && stash.out.trim() ? stash.out.trim() : head.out.trim();
|
|
90
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), CLEAN_TREE_PREFIX));
|
|
91
|
+
fs.rmdirSync(dir);
|
|
92
|
+
const args = ['-c', 'core.autocrlf=false', 'worktree', 'add', '--detach', dir, commit];
|
|
93
|
+
const add = await git(root, args);
|
|
94
|
+
if (add.code !== 0) {
|
|
95
|
+
await removeDir(dir);
|
|
96
|
+
return { ok: false, reason: 'could not create the clean tree' };
|
|
97
|
+
}
|
|
98
|
+
await copyUntracked(root, dir);
|
|
99
|
+
const linked = (opts.links ?? []).filter((n) => linkWorktreeDir(dir, root, n));
|
|
100
|
+
return { ok: true, dir, dispose: () => disposeTree(root, dir, linked) };
|
|
101
|
+
}
|
|
102
|
+
function ownedBy(root, dir) {
|
|
103
|
+
try {
|
|
104
|
+
const text = fs.readFileSync(path.join(dir, '.git'), 'utf8');
|
|
105
|
+
const gitdir = text.replace(/^gitdir:\s*/, '').trim();
|
|
106
|
+
const main = realOf(path.resolve(root, '.git')).toLowerCase();
|
|
107
|
+
return realOf(path.resolve(gitdir)).toLowerCase().startsWith(main);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Real paths make Windows short (8.3) and aliased paths compare equal.
|
|
114
|
+
function realOf(p) {
|
|
115
|
+
try {
|
|
116
|
+
return fs.realpathSync.native(p);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return p;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function linkNames(dir) {
|
|
123
|
+
return fs.readdirSync(dir).filter((n) => fs.lstatSync(path.join(dir, n)).isSymbolicLink());
|
|
124
|
+
}
|
|
125
|
+
export async function sweepStaleCleanTrees(root) {
|
|
126
|
+
let removed = 0;
|
|
127
|
+
try {
|
|
128
|
+
const tmp = os.tmpdir();
|
|
129
|
+
for (const name of fs.readdirSync(tmp)) {
|
|
130
|
+
if (!name.startsWith(CLEAN_TREE_PREFIX))
|
|
131
|
+
continue;
|
|
132
|
+
const dir = path.join(tmp, name);
|
|
133
|
+
try {
|
|
134
|
+
if (Date.now() - fs.statSync(dir).mtimeMs < STALE_MS)
|
|
135
|
+
continue;
|
|
136
|
+
if (!ownedBy(root, dir))
|
|
137
|
+
continue;
|
|
138
|
+
await disposeTree(root, dir, linkNames(dir));
|
|
139
|
+
if (!fs.existsSync(dir))
|
|
140
|
+
removed++;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Skip entries we cannot read.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Nothing to sweep.
|
|
149
|
+
}
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
@@ -2,4 +2,6 @@ import type { VerificationResult } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Execute a verification command in the workspace directory with timeout and output capture.
|
|
4
4
|
*/
|
|
5
|
-
export declare function executeVerification(command: string, cwd: string, timeoutMs?: number
|
|
5
|
+
export declare function executeVerification(command: string, cwd: string, timeoutMs?: number, options?: {
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
}): Promise<VerificationResult>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
const MAX_OUTPUT = 10 * 1024 * 1024;
|
|
2
3
|
/**
|
|
3
4
|
* Execute a verification command in the workspace directory with timeout and output capture.
|
|
4
5
|
*/
|
|
5
|
-
export async function executeVerification(command, cwd, timeoutMs = 120_000) {
|
|
6
|
+
export async function executeVerification(command, cwd, timeoutMs = 120_000, options = {}) {
|
|
6
7
|
const trimmed = (command || '').trim();
|
|
7
8
|
// Treat empty, 'none', or 'skip' as an informational pass
|
|
8
9
|
if (!trimmed || trimmed.toLowerCase() === 'none' || trimmed.toLowerCase() === 'skip') {
|
|
@@ -26,37 +27,59 @@ export async function executeVerification(command, cwd, timeoutMs = 120_000) {
|
|
|
26
27
|
}
|
|
27
28
|
const start = Date.now();
|
|
28
29
|
return new Promise((resolve) => {
|
|
29
|
-
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
let out = '';
|
|
32
|
+
let errOut = '';
|
|
33
|
+
const child = spawn(finalCommand, {
|
|
30
34
|
cwd,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
shell: true,
|
|
36
|
+
env: options.env ?? { ...process.env, NATIV_VERIFY_SANDBOX: '1' },
|
|
37
|
+
detached: process.platform !== 'win32',
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
});
|
|
40
|
+
const cap = (cur, chunk) => cur.length < MAX_OUTPUT ? cur + chunk.toString() : cur;
|
|
41
|
+
child.stdout?.on('data', (c) => (out = cap(out, c)));
|
|
42
|
+
child.stderr?.on('data', (c) => (errOut = cap(errOut, c)));
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
timedOut = true;
|
|
45
|
+
killTree(child.pid);
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
const finish = (code, message) => {
|
|
48
|
+
clearTimeout(timer);
|
|
35
49
|
const durationMs = Date.now() - start;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const exitCode = typeof err.code === 'number' ? err.code : 1;
|
|
40
|
-
resolve({
|
|
41
|
-
success: false,
|
|
42
|
-
exitCode,
|
|
43
|
-
command: trimmed,
|
|
44
|
-
stdout: out,
|
|
45
|
-
stderr: errOut || err.message,
|
|
46
|
-
durationMs,
|
|
47
|
-
error: err.killed ? `Command timed out after ${timeoutMs}ms` : err.message,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
resolve({
|
|
52
|
-
success: true,
|
|
53
|
-
exitCode: 0,
|
|
54
|
-
command: trimmed,
|
|
55
|
-
stdout: out,
|
|
56
|
-
stderr: errOut,
|
|
57
|
-
durationMs,
|
|
58
|
-
});
|
|
50
|
+
if (code === 0 && !message && !timedOut) {
|
|
51
|
+
resolve({ success: true, exitCode: 0, command: trimmed, stdout: out, stderr: errOut, durationMs });
|
|
52
|
+
return;
|
|
59
53
|
}
|
|
60
|
-
|
|
54
|
+
const error = timedOut
|
|
55
|
+
? `Command timed out after ${timeoutMs}ms`
|
|
56
|
+
: message ?? `Command failed: ${trimmed}`;
|
|
57
|
+
resolve({
|
|
58
|
+
success: false,
|
|
59
|
+
exitCode: typeof code === 'number' && code !== 0 ? code : 1,
|
|
60
|
+
command: trimmed,
|
|
61
|
+
stdout: out,
|
|
62
|
+
stderr: errOut || error,
|
|
63
|
+
durationMs,
|
|
64
|
+
error,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
child.on('error', (e) => finish(null, e.message));
|
|
68
|
+
child.on('close', (code) => finish(code));
|
|
61
69
|
});
|
|
62
70
|
}
|
|
71
|
+
/** Kills the process and all its descendants. */
|
|
72
|
+
function killTree(pid) {
|
|
73
|
+
if (!pid)
|
|
74
|
+
return;
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
execFile('taskkill', ['/pid', String(pid), '/T', '/F'], () => { });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
process.kill(-pid, 'SIGKILL');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Group already gone
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MasterPlanTask } from '../../scanner/types.js';
|
|
2
|
+
import type { PhasesOutcome, VerificationResult } from './types.js';
|
|
3
|
+
/** Where a verification runs: the real tree, or a disposable clean copy. */
|
|
4
|
+
export interface IsolationPlan {
|
|
5
|
+
mode: 'none' | 'clean';
|
|
6
|
+
dir?: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
warnings: string[];
|
|
9
|
+
dispose?: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare const NO_ISOLATION: IsolationPlan;
|
|
12
|
+
export declare function needsRun(commands: string[], configDir: string, sharedPhases: boolean): boolean;
|
|
13
|
+
export declare function openIsolation(cwd: string, configDir: string, wanted: 'none' | 'clean'): Promise<IsolationPlan>;
|
|
14
|
+
/** Like runVerifyPhases, but with an explicit environment for each phase. */
|
|
15
|
+
export declare function runPhasesIn(dir: string, configDir: string, timeout: number | undefined, env?: NodeJS.ProcessEnv): Promise<PhasesOutcome>;
|
|
16
|
+
export interface RunOptions {
|
|
17
|
+
cwd: string;
|
|
18
|
+
configDir?: string;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
phases?: PhasesOutcome;
|
|
21
|
+
isolation?: 'none' | 'clean';
|
|
22
|
+
plan?: IsolationPlan;
|
|
23
|
+
}
|
|
24
|
+
export declare function runInPlan(task: MasterPlanTask, o: RunOptions, plan: IsolationPlan): Promise<VerificationResult>;
|
|
25
|
+
export declare function diagnose(task: MasterPlanTask, o: RunOptions, failed: VerificationResult, failure: 'warn' | 'block'): Promise<VerificationResult>;
|