@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +36 -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/pm-playbook.d.ts +1 -1
  32. package/dist/core/setup/pm-playbook.js +1 -1
  33. package/dist/core/setup/setup-planner.js +36 -3
  34. package/dist/core/setup/types.d.ts +6 -0
  35. package/dist/core/verifier/clean-tree.d.ts +14 -0
  36. package/dist/core/verifier/clean-tree.js +151 -0
  37. package/dist/core/verifier/executor.d.ts +3 -1
  38. package/dist/core/verifier/executor.js +54 -31
  39. package/dist/core/verifier/isolation.d.ts +25 -0
  40. package/dist/core/verifier/isolation.js +77 -0
  41. package/dist/core/verifier/portability-lint.d.ts +2 -0
  42. package/dist/core/verifier/portability-lint.js +64 -0
  43. package/dist/core/verifier/skip-policy.d.ts +8 -0
  44. package/dist/core/verifier/skip-policy.js +13 -0
  45. package/dist/core/verifier/task-verifier.d.ts +6 -8
  46. package/dist/core/verifier/task-verifier.js +68 -39
  47. package/dist/core/verifier/types.d.ts +4 -0
  48. package/dist/core/verifier/verify-env.d.ts +2 -0
  49. package/dist/core/verifier/verify-env.js +19 -0
  50. package/dist/index.js +33 -4
  51. package/dist/mcp/server.js +9 -2
  52. package/dist/scanner/types.d.ts +4 -0
  53. package/dist/scanner/types.js +9 -0
  54. package/dist/server/routes/task-routes.d.ts +1 -0
  55. package/dist/server/studio-ui/pages/benchmarks/script.js +9 -8
  56. package/dist/server/studio-ui/pages/benchmarks/styles.js +3 -1
  57. package/dist/server/studio-ui/pages/database/browser.js +1 -1
  58. package/dist/server/studio-ui/pages/database/connection.js +5 -4
  59. package/dist/server/studio-ui/pages/database/data-shell.js +8 -13
  60. package/dist/server/studio-ui/pages/database/drift.js +1 -1
  61. package/dist/server/studio-ui/pages/database/events.js +1 -2
  62. package/dist/server/studio-ui/pages/database/explorer.js +4 -4
  63. package/dist/server/studio-ui/pages/flow/layout.js +1 -1
  64. package/dist/server/studio-ui/pages/flow/styles.js +1 -1
  65. package/dist/server/studio-ui/pages/flow/timeline-view.js +1 -1
  66. package/dist/server/studio-ui/pages/home/script.js +26 -29
  67. package/dist/server/studio-ui/pages/home/styles.js +2 -6
  68. package/dist/server/studio-ui/pages/tasks/script.js +25 -40
  69. package/dist/server/studio-ui/pages/tasks/styles.js +9 -9
  70. package/dist/server/studio-ui/pages/team/script.js +27 -8
  71. package/dist/server/studio-ui/pages/team/styles.js +0 -1
  72. package/dist/server/studio-ui/shell/console.js +13 -7
  73. package/dist/server/studio-ui/shell/detail-view.js +3 -3
  74. package/dist/server/studio-ui/shell/dispatch.js +9 -8
  75. package/dist/server/studio-ui/shell/events-tools.js +11 -0
  76. package/dist/server/studio-ui/shell/format.js +31 -2
  77. package/dist/server/studio-ui/shell/markup-head.js +1 -1
  78. package/dist/server/studio-ui/shell/markup-tools.js +13 -8
  79. package/dist/server/studio-ui/shell/model.js +8 -2
  80. package/dist/server/studio-ui/shell/paint.js +1 -3
  81. package/dist/server/studio-ui/shell/pipeline-state.js +2 -11
  82. package/dist/server/studio-ui/shell/pulse.js +8 -7
  83. package/dist/server/studio-ui/shell/sidebar-view.js +2 -3
  84. package/dist/server/studio-ui/shell/triage.js +1 -1
  85. package/dist/server/studio-ui/styles/console.js +3 -0
  86. package/dist/server/studio-ui/styles/shell.js +4 -6
  87. package/package.json +1 -1
  88. package/plugin/.claude-plugin/plugin.json +1 -1
  89. package/plugin/agents/architect.md +15 -8
  90. package/plugin/agents/verifier.md +16 -11
  91. package/plugin/skills/nativ/SKILL.md +18 -15
  92. package/templates/AGENTS.md +18 -15
  93. package/templates/GEMINI.md +1 -1
  94. package/templates/claude-agents/architect.md +15 -8
  95. package/templates/claude-agents/verifier.md +16 -11
  96. package/templates/dot-ai/subagents/architect.md +2 -2
  97. package/templates/dot-ai/subagents/qa-tester.md +14 -13
@@ -0,0 +1,77 @@
1
+ import { loadVerifySettings } from '../profiles.js';
2
+ import { resolveMainRoot } from '../root-resolver.js';
3
+ import { executeVerification } from './executor.js';
4
+ import { createCleanTree, sweepStaleCleanTrees } from './clean-tree.js';
5
+ import { lintPortability } from './portability-lint.js';
6
+ import { buildVerifyEnv } from './verify-env.js';
7
+ import { loadVerifyPhases, runVerifyPhases } from './phases.js';
8
+ export const NO_ISOLATION = { mode: 'none', warnings: [] };
9
+ function isSkipped(command) {
10
+ const t = (command || '').trim().toLowerCase();
11
+ return !t || t === 'none' || t === 'skip';
12
+ }
13
+ export function needsRun(commands, configDir, sharedPhases) {
14
+ if (commands.some((c) => !isSkipped(c)))
15
+ return true;
16
+ return !sharedPhases && loadVerifyPhases(configDir).phases.length > 0;
17
+ }
18
+ export async function openIsolation(cwd, configDir, wanted) {
19
+ if (wanted !== 'clean')
20
+ return NO_ISOLATION;
21
+ await sweepStaleCleanTrees(cwd);
22
+ const settings = loadVerifySettings(resolveMainRoot(configDir));
23
+ const tree = await createCleanTree(cwd, { links: settings.verifyLinks });
24
+ if (!tree.ok) {
25
+ return { mode: 'none', warnings: [`Clean isolation unavailable (${tree.reason}); ran in place.`] };
26
+ }
27
+ const env = buildVerifyEnv(settings.verifyEnv);
28
+ return { mode: 'clean', dir: tree.dir, env, warnings: [], dispose: tree.dispose };
29
+ }
30
+ /** Like runVerifyPhases, but with an explicit environment for each phase. */
31
+ export async function runPhasesIn(dir, configDir, timeout, env) {
32
+ const config = loadVerifyPhases(configDir);
33
+ if (!env || config.error)
34
+ return runVerifyPhases(dir, configDir, timeout);
35
+ const reports = [];
36
+ for (const phase of config.phases) {
37
+ const res = await executeVerification(phase.run, dir, timeout, { env });
38
+ reports.push({ name: phase.name, command: res.command, success: res.success, durationMs: res.durationMs });
39
+ if (!res.success) {
40
+ const cause = res.error ? `: ${res.error.trim()}` : '';
41
+ return { reports, failure: { ...res, phases: reports, error: `Phase "${phase.name}" failed${cause}` } };
42
+ }
43
+ }
44
+ return { reports };
45
+ }
46
+ // The phases then the task command, in the plan's directory; code-shape is not part of this.
47
+ export async function runInPlan(task, o, plan) {
48
+ const configDir = o.configDir ?? o.cwd;
49
+ const dir = plan.dir ?? o.cwd;
50
+ const outcome = o.phases ?? (await runPhasesIn(dir, configDir, o.timeout, plan.env));
51
+ // In a batch the shared phase time is counted once, by the batch, not again for every task.
52
+ if (outcome.failure) {
53
+ return { ...outcome.failure, phases: outcome.reports, ...(o.phases ? { durationMs: 0 } : {}) };
54
+ }
55
+ const raw = await executeVerification(task.verificationCommand, dir, o.timeout, { env: plan.env });
56
+ if (outcome.reports.length) {
57
+ raw.phases = outcome.reports;
58
+ raw.durationMs += o.phases ? 0 : outcome.reports.reduce((sum, r) => sum + r.durationMs, 0);
59
+ raw.skipped = false;
60
+ }
61
+ return raw;
62
+ }
63
+ // A clean failure is re-run in place only to learn whether the clean copy is the cause.
64
+ export async function diagnose(task, o, failed, failure) {
65
+ if (failed.error?.startsWith('Command timed out'))
66
+ return failed;
67
+ const local = await runInPlan(task, { ...o, phases: undefined }, NO_ISOLATION);
68
+ if (!local.success)
69
+ return failed;
70
+ const base = 'Passes only on this machine: it failed in a clean copy of the project.';
71
+ const note = [base, ...lintPortability(o.cwd, task.targetFiles || [])].join(' ');
72
+ if (failure === 'warn') {
73
+ return { ...local, isolation: 'clean', warnings: [...(failed.warnings ?? []), note] };
74
+ }
75
+ const error = `${failed.error ?? 'Verification failed'}. ${note} Isolation failure is set to block.`;
76
+ return { ...failed, error, warnings: [...(failed.warnings ?? []), note] };
77
+ }
@@ -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;