@njeybe/nativ 2.8.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 +21 -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/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/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +14 -7
- package/plugin/agents/verifier.md +16 -14
- package/plugin/skills/nativ/SKILL.md +11 -8
- package/templates/AGENTS.md +11 -8
- package/templates/claude-agents/architect.md +14 -7
- package/templates/claude-agents/verifier.md +16 -14
- package/templates/dot-ai/subagents/qa-tester.md +14 -13
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
const TEST_DIR = /(^|\/)(test|tests|__tests__|scripts)\//;
|
|
5
|
+
const TEST_NAME = /(\.test\.|\.spec\.)|(^|\/)test_[^/]*\.py$|_test\.go$/;
|
|
6
|
+
const LITERAL = /(["'`])([^"'`\n$]{3,200}?)\1/g;
|
|
7
|
+
const SPAWN_NATIV = /(spawn|spawnSync|exec|execSync|execFile|execFileSync)\(\s*['"`]nativ\b/;
|
|
8
|
+
const AI_HINT = 'track a fixture copy in the repo instead';
|
|
9
|
+
export function isPortabilityTarget(file) {
|
|
10
|
+
const f = file.split(String.fromCharCode(92)).join('/');
|
|
11
|
+
return TEST_DIR.test(f) || TEST_NAME.test(f);
|
|
12
|
+
}
|
|
13
|
+
function looksLikePath(s) {
|
|
14
|
+
if (/^[a-z]+:\/\//i.test(s) || /\s/.test(s) || s.startsWith('-'))
|
|
15
|
+
return false;
|
|
16
|
+
return /^\.{0,2}\/?[\w.@-]+(\/[\w.@-]+)+\/?$/.test(s) || /^\.[\w-]+(\/|$)/.test(s);
|
|
17
|
+
}
|
|
18
|
+
function scan(cwd, file, hits, spawns) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(path.join(cwd, file), 'utf8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
text.split(/\r?\n/).forEach((line, i) => {
|
|
27
|
+
if (SPAWN_NATIV.test(line))
|
|
28
|
+
spawns.push(`${file}:${i + 1} spawns nativ, which may not be installed in CI`);
|
|
29
|
+
for (const m of line.matchAll(LITERAL)) {
|
|
30
|
+
if (looksLikePath(m[2]))
|
|
31
|
+
hits.push({ file, line: i + 1, lit: m[2] });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function ignoredSet(cwd, paths) {
|
|
36
|
+
const r = spawnSync('git', ['check-ignore', '--stdin'], {
|
|
37
|
+
cwd, input: paths.join('\n'), encoding: 'utf8',
|
|
38
|
+
});
|
|
39
|
+
if (r.error || (r.status !== 0 && r.status !== 1))
|
|
40
|
+
return null;
|
|
41
|
+
return new Set(r.stdout.split(/\r?\n/).filter(Boolean));
|
|
42
|
+
}
|
|
43
|
+
// Cheap warning pass: findings only, never changes a result.
|
|
44
|
+
export function lintPortability(cwd, targetFiles) {
|
|
45
|
+
const files = targetFiles.map((f) => f.split(String.fromCharCode(92)).join('/')).filter(isPortabilityTarget);
|
|
46
|
+
if (!files.length)
|
|
47
|
+
return [];
|
|
48
|
+
const hits = [];
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const f of files)
|
|
51
|
+
scan(cwd, f, hits, out);
|
|
52
|
+
const uniq = [...new Set(hits.map((h) => h.lit.replace(/^\.\//, '')))];
|
|
53
|
+
const ignored = uniq.length ? ignoredSet(cwd, uniq) : new Set();
|
|
54
|
+
if (!ignored)
|
|
55
|
+
return [];
|
|
56
|
+
for (const h of hits) {
|
|
57
|
+
const key = h.lit.replace(/^\.\//, '');
|
|
58
|
+
if (!ignored.has(key) && !ignored.has(`${key}/`))
|
|
59
|
+
continue;
|
|
60
|
+
const hint = key.startsWith('.ai/') ? `; ${AI_HINT}` : '';
|
|
61
|
+
out.push(`${h.file}:${h.line} reads ${h.lit}, which is gitignored: this check cannot run in CI${hint}`);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MasterPlanTask } from '../../scanner/types.js';
|
|
2
|
+
export type SkipMode = 'allow' | 'warn' | 'block';
|
|
3
|
+
export interface SkipDecision {
|
|
4
|
+
action: 'pass' | 'warn' | 'block';
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Decides what a skipped check (empty, none or skip) means for a task under the profile. */
|
|
8
|
+
export declare function evaluateSkip(task: MasterPlanTask, mode: SkipMode): SkipDecision;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const FIX = 'Add a real verificationCommand, or set verificationKind "manual" with notes saying how it was checked.';
|
|
2
|
+
/** Decides what a skipped check (empty, none or skip) means for a task under the profile. */
|
|
3
|
+
export function evaluateSkip(task, mode) {
|
|
4
|
+
if (task.verificationKind === 'manual' && (task.notes ?? '').trim())
|
|
5
|
+
return { action: 'pass' };
|
|
6
|
+
if (mode === 'allow')
|
|
7
|
+
return { action: 'pass' };
|
|
8
|
+
const why = `Task [${task.id}] has no real check, so nothing proves it works. ${FIX}`;
|
|
9
|
+
// Older plans without a kind only warn, so work already in progress does not stall.
|
|
10
|
+
if (mode === 'block' && task.verificationKind !== undefined)
|
|
11
|
+
return { action: 'block', message: why };
|
|
12
|
+
return { action: 'warn', message: why };
|
|
13
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MasterPlan, MasterPlanTask } from '../../scanner/types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RunOptions } from './isolation.js';
|
|
3
|
+
import type { BatchVerificationResult, TaskVerificationResult, VerificationResult } from './types.js';
|
|
3
4
|
export declare function loadMasterPlan(targetDir: string): {
|
|
4
5
|
planPath: string;
|
|
5
6
|
plan: MasterPlan | null;
|
|
@@ -12,14 +13,10 @@ export declare function loadMasterPlan(targetDir: string): {
|
|
|
12
13
|
export declare function applyCodeShape(result: VerificationResult, targetDir: string, task: MasterPlanTask): VerificationResult;
|
|
13
14
|
/**
|
|
14
15
|
* The one verification every path shares: the configured phases in order, stopping at the
|
|
15
|
-
* first failure, then the task's own command
|
|
16
|
+
* first failure, then the task's own command (both in a clean copy when isolation is on),
|
|
17
|
+
* then the code-shape check on the real tree.
|
|
16
18
|
*/
|
|
17
|
-
export declare function runTaskVerification(task: MasterPlanTask, options:
|
|
18
|
-
cwd: string;
|
|
19
|
-
configDir?: string;
|
|
20
|
-
timeout?: number;
|
|
21
|
-
phases?: PhasesOutcome;
|
|
22
|
-
}): Promise<VerificationResult>;
|
|
19
|
+
export declare function runTaskVerification(task: MasterPlanTask, options: RunOptions): Promise<VerificationResult>;
|
|
23
20
|
/**
|
|
24
21
|
* Run verification for a single specific task in the plan.
|
|
25
22
|
*/
|
|
@@ -34,4 +31,5 @@ export declare function verifyBatch(targetDirArg?: string, options?: {
|
|
|
34
31
|
milestoneId?: string;
|
|
35
32
|
all?: boolean;
|
|
36
33
|
timeout?: number;
|
|
34
|
+
isolation?: 'none' | 'clean';
|
|
37
35
|
}): Promise<BatchVerificationResult>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { checkCodeShape, formatCodeShapeReport } from '../code-shape.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { resolveActiveProfileSettings } from '../profiles.js';
|
|
5
|
+
import { resolveMainRoot } from '../root-resolver.js';
|
|
6
|
+
import { NO_ISOLATION, needsRun, openIsolation, runPhasesIn, runInPlan, diagnose, } from './isolation.js';
|
|
6
7
|
export function loadMasterPlan(targetDir) {
|
|
7
8
|
const planPath = path.join(targetDir, '.ai', 'master_plan.json');
|
|
8
9
|
if (!fs.existsSync(planPath)) {
|
|
@@ -25,37 +26,51 @@ export function applyCodeShape(result, targetDir, task) {
|
|
|
25
26
|
if (result.skipped || !result.success)
|
|
26
27
|
return result;
|
|
27
28
|
const report = checkCodeShape(targetDir, task.targetFiles || []);
|
|
28
|
-
if (report.
|
|
29
|
+
if (report.issues.length === 0)
|
|
29
30
|
return result;
|
|
30
31
|
result.codeShape = report;
|
|
31
32
|
const summary = formatCodeShapeReport(report);
|
|
32
33
|
if (summary)
|
|
33
34
|
console.error(summary);
|
|
34
|
-
|
|
35
|
+
const blocking = report.issues.filter((i) => (i.category === 'config' ? report.configMode : report.mode) === 'block');
|
|
36
|
+
if (blocking.length > 0) {
|
|
37
|
+
const names = [...new Set(blocking.filter((i) => i.category === 'config')
|
|
38
|
+
.map((i) => i.message.split(' ')[0]))];
|
|
39
|
+
const config = names.length > 0 ? `; unset-variable fallbacks: ${names.join(', ')}` : '';
|
|
35
40
|
result.success = false;
|
|
36
41
|
result.exitCode = 1;
|
|
37
|
-
result.error = `Code shape check failed: ${
|
|
42
|
+
result.error = `Code shape check failed: ${blocking.length} issue(s) set to block${config}`;
|
|
38
43
|
}
|
|
39
44
|
return result;
|
|
40
45
|
}
|
|
41
46
|
/**
|
|
42
47
|
* The one verification every path shares: the configured phases in order, stopping at the
|
|
43
|
-
* first failure, then the task's own command
|
|
48
|
+
* first failure, then the task's own command (both in a clean copy when isolation is on),
|
|
49
|
+
* then the code-shape check on the real tree.
|
|
44
50
|
*/
|
|
45
51
|
export async function runTaskVerification(task, options) {
|
|
46
52
|
const configDir = options.configDir ?? options.cwd;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const settings = resolveActiveProfileSettings(resolveMainRoot(configDir));
|
|
54
|
+
const wanted = options.isolation ?? settings.verifyIsolation;
|
|
55
|
+
const owned = !options.plan;
|
|
56
|
+
const plan = options.plan
|
|
57
|
+
?? (needsRun([task.verificationCommand], configDir, !!options.phases)
|
|
58
|
+
? await openIsolation(options.cwd, configDir, wanted)
|
|
59
|
+
: NO_ISOLATION);
|
|
60
|
+
try {
|
|
61
|
+
let result = await runInPlan(task, options, plan);
|
|
62
|
+
result.isolation = plan.mode;
|
|
63
|
+
if (plan.warnings.length)
|
|
64
|
+
result.warnings = [...plan.warnings];
|
|
65
|
+
if (plan.mode === 'clean' && !result.success) {
|
|
66
|
+
result = await diagnose(task, options, result, settings.verifyIsolationFailure);
|
|
67
|
+
}
|
|
68
|
+
return applyCodeShape(result, options.cwd, task);
|
|
51
69
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raw.durationMs += options.phases ? 0 : outcome.reports.reduce((sum, r) => sum + r.durationMs, 0);
|
|
56
|
-
raw.skipped = false;
|
|
70
|
+
finally {
|
|
71
|
+
if (owned)
|
|
72
|
+
await plan.dispose?.();
|
|
57
73
|
}
|
|
58
|
-
return applyCodeShape(raw, options.cwd, task);
|
|
59
74
|
}
|
|
60
75
|
/**
|
|
61
76
|
* Run verification for a single specific task in the plan.
|
|
@@ -119,6 +134,9 @@ export async function verifyTask(taskId, targetDirArg, options = {}) {
|
|
|
119
134
|
result,
|
|
120
135
|
};
|
|
121
136
|
}
|
|
137
|
+
function matchesMilestone(m, wanted) {
|
|
138
|
+
return m.id === wanted || m.name.toLowerCase() === wanted.toLowerCase();
|
|
139
|
+
}
|
|
122
140
|
/**
|
|
123
141
|
* Verify a batch of tasks (by milestone, all completed tasks, or the active in-progress task).
|
|
124
142
|
*/
|
|
@@ -160,7 +178,7 @@ export async function verifyBatch(targetDirArg, options = {}) {
|
|
|
160
178
|
if (options.taskId && t.id === options.taskId) {
|
|
161
179
|
candidates.push({ task: t, milestone: m });
|
|
162
180
|
}
|
|
163
|
-
else if (options.milestoneId && (m
|
|
181
|
+
else if (options.milestoneId && matchesMilestone(m, options.milestoneId)) {
|
|
164
182
|
candidates.push({ task: t, milestone: m });
|
|
165
183
|
}
|
|
166
184
|
else if (options.all) {
|
|
@@ -197,29 +215,40 @@ export async function verifyBatch(targetDirArg, options = {}) {
|
|
|
197
215
|
let failed = 0;
|
|
198
216
|
let skipped = 0;
|
|
199
217
|
let totalDuration = 0;
|
|
200
|
-
// The phases check the whole project, so one run serves every task
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
218
|
+
// The phases check the whole project, so one run (and one clean tree) serves every task.
|
|
219
|
+
const wanted = options.isolation
|
|
220
|
+
?? resolveActiveProfileSettings(resolveMainRoot(targetDir)).verifyIsolation;
|
|
221
|
+
const commands = candidates.map((c) => c.task.verificationCommand);
|
|
222
|
+
const isolation = candidates.length && needsRun(commands, targetDir, false)
|
|
223
|
+
? await openIsolation(targetDir, targetDir, wanted)
|
|
224
|
+
: NO_ISOLATION;
|
|
225
|
+
try {
|
|
226
|
+
const phases = candidates.length
|
|
227
|
+
? await runPhasesIn(isolation.dir ?? targetDir, targetDir, options.timeout, isolation.env)
|
|
228
|
+
: { reports: [] };
|
|
229
|
+
totalDuration += phases.reports.reduce((sum, r) => sum + r.durationMs, 0);
|
|
230
|
+
for (const { task, milestone } of candidates) {
|
|
231
|
+
const runOpts = { cwd: targetDir, timeout: options.timeout, phases, plan: isolation, isolation: wanted };
|
|
232
|
+
const vResult = await runTaskVerification(task, runOpts);
|
|
233
|
+
totalDuration += vResult.durationMs;
|
|
234
|
+
if (vResult.skipped)
|
|
235
|
+
skipped++;
|
|
236
|
+
else if (vResult.success)
|
|
237
|
+
passed++;
|
|
238
|
+
else
|
|
239
|
+
failed++;
|
|
240
|
+
results.push({
|
|
241
|
+
taskId: task.id,
|
|
242
|
+
title: task.title,
|
|
243
|
+
milestoneId: milestone.id,
|
|
244
|
+
milestoneName: milestone.name,
|
|
245
|
+
status: task.status,
|
|
246
|
+
result: vResult,
|
|
247
|
+
});
|
|
214
248
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
milestoneId: milestone.id,
|
|
219
|
-
milestoneName: milestone.name,
|
|
220
|
-
status: task.status,
|
|
221
|
-
result: vResult,
|
|
222
|
-
});
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await isolation.dispose?.();
|
|
223
252
|
}
|
|
224
253
|
return {
|
|
225
254
|
projectName: plan.projectName,
|
|
@@ -11,6 +11,10 @@ export interface VerificationResult {
|
|
|
11
11
|
codeShape?: CodeShapeReport;
|
|
12
12
|
/** Results of the configured `verifyPhases`, when any ran. */
|
|
13
13
|
phases?: PhaseResult[];
|
|
14
|
+
/** Which isolation actually ran the check. */
|
|
15
|
+
isolation?: 'none' | 'clean';
|
|
16
|
+
/** Notes the reader should see, such as a fallback or a pass that holds only locally. */
|
|
17
|
+
warnings?: string[];
|
|
14
18
|
}
|
|
15
19
|
export interface PhaseResult {
|
|
16
20
|
name: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Builds the minimal environment a verification command runs with.
|
|
2
|
+
const BASE = ['PATH', 'HOME', 'USERPROFILE', 'LANG', 'LC_ALL', 'TMPDIR', 'TEMP', 'TMP'];
|
|
3
|
+
const WINDOWS = ['SystemRoot', 'windir', 'ComSpec', 'PATHEXT', 'APPDATA', 'LOCALAPPDATA'];
|
|
4
|
+
const TOOLS = ['VIRTUAL_ENV', 'GOPATH', 'GOROOT', 'GOCACHE', 'GOMODCACHE'];
|
|
5
|
+
/** Allowlisted variables from the parent env, plus the extra names, plus the sandbox markers. */
|
|
6
|
+
export function buildVerifyEnv(extra = [], source = process.env) {
|
|
7
|
+
const names = new Set([...BASE, ...TOOLS, ...(process.platform === 'win32' ? WINDOWS : []), ...extra].map((n) => n.toLowerCase()));
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const [key, value] of Object.entries(source)) {
|
|
10
|
+
if (value === undefined || !names.has(key.toLowerCase()))
|
|
11
|
+
continue;
|
|
12
|
+
if (key.startsWith('NATIV_') || key === 'CI')
|
|
13
|
+
continue;
|
|
14
|
+
out[key] = value;
|
|
15
|
+
}
|
|
16
|
+
out.CI = 'true';
|
|
17
|
+
out.NATIV_VERIFY_SANDBOX = '1';
|
|
18
|
+
return out;
|
|
19
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
2
2
|
import { runInit } from './commands/init.js';
|
|
3
3
|
import { runStatus } from './commands/status.js';
|
|
4
4
|
import { runValidate } from './commands/validate.js';
|
|
@@ -19,7 +19,14 @@ import { runNotifyTest } from './commands/notify.js';
|
|
|
19
19
|
import { runLearnPropose, runLearnDecide, runLearnList } from './commands/learn.js';
|
|
20
20
|
import { runProfileCommand } from './commands/profile.js';
|
|
21
21
|
import { runDesignCheck } from './commands/design-check.js';
|
|
22
|
+
import { runCheckDocs } from './commands/check.js';
|
|
22
23
|
import { packageVersion } from './core/version.js';
|
|
24
|
+
function parseGitVisibility(value) {
|
|
25
|
+
if (value !== 'hidden' && value !== 'shared') {
|
|
26
|
+
throw new InvalidArgumentError('Use hidden or shared.');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
23
30
|
export function createProgram() {
|
|
24
31
|
const program = new Command();
|
|
25
32
|
program
|
|
@@ -31,6 +38,7 @@ export function createProgram() {
|
|
|
31
38
|
.description('Scaffold the workflow (.ai/, AGENTS.md, directives) and the Claude Code configuration in the target directory')
|
|
32
39
|
.option('-f, --force', 'Overwrite existing specification and directive files')
|
|
33
40
|
.option('--design <profile>', 'Design profile: nativ, taste-v1 or taste-v2')
|
|
41
|
+
.option('--git-visibility <mode>', 'Git visibility of generated files: hidden (default) or shared', parseGitVisibility)
|
|
34
42
|
.action(async (targetDir, options) => {
|
|
35
43
|
await runInit(targetDir, options);
|
|
36
44
|
});
|
|
@@ -40,10 +48,11 @@ export function createProgram() {
|
|
|
40
48
|
.option('--command <cli>', 'How Claude Code should start nativ, e.g. "npx -y @njeybe/nativ" (default: nativ when installed globally)')
|
|
41
49
|
.option('--enforcement <mode>', 'Role enforcement mode: warn (default), block or off')
|
|
42
50
|
.option('--dry-run', 'Show what would change without writing anything')
|
|
51
|
+
.option('--git-visibility <mode>', 'Git visibility of generated files: hidden (default) or shared', parseGitVisibility)
|
|
43
52
|
.option('-f, --force', 'Also overwrite agent and directive files that were edited by hand')
|
|
44
53
|
.option('--json', 'Output the result as JSON')
|
|
45
54
|
.action(async (targetDir, options) => {
|
|
46
|
-
await runSetup(targetDir, { command: options.command, enforcement: options.enforcement, dryRun: options.dryRun, force: options.force, json: options.json });
|
|
55
|
+
await runSetup(targetDir, { command: options.command, enforcement: options.enforcement, dryRun: options.dryRun, force: options.force, gitVisibility: options.gitVisibility, json: options.json });
|
|
47
56
|
});
|
|
48
57
|
program
|
|
49
58
|
.command('doctor [targetDir]')
|
|
@@ -59,8 +68,9 @@ export function createProgram() {
|
|
|
59
68
|
.command('update [targetDir]')
|
|
60
69
|
.description('Refresh directives, role guides and the Claude Code configuration to the latest templates. Files you edited are kept; contracts and the plan are never touched')
|
|
61
70
|
.option('-f, --force', 'Also replace directive and role-guide files that were edited by hand')
|
|
71
|
+
.option('--git-visibility <mode>', 'Git visibility of generated files: hidden (default) or shared', parseGitVisibility)
|
|
62
72
|
.action(async (targetDir, options) => {
|
|
63
|
-
await runUpdate(targetDir, { force: options.force });
|
|
73
|
+
await runUpdate(targetDir, { force: options.force, gitVisibility: options.gitVisibility });
|
|
64
74
|
});
|
|
65
75
|
program
|
|
66
76
|
.command('status [targetDir]')
|
|
@@ -95,6 +105,8 @@ export function createProgram() {
|
|
|
95
105
|
.option('-m, --milestone <id>', 'Run verification commands for all tasks in a specific milestone')
|
|
96
106
|
.option('--json', 'Output verification telemetry as JSON')
|
|
97
107
|
.option('--timeout <ms>', 'Execution timeout in milliseconds per command (default 120000)', parseInt)
|
|
108
|
+
.option('--isolation <mode>', 'Run checks in place (none) or in a clean copy (clean)')
|
|
109
|
+
.option('--clean', 'Short for --isolation clean')
|
|
98
110
|
.action(async (taskId, targetDir, options) => {
|
|
99
111
|
await runVerify(taskId, targetDir, options);
|
|
100
112
|
});
|
|
@@ -142,7 +154,11 @@ export function createProgram() {
|
|
|
142
154
|
.option('--deps <ids>', 'Comma-separated dependency task IDs')
|
|
143
155
|
.option('--spec-refs <refs>', 'Comma-separated spec anchors, e.g. ui_specs.md#appointment-list')
|
|
144
156
|
.option('--complexity <level>', 'Task complexity: simple, standard or complex')
|
|
145
|
-
.option('--
|
|
157
|
+
.option('--kind <kind>', 'Verification kind: unit, integration, static-config, docs, probe or manual')
|
|
158
|
+
.option('--notes <text>', 'Task notes (required with --kind manual)')
|
|
159
|
+
.option('--accept <text>', 'Acceptance ("done when") line; repeatable, each value kept whole', (value, prev = []) => [...prev, value])
|
|
160
|
+
.option('--new-milestone <name>', 'Create the next milestone with this name and add the task to it')
|
|
161
|
+
.option('--force', 'With --new-milestone, allow it while a planned milestone is still open')
|
|
146
162
|
.option('--json', 'Output the created task as JSON')
|
|
147
163
|
.action(async (title, targetDir, options) => {
|
|
148
164
|
await runTaskAdd(title, targetDir, options);
|
|
@@ -168,8 +184,11 @@ export function createProgram() {
|
|
|
168
184
|
.option('-n, --notes <notes>', 'Completion notes or summary')
|
|
169
185
|
.option('--no-verify', 'Skip automated verification command execution')
|
|
170
186
|
.option('--timeout <ms>', 'Verification execution timeout in milliseconds', parseInt)
|
|
187
|
+
.option('--isolation <mode>', 'Run the check in place (none) or in a clean copy (clean)')
|
|
188
|
+
.option('--clean', 'Short for --isolation clean')
|
|
171
189
|
.action(async (taskId, targetDir, options) => {
|
|
172
190
|
await runTaskComplete(taskId, targetDir, {
|
|
191
|
+
isolation: options.clean ? 'clean' : options.isolation,
|
|
173
192
|
notes: options.notes,
|
|
174
193
|
skipVerify: options.verify === false,
|
|
175
194
|
timeout: options.timeout,
|
|
@@ -182,6 +201,8 @@ export function createProgram() {
|
|
|
182
201
|
.option('-m, --milestone <id>', 'Run verification commands for all tasks in a specific milestone')
|
|
183
202
|
.option('--json', 'Output verification telemetry as JSON')
|
|
184
203
|
.option('--timeout <ms>', 'Execution timeout in milliseconds per command (default 120000)', parseInt)
|
|
204
|
+
.option('--isolation <mode>', 'Run checks in place (none) or in a clean copy (clean)')
|
|
205
|
+
.option('--clean', 'Short for --isolation clean')
|
|
185
206
|
.action(async (taskId, targetDir, options) => {
|
|
186
207
|
await runVerify(taskId, targetDir, options);
|
|
187
208
|
});
|
|
@@ -241,6 +262,14 @@ export function createProgram() {
|
|
|
241
262
|
.action(async (taskId, targetDir, options) => {
|
|
242
263
|
await runTaskUnlock(taskId, targetDir, { reason: options.reason, revoke: options.revoke });
|
|
243
264
|
});
|
|
265
|
+
const check = program.command('check').description('Checks on project files');
|
|
266
|
+
check
|
|
267
|
+
.command('docs <files...>')
|
|
268
|
+
.description('Check backticked paths, placeholders, settings and nativ flags in markdown files')
|
|
269
|
+
.option('--json', 'Output findings as JSON')
|
|
270
|
+
.action(async (files, options) => {
|
|
271
|
+
await runCheckDocs(files, options);
|
|
272
|
+
});
|
|
244
273
|
const design = program.command('design').description('Design profile tools');
|
|
245
274
|
design
|
|
246
275
|
.command('check [paths...]')
|
package/dist/mcp/server.js
CHANGED
|
@@ -67,8 +67,11 @@ export function createMcpServer(targetDirArg) {
|
|
|
67
67
|
specRefs: z.array(z.string()).optional().describe('Spec anchors, e.g. ui_specs.md#appointment-list'),
|
|
68
68
|
complexity: z.string().optional().describe('simple, standard or complex'),
|
|
69
69
|
acceptanceCriteria: z.array(z.string()).optional().describe('Plain-language "done when" lines'),
|
|
70
|
+
verificationKind: z.string().optional().describe('unit, integration, static-config, docs, probe or manual'),
|
|
71
|
+
notes: z.string().optional().describe('Task notes; required for a manual check'),
|
|
72
|
+
newMilestone: z.string().optional().describe('Create the next milestone with this name and add the task to it'),
|
|
70
73
|
},
|
|
71
|
-
}, ({ title, description, assignedSubagent, milestone, verificationCommand, targetFiles, dependencies, fastPath, specRefs, complexity, acceptanceCriteria }) => captureOutput(() => runTaskAdd(title, targetDir, {
|
|
74
|
+
}, ({ title, description, assignedSubagent, milestone, verificationCommand, targetFiles, dependencies, fastPath, specRefs, complexity, acceptanceCriteria, verificationKind, notes, newMilestone }) => captureOutput(() => runTaskAdd(title, targetDir, {
|
|
72
75
|
description,
|
|
73
76
|
agent: assignedSubagent,
|
|
74
77
|
milestone,
|
|
@@ -78,7 +81,10 @@ export function createMcpServer(targetDirArg) {
|
|
|
78
81
|
fastPath,
|
|
79
82
|
specRefs,
|
|
80
83
|
complexity,
|
|
84
|
+
kind: verificationKind,
|
|
85
|
+
notes,
|
|
81
86
|
accept: acceptanceCriteria,
|
|
87
|
+
newMilestone,
|
|
82
88
|
json: true,
|
|
83
89
|
})));
|
|
84
90
|
registerNativTool('task_start', {
|
|
@@ -107,8 +113,9 @@ export function createMcpServer(targetDirArg) {
|
|
|
107
113
|
taskId: z.string().optional().describe('Task ID to verify. Defaults to active in_progress task.'),
|
|
108
114
|
milestone: z.string().optional().describe('Filter by milestone ID or name'),
|
|
109
115
|
all: z.boolean().optional().describe('Run verification commands for all completed tasks to detect regressions'),
|
|
116
|
+
isolation: z.enum(['none', 'clean']).optional().describe('In place or clean copy; default is the profile'),
|
|
110
117
|
},
|
|
111
|
-
}, ({ taskId, milestone, all }) => captureOutput(() => runVerify(taskId, targetDir, { milestone, all, json: true })));
|
|
118
|
+
}, ({ taskId, milestone, all, isolation }) => captureOutput(() => runVerify(taskId, targetDir, { milestone, all, isolation, json: true })));
|
|
112
119
|
registerNativTool('task_block', {
|
|
113
120
|
description: 'Mark a task as blocked with a required reason (e.g. verification failed after 3 attempts).',
|
|
114
121
|
inputSchema: {
|
package/dist/scanner/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface MasterPlanTask {
|
|
|
40
40
|
/** Spec anchors, e.g. "ui_specs.md#appointment-list" or "api_contracts.json#/paths/~1appointments". */
|
|
41
41
|
specRefs?: string[];
|
|
42
42
|
complexity?: TaskComplexity;
|
|
43
|
+
/** What kind of check proves the task; absent means unspecified. */
|
|
44
|
+
verificationKind?: VerificationKind;
|
|
43
45
|
/** Plain-language "done when" lines. */
|
|
44
46
|
acceptanceCriteria?: string[];
|
|
45
47
|
/** Set by `task start` while in_progress: who claimed it (NATIV_AGENT_ID or --agent) and when. */
|
|
@@ -48,6 +50,8 @@ export interface MasterPlanTask {
|
|
|
48
50
|
}
|
|
49
51
|
export type TaskComplexity = 'simple' | 'standard' | 'complex';
|
|
50
52
|
export declare const TASK_COMPLEXITIES: readonly TaskComplexity[];
|
|
53
|
+
export type VerificationKind = 'unit' | 'integration' | 'static-config' | 'docs' | 'probe' | 'manual';
|
|
54
|
+
export declare const TASK_VERIFICATION_KINDS: readonly VerificationKind[];
|
|
51
55
|
/** Lists every structural problem with a task; empty when the task is valid. */
|
|
52
56
|
export declare function validateMasterPlanTask(task: unknown): string[];
|
|
53
57
|
export interface MasterPlanMilestone {
|
package/dist/scanner/types.js
CHANGED
|
@@ -17,6 +17,9 @@ export function isSubagentType(value) {
|
|
|
17
17
|
}
|
|
18
18
|
export const TASK_STATUSES = ['pending', 'in_progress', 'completed', 'blocked'];
|
|
19
19
|
export const TASK_COMPLEXITIES = ['simple', 'standard', 'complex'];
|
|
20
|
+
export const TASK_VERIFICATION_KINDS = [
|
|
21
|
+
'unit', 'integration', 'static-config', 'docs', 'probe', 'manual',
|
|
22
|
+
];
|
|
20
23
|
/** Lists every structural problem with a task; empty when the task is valid. */
|
|
21
24
|
export function validateMasterPlanTask(task) {
|
|
22
25
|
if (!task || typeof task !== 'object' || Array.isArray(task))
|
|
@@ -33,6 +36,9 @@ export function validateMasterPlanTask(task) {
|
|
|
33
36
|
errors.push(`${label}: "description" must be a string`);
|
|
34
37
|
if (typeof t.verificationCommand !== 'string')
|
|
35
38
|
errors.push(`${label}: "verificationCommand" must be a string`);
|
|
39
|
+
if (typeof t.verificationCommand === 'string' && /NATIV_(CI|HEADLESS)_OVERRIDE/.test(t.verificationCommand)) {
|
|
40
|
+
errors.push(`${label}: "verificationCommand" must not set NATIV_CI_OVERRIDE or NATIV_HEADLESS_OVERRIDE`);
|
|
41
|
+
}
|
|
36
42
|
if (typeof t.assignedSubagent !== 'string' || !isSubagentType(t.assignedSubagent)) {
|
|
37
43
|
errors.push(`${label}: "assignedSubagent" must be one of ${SUBAGENT_TYPES.join(', ')}`);
|
|
38
44
|
}
|
|
@@ -54,6 +60,9 @@ export function validateMasterPlanTask(task) {
|
|
|
54
60
|
if (t.complexity !== undefined && !TASK_COMPLEXITIES.includes(t.complexity)) {
|
|
55
61
|
errors.push(`${label}: "complexity" must be one of ${TASK_COMPLEXITIES.join(', ')}`);
|
|
56
62
|
}
|
|
63
|
+
if (t.verificationKind !== undefined && !TASK_VERIFICATION_KINDS.includes(t.verificationKind)) {
|
|
64
|
+
errors.push(`${label}: "verificationKind" must be one of ${TASK_VERIFICATION_KINDS.join(', ')}`);
|
|
65
|
+
}
|
|
57
66
|
return errors;
|
|
58
67
|
}
|
|
59
68
|
export const ESCALATION_TYPES = [
|
|
@@ -70,6 +70,7 @@ export declare function handlePipelineTasks(root: string): {
|
|
|
70
70
|
fastPath?: boolean;
|
|
71
71
|
specRefs?: string[];
|
|
72
72
|
complexity?: import("../../scanner/types.js").TaskComplexity;
|
|
73
|
+
verificationKind?: import("../../scanner/types.js").VerificationKind;
|
|
73
74
|
acceptanceCriteria?: string[];
|
|
74
75
|
claimedBy?: string;
|
|
75
76
|
claimedAt?: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nativ",
|
|
3
3
|
"displayName": "nativ",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.9.0",
|
|
5
5
|
"description": "Role-based multi-agent workflow for Claude Code: architect, worker, verifier and explorer agents, task-scope enforcement, and the nativ MCP server.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "JB Natividad"
|
|
@@ -13,14 +13,14 @@ Your job is design, not implementation. You turn a feature request or an escalat
|
|
|
13
13
|
|
|
14
14
|
## How you work
|
|
15
15
|
|
|
16
|
-
1. Orient: read `.ai/context.md` and `.ai/codebase_map.md` if it exists, then only the contract you are changing.
|
|
16
|
+
1. Orient: read `.ai/context.md` and `.ai/codebase_map.md` if it exists, then only the contract you are changing. Without a map, or with one well behind `git rev-parse --short HEAD`, ask the project manager to run the **explorer** first; you cannot start it. Save its report unchanged as `.ai/codebase_map.md` and design from it. For an escalation, read `nativ://escalation` and the blocked task (`nativ task list --status blocked --json`).
|
|
17
17
|
2. **Pick the design track first** and record it in `.ai/ui_specs.md` under Planning Level. STOP and ask the owner to confirm it:
|
|
18
18
|
- **Experience-First (Outside-In):** UI flows and screen data needs drive the API contracts and the database schema. Order: UI specs, then API contracts, then database schema.
|
|
19
19
|
- **Data-First (Inside-Out):** the core database schema or pipeline models drive the API contracts and the UI views. Order: database schema, then API contracts, then UI specs.
|
|
20
20
|
|
|
21
|
-
Recommend Experience-First for user-facing products, Data-First for data pipelines or an existing schema. Design in the chosen order
|
|
22
|
-
3. Present each step in plain language: what changes for the user, why, what stays safe, and two or three options
|
|
23
|
-
4. After approval, write the contract.
|
|
21
|
+
Recommend Experience-First for user-facing products, Data-First for data pipelines or an existing schema. Design in the chosen order, **stopping for the human's approval after each step**.
|
|
22
|
+
3. Present each step in plain language: what changes for the user, why, what stays safe, and two or three options, the best marked `(Recommended)`. No raw stack traces.
|
|
23
|
+
4. After approval, write the contract. Never route around the write prompt under `.ai/`; it is the approval gate.
|
|
24
24
|
5. Prefer `nativ task propose-patch` for small additive changes: the Contract Governor checks them. Any `[DROPPED]` or `[DESTRUCTIVE]` change needs the human's explicit yes first.
|
|
25
25
|
6. Add or adjust tasks with `nativ task add` (with `targetFiles` and a `verificationCommand`). Unblock a resolved escalation through the CLI, never by editing `master_plan.json`.
|
|
26
26
|
7. Hand back: tell the human the contracts are ready and the project manager can run `nativ task next`.
|
|
@@ -44,12 +44,19 @@ Use this for the UI step of the chosen track. Write the result into `.ai/ui_spec
|
|
|
44
44
|
## Task breakdown
|
|
45
45
|
|
|
46
46
|
- About one component or endpoint per task, at most 5 files.
|
|
47
|
-
-
|
|
47
|
+
- Shared UI first: a component needed by several views gets its own task, registered in the Shared Component Registry (`.ai/ui_specs.md`), before its dependents.
|
|
48
48
|
- `acceptanceCriteria` in plain words, including which states apply (loading, empty, error) and which shared components must be reused.
|
|
49
49
|
- `specRefs` point to exact sections, such as `ui_specs.md#appointment-list` or an `api_contracts.json` route.
|
|
50
50
|
- `complexity` is `simple`, `standard` or `complex`.
|
|
51
|
-
- `verificationCommand`
|
|
52
|
-
-
|
|
51
|
+
- `verificationCommand` is a targeted single-file test (e.g. `npx vitest run path/to/file.test.ts`). Full regression suites are for the Verifier.
|
|
52
|
+
- Each `verificationKind` needs a real check:
|
|
53
|
+
- `unit`: one test file for one function or component, under 5s.
|
|
54
|
+
- `integration`: one test file across a real module boundary, under 5s.
|
|
55
|
+
- `static-config`: parse the config or manifest and assert its keys and values.
|
|
56
|
+
- `docs`: `nativ check docs <files>` on the named docs.
|
|
57
|
+
- `probe`: call the live feature and assert the response.
|
|
58
|
+
- `manual`: a person checks it; the task needs `notes` saying what was checked.
|
|
59
|
+
- Dependencies are explicit: independent tasks have none between them.
|
|
53
60
|
|
|
54
61
|
```bash
|
|
55
62
|
nativ task add "Appointment list" --agent frontend \
|