@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +26 -1
  3. package/dist/commands/check.d.ts +3 -0
  4. package/dist/commands/check.js +36 -0
  5. package/dist/commands/doctor.js +7 -1
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +1 -1
  8. package/dist/commands/setup.d.ts +1 -0
  9. package/dist/commands/setup.js +1 -0
  10. package/dist/commands/task/task-add.js +49 -6
  11. package/dist/commands/task/task-common.d.ts +6 -0
  12. package/dist/commands/task/task-lifecycle.d.ts +3 -1
  13. package/dist/commands/task/task-lifecycle.js +40 -2
  14. package/dist/commands/update.d.ts +1 -0
  15. package/dist/commands/update.js +1 -1
  16. package/dist/commands/verify.d.ts +7 -0
  17. package/dist/commands/verify.js +26 -2
  18. package/dist/core/checks/docs-check.d.ts +18 -0
  19. package/dist/core/checks/docs-check.js +186 -0
  20. package/dist/core/code-shape.d.ts +7 -2
  21. package/dist/core/code-shape.js +46 -16
  22. package/dist/core/env-reads.d.ts +15 -0
  23. package/dist/core/env-reads.js +126 -0
  24. package/dist/core/human-gate.js +6 -3
  25. package/dist/core/profiles.d.ts +8 -0
  26. package/dist/core/profiles.js +43 -2
  27. package/dist/core/root-resolver.d.ts +4 -0
  28. package/dist/core/root-resolver.js +32 -0
  29. package/dist/core/setup/gitignore-block.d.ts +17 -0
  30. package/dist/core/setup/gitignore-block.js +182 -0
  31. package/dist/core/setup/setup-planner.js +36 -3
  32. package/dist/core/setup/types.d.ts +6 -0
  33. package/dist/core/verifier/clean-tree.d.ts +14 -0
  34. package/dist/core/verifier/clean-tree.js +151 -0
  35. package/dist/core/verifier/executor.d.ts +3 -1
  36. package/dist/core/verifier/executor.js +54 -31
  37. package/dist/core/verifier/isolation.d.ts +25 -0
  38. package/dist/core/verifier/isolation.js +77 -0
  39. package/dist/core/verifier/portability-lint.d.ts +2 -0
  40. package/dist/core/verifier/portability-lint.js +64 -0
  41. package/dist/core/verifier/skip-policy.d.ts +8 -0
  42. package/dist/core/verifier/skip-policy.js +13 -0
  43. package/dist/core/verifier/task-verifier.d.ts +6 -8
  44. package/dist/core/verifier/task-verifier.js +68 -39
  45. package/dist/core/verifier/types.d.ts +4 -0
  46. package/dist/core/verifier/verify-env.d.ts +2 -0
  47. package/dist/core/verifier/verify-env.js +19 -0
  48. package/dist/index.js +33 -4
  49. package/dist/mcp/server.js +9 -2
  50. package/dist/scanner/types.d.ts +4 -0
  51. package/dist/scanner/types.js +9 -0
  52. package/dist/server/routes/task-routes.d.ts +1 -0
  53. package/package.json +1 -1
  54. package/plugin/.claude-plugin/plugin.json +1 -1
  55. package/plugin/agents/architect.md +14 -7
  56. package/plugin/agents/verifier.md +16 -14
  57. package/plugin/skills/nativ/SKILL.md +11 -8
  58. package/templates/AGENTS.md +11 -8
  59. package/templates/claude-agents/architect.md +14 -7
  60. package/templates/claude-agents/verifier.md +16 -14
  61. package/templates/dot-ai/subagents/qa-tester.md +14 -13
@@ -0,0 +1,2 @@
1
+ export declare function isPortabilityTarget(file: string): boolean;
2
+ export declare function lintPortability(cwd: string, targetFiles: string[]): string[];
@@ -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 { BatchVerificationResult, PhasesOutcome, TaskVerificationResult, VerificationResult } from './types.js';
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, then the code-shape check.
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 { executeVerification } from './executor.js';
5
- import { runVerifyPhases } from './phases.js';
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.mode === 'off')
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
- if (report.mode === 'block' && report.issues.length > 0) {
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: ${report.issues.length} issue(s) (codeStyle.mode is "block")`;
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, then the code-shape check.
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 outcome = options.phases ?? (await runVerifyPhases(options.cwd, configDir, options.timeout));
48
- // In a batch the shared phase time is counted once, by the batch, not again for every task.
49
- if (outcome.failure) {
50
- return { ...outcome.failure, phases: outcome.reports, ...(options.phases ? { durationMs: 0 } : {}) };
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
- const raw = await executeVerification(task.verificationCommand, options.cwd, options.timeout);
53
- if (outcome.reports.length) {
54
- raw.phases = outcome.reports;
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.id === options.milestoneId || m.name.toLowerCase() === options.milestoneId.toLowerCase())) {
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 in the batch.
201
- const phases = candidates.length ? await runVerifyPhases(targetDir, targetDir, options.timeout) : { reports: [] };
202
- totalDuration += phases.reports.reduce((sum, r) => sum + r.durationMs, 0);
203
- for (const { task, milestone } of candidates) {
204
- const vResult = await runTaskVerification(task, { cwd: targetDir, timeout: options.timeout, phases });
205
- totalDuration += vResult.durationMs;
206
- if (vResult.skipped) {
207
- skipped++;
208
- }
209
- else if (vResult.success) {
210
- passed++;
211
- }
212
- else {
213
- failed++;
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
- results.push({
216
- taskId: task.id,
217
- title: task.title,
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,2 @@
1
+ /** Allowlisted variables from the parent env, plus the extra names, plus the sandbox markers. */
2
+ export declare function buildVerifyEnv(extra?: string[], source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
@@ -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('--accept <text>', 'Acceptance ("done when") line; repeatable or "|"-separated', (value, prev = []) => [...prev, value])
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...]')
@@ -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: {
@@ -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 {
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@njeybe/nativ",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Role-based multi-agent workflow harness: Claude Code by default, other AI agents pluggable",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nativ",
3
3
  "displayName": "nativ",
4
- "version": "2.8.0",
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. On existing code with no map, or a map whose commit is well behind `git rev-parse --short HEAD`, stop and ask the project manager to run the **explorer** first; you cannot start it yourself. Save its report unchanged as `.ai/codebase_map.md` (Claude Code asks the human first) and design from it. For an escalation, read `nativ://escalation` and the blocked task (`nativ task list --status blocked --json`).
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 and **stop for the human's explicit approval after each step**. Do not start the next until the previous is approved.
22
- 3. Present each step in plain language: what changes for the user, why, what stays safe, and two or three options with the best marked `(Recommended)`, plus one line on when the other option would be the better choice. No raw stack traces.
23
- 4. After approval, write the contract. Claude Code will ask the human to confirm every write under `.ai/`; that prompt is the approval gate, so never try to route around it.
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
- - Reusable components first: If a UI element or primitive is needed across multiple views, create a dedicated foundational component task first and register it under the Shared Component Registry in `.ai/ui_specs.md` before dependent screen tasks.
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` must be a targeted single-file test (e.g. `npx vitest run path/to/file.test.ts`), taking < 5s. Full regression suites are for the Verifier.
52
- - Dependencies make parallel-safe work explicit: tasks that touch different files and need no result from each other have none between them.
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 \