@njeybe/nativ 2.8.0 → 2.9.1

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 (82) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +62 -6
  3. package/bin/cli.js +1 -1
  4. package/dist/commands/check.d.ts +3 -0
  5. package/dist/commands/check.js +36 -0
  6. package/dist/commands/doctor.js +7 -1
  7. package/dist/commands/init.d.ts +1 -0
  8. package/dist/commands/init.js +1 -1
  9. package/dist/commands/milestone.d.ts +10 -0
  10. package/dist/commands/milestone.js +84 -0
  11. package/dist/commands/setup.d.ts +1 -0
  12. package/dist/commands/setup.js +1 -0
  13. package/dist/commands/status.d.ts +1 -0
  14. package/dist/commands/status.js +39 -8
  15. package/dist/commands/task/task-add.js +62 -9
  16. package/dist/commands/task/task-common.d.ts +6 -0
  17. package/dist/commands/task/task-lifecycle.d.ts +3 -1
  18. package/dist/commands/task/task-lifecycle.js +48 -4
  19. package/dist/commands/task/task-query.js +4 -2
  20. package/dist/commands/task/task-show.d.ts +9 -0
  21. package/dist/commands/task/task-show.js +74 -0
  22. package/dist/commands/update.d.ts +1 -0
  23. package/dist/commands/update.js +1 -1
  24. package/dist/commands/validate.js +24 -0
  25. package/dist/commands/verify.d.ts +7 -0
  26. package/dist/commands/verify.js +26 -2
  27. package/dist/core/checks/docs-check.d.ts +18 -0
  28. package/dist/core/checks/docs-check.js +186 -0
  29. package/dist/core/code-shape.d.ts +7 -2
  30. package/dist/core/code-shape.js +46 -16
  31. package/dist/core/env-reads.d.ts +15 -0
  32. package/dist/core/env-reads.js +126 -0
  33. package/dist/core/human-gate.js +6 -3
  34. package/dist/core/milestone-archive.d.ts +40 -0
  35. package/dist/core/milestone-archive.js +299 -0
  36. package/dist/core/profiles.d.ts +8 -0
  37. package/dist/core/profiles.js +43 -2
  38. package/dist/core/root-resolver.d.ts +4 -0
  39. package/dist/core/root-resolver.js +32 -0
  40. package/dist/core/setup/gitignore-block.d.ts +17 -0
  41. package/dist/core/setup/gitignore-block.js +182 -0
  42. package/dist/core/setup/pm-playbook.d.ts +1 -1
  43. package/dist/core/setup/pm-playbook.js +7 -1
  44. package/dist/core/setup/session-context.d.ts +9 -1
  45. package/dist/core/setup/session-context.js +24 -2
  46. package/dist/core/setup/setup-planner.js +36 -3
  47. package/dist/core/setup/types.d.ts +6 -0
  48. package/dist/core/verifier/clean-tree.d.ts +14 -0
  49. package/dist/core/verifier/clean-tree.js +151 -0
  50. package/dist/core/verifier/executor.d.ts +3 -1
  51. package/dist/core/verifier/executor.js +54 -31
  52. package/dist/core/verifier/isolation.d.ts +25 -0
  53. package/dist/core/verifier/isolation.js +77 -0
  54. package/dist/core/verifier/portability-lint.d.ts +2 -0
  55. package/dist/core/verifier/portability-lint.js +64 -0
  56. package/dist/core/verifier/skip-policy.d.ts +8 -0
  57. package/dist/core/verifier/skip-policy.js +13 -0
  58. package/dist/core/verifier/task-verifier.d.ts +6 -8
  59. package/dist/core/verifier/task-verifier.js +95 -41
  60. package/dist/core/verifier/types.d.ts +4 -0
  61. package/dist/core/verifier/verify-env.d.ts +2 -0
  62. package/dist/core/verifier/verify-env.js +22 -0
  63. package/dist/index.js +47 -7
  64. package/dist/mcp/server.js +27 -2
  65. package/dist/runner/agent-supervisor.js +4 -1
  66. package/dist/scanner/types.d.ts +18 -0
  67. package/dist/scanner/types.js +11 -0
  68. package/dist/server/routes/task-routes.d.ts +2 -0
  69. package/dist/server/routes/task-routes.js +13 -5
  70. package/dist/server/studio-ui/shell/model.js +16 -9
  71. package/package.json +1 -1
  72. package/plugin/.claude-plugin/plugin.json +1 -1
  73. package/plugin/agents/architect.md +14 -7
  74. package/plugin/agents/verifier.md +16 -14
  75. package/plugin/agents/worker.md +3 -3
  76. package/plugin/hooks/hooks.json +1 -1
  77. package/plugin/skills/nativ/SKILL.md +13 -9
  78. package/templates/AGENTS.md +13 -9
  79. package/templates/claude-agents/architect.md +14 -7
  80. package/templates/claude-agents/verifier.md +16 -14
  81. package/templates/claude-agents/worker.md +3 -3
  82. package/templates/dot-ai/subagents/qa-tester.md +14 -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 } : {}) };
51
- }
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;
57
- }
58
- return applyCodeShape(raw, options.cwd, task);
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);
69
+ }
70
+ finally {
71
+ if (owned)
72
+ await plan.dispose?.();
73
+ }
59
74
  }
60
75
  /**
61
76
  * Run verification for a single specific task in the plan.
@@ -91,6 +106,15 @@ export async function verifyTask(taskId, targetDirArg, options = {}) {
91
106
  break;
92
107
  }
93
108
  }
109
+ const archivedIn = plan.milestones.find((m) => m.archived?.taskIds.includes(taskId));
110
+ if (!foundTask && archivedIn) {
111
+ const error = archivedMessage(taskId, archivedIn.id);
112
+ return {
113
+ taskId, title: 'Archived', milestoneId: archivedIn.id, milestoneName: archivedIn.name,
114
+ status: 'archived',
115
+ result: { success: false, exitCode: 1, command: '', stdout: '', stderr: error, durationMs: 0, error },
116
+ };
117
+ }
94
118
  if (!foundTask || !foundMilestone) {
95
119
  return {
96
120
  taskId,
@@ -119,6 +143,13 @@ export async function verifyTask(taskId, targetDirArg, options = {}) {
119
143
  result,
120
144
  };
121
145
  }
146
+ function archivedMessage(what, milestoneId) {
147
+ return `${what} is archived (milestone ${milestoneId}). `
148
+ + `Run \`nativ milestone restore ${milestoneId}\` to bring its tasks back first.`;
149
+ }
150
+ function matchesMilestone(m, wanted) {
151
+ return m.id === wanted || m.name.toLowerCase() === wanted.toLowerCase();
152
+ }
122
153
  /**
123
154
  * Verify a batch of tasks (by milestone, all completed tasks, or the active in-progress task).
124
155
  */
@@ -155,12 +186,19 @@ export async function verifyBatch(targetDirArg, options = {}) {
155
186
  }
156
187
  // Filter tasks to run
157
188
  const candidates = [];
189
+ const archivedWanted = options.milestoneId
190
+ ? plan.milestones.find((m) => m.archived && matchesMilestone(m, options.milestoneId))
191
+ : undefined;
192
+ if (archivedWanted) {
193
+ return refusedBatch(plan.projectName, archivedWanted, archivedMessage(`Milestone ${archivedWanted.id}`, archivedWanted.id));
194
+ }
195
+ // verify --all leaves archived milestones out: their stubs hold no tasks.
158
196
  for (const m of plan.milestones) {
159
197
  for (const t of m.tasks) {
160
198
  if (options.taskId && t.id === options.taskId) {
161
199
  candidates.push({ task: t, milestone: m });
162
200
  }
163
- else if (options.milestoneId && (m.id === options.milestoneId || m.name.toLowerCase() === options.milestoneId.toLowerCase())) {
201
+ else if (options.milestoneId && matchesMilestone(m, options.milestoneId)) {
164
202
  candidates.push({ task: t, milestone: m });
165
203
  }
166
204
  else if (options.all) {
@@ -197,29 +235,40 @@ export async function verifyBatch(targetDirArg, options = {}) {
197
235
  let failed = 0;
198
236
  let skipped = 0;
199
237
  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++;
238
+ // The phases check the whole project, so one run (and one clean tree) serves every task.
239
+ const wanted = options.isolation
240
+ ?? resolveActiveProfileSettings(resolveMainRoot(targetDir)).verifyIsolation;
241
+ const commands = candidates.map((c) => c.task.verificationCommand);
242
+ const isolation = candidates.length && needsRun(commands, targetDir, false)
243
+ ? await openIsolation(targetDir, targetDir, wanted)
244
+ : NO_ISOLATION;
245
+ try {
246
+ const phases = candidates.length
247
+ ? await runPhasesIn(isolation.dir ?? targetDir, targetDir, options.timeout, isolation.env)
248
+ : { reports: [] };
249
+ totalDuration += phases.reports.reduce((sum, r) => sum + r.durationMs, 0);
250
+ for (const { task, milestone } of candidates) {
251
+ const runOpts = { cwd: targetDir, timeout: options.timeout, phases, plan: isolation, isolation: wanted };
252
+ const vResult = await runTaskVerification(task, runOpts);
253
+ totalDuration += vResult.durationMs;
254
+ if (vResult.skipped)
255
+ skipped++;
256
+ else if (vResult.success)
257
+ passed++;
258
+ else
259
+ failed++;
260
+ results.push({
261
+ taskId: task.id,
262
+ title: task.title,
263
+ milestoneId: milestone.id,
264
+ milestoneName: milestone.name,
265
+ status: task.status,
266
+ result: vResult,
267
+ });
214
268
  }
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
- });
269
+ }
270
+ finally {
271
+ await isolation.dispose?.();
223
272
  }
224
273
  return {
225
274
  projectName: plan.projectName,
@@ -231,3 +280,8 @@ export async function verifyBatch(targetDirArg, options = {}) {
231
280
  tasks: results,
232
281
  };
233
282
  }
283
+ function refusedBatch(projectName, m, error) {
284
+ const result = { success: false, exitCode: 1, command: '', stdout: '', stderr: error, durationMs: 0, error };
285
+ const task = { taskId: m.id, title: 'Archived milestone', milestoneId: m.id, milestoneName: m.name, status: 'archived', result };
286
+ return { projectName, total: 1, passed: 0, failed: 1, skipped: 0, durationMs: 0, tasks: [task] };
287
+ }
@@ -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,22 @@
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 = [
4
+ 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT', 'APPDATA', 'LOCALAPPDATA',
5
+ 'ProgramFiles', 'ProgramFiles(x86)',
6
+ ];
7
+ const TOOLS = ['VIRTUAL_ENV', 'GOPATH', 'GOROOT', 'GOCACHE', 'GOMODCACHE'];
8
+ /** Allowlisted variables from the parent env, plus the extra names, plus the sandbox markers. */
9
+ export function buildVerifyEnv(extra = [], source = process.env) {
10
+ const names = new Set([...BASE, ...TOOLS, ...(process.platform === 'win32' ? WINDOWS : []), ...extra].map((n) => n.toLowerCase()));
11
+ const out = {};
12
+ for (const [key, value] of Object.entries(source)) {
13
+ if (value === undefined || !names.has(key.toLowerCase()))
14
+ continue;
15
+ if (key.startsWith('NATIV_') || key === 'CI')
16
+ continue;
17
+ out[key] = value;
18
+ }
19
+ out.CI = 'true';
20
+ out.NATIV_VERIFY_SANDBOX = '1';
21
+ return out;
22
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
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';
5
5
  import { runTaskList, runTaskNext, runTaskStart, runTaskComplete, runTaskBlock, runTaskReclaim, runTaskEscalate, runTaskAdd, runTaskProposePatch, runTaskUnlock, } from './commands/task.js';
6
+ import { runTaskShow } from './commands/task/task-show.js';
6
7
  import { runHookCheck, runHookStatus } from './commands/hook.js';
7
8
  import { runSetup } from './commands/setup.js';
8
9
  import { runDoctor } from './commands/doctor.js';
@@ -19,7 +20,15 @@ import { runNotifyTest } from './commands/notify.js';
19
20
  import { runLearnPropose, runLearnDecide, runLearnList } from './commands/learn.js';
20
21
  import { runProfileCommand } from './commands/profile.js';
21
22
  import { runDesignCheck } from './commands/design-check.js';
23
+ import { runCheckDocs } from './commands/check.js';
24
+ import { registerMilestoneCommands } from './commands/milestone.js';
22
25
  import { packageVersion } from './core/version.js';
26
+ function parseGitVisibility(value) {
27
+ if (value !== 'hidden' && value !== 'shared') {
28
+ throw new InvalidArgumentError('Use hidden or shared.');
29
+ }
30
+ return value;
31
+ }
23
32
  export function createProgram() {
24
33
  const program = new Command();
25
34
  program
@@ -31,6 +40,7 @@ export function createProgram() {
31
40
  .description('Scaffold the workflow (.ai/, AGENTS.md, directives) and the Claude Code configuration in the target directory')
32
41
  .option('-f, --force', 'Overwrite existing specification and directive files')
33
42
  .option('--design <profile>', 'Design profile: nativ, taste-v1 or taste-v2')
43
+ .option('--git-visibility <mode>', 'Git visibility of generated files: hidden (default) or shared', parseGitVisibility)
34
44
  .action(async (targetDir, options) => {
35
45
  await runInit(targetDir, options);
36
46
  });
@@ -40,10 +50,11 @@ export function createProgram() {
40
50
  .option('--command <cli>', 'How Claude Code should start nativ, e.g. "npx -y @njeybe/nativ" (default: nativ when installed globally)')
41
51
  .option('--enforcement <mode>', 'Role enforcement mode: warn (default), block or off')
42
52
  .option('--dry-run', 'Show what would change without writing anything')
53
+ .option('--git-visibility <mode>', 'Git visibility of generated files: hidden (default) or shared', parseGitVisibility)
43
54
  .option('-f, --force', 'Also overwrite agent and directive files that were edited by hand')
44
55
  .option('--json', 'Output the result as JSON')
45
56
  .action(async (targetDir, options) => {
46
- await runSetup(targetDir, { command: options.command, enforcement: options.enforcement, dryRun: options.dryRun, force: options.force, json: options.json });
57
+ await runSetup(targetDir, { command: options.command, enforcement: options.enforcement, dryRun: options.dryRun, force: options.force, gitVisibility: options.gitVisibility, json: options.json });
47
58
  });
48
59
  program
49
60
  .command('doctor [targetDir]')
@@ -59,14 +70,16 @@ export function createProgram() {
59
70
  .command('update [targetDir]')
60
71
  .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
72
  .option('-f, --force', 'Also replace directive and role-guide files that were edited by hand')
73
+ .option('--git-visibility <mode>', 'Git visibility of generated files: hidden (default) or shared', parseGitVisibility)
62
74
  .action(async (targetDir, options) => {
63
- await runUpdate(targetDir, { force: options.force });
75
+ await runUpdate(targetDir, { force: options.force, gitVisibility: options.gitVisibility });
64
76
  });
65
77
  program
66
78
  .command('status [targetDir]')
67
79
  .description('Inspect current execution progress and telemetry from .ai/master_plan.json')
68
80
  .option('-t, --telemetry', 'Display token usage, duration, and cost estimation telemetry')
69
- .option('--json', 'Output project status and telemetry as JSON')
81
+ .option('--json', 'Output project status and telemetry as JSON (compact)')
82
+ .option('--full', 'With --json, include every milestone task and per-task telemetry')
70
83
  .action(async (targetDir, options) => {
71
84
  await runStatus(targetDir, options);
72
85
  });
@@ -86,7 +99,7 @@ export function createProgram() {
86
99
  .option('--json', 'Output filtered tasks as JSON (a summary per task)')
87
100
  .option('--full', 'With --json, include every task field')
88
101
  .action(async (targetDir, options) => {
89
- await runTaskList(targetDir, options);
102
+ await runTaskList(targetDir, { ...options, brief: !options.full });
90
103
  });
91
104
  program
92
105
  .command('verify [taskId] [targetDir]')
@@ -95,6 +108,8 @@ export function createProgram() {
95
108
  .option('-m, --milestone <id>', 'Run verification commands for all tasks in a specific milestone')
96
109
  .option('--json', 'Output verification telemetry as JSON')
97
110
  .option('--timeout <ms>', 'Execution timeout in milliseconds per command (default 120000)', parseInt)
111
+ .option('--isolation <mode>', 'Run checks in place (none) or in a clean copy (clean)')
112
+ .option('--clean', 'Short for --isolation clean')
98
113
  .action(async (taskId, targetDir, options) => {
99
114
  await runVerify(taskId, targetDir, options);
100
115
  });
@@ -128,7 +143,7 @@ export function createProgram() {
128
143
  .option('--json', 'Output filtered tasks as JSON (a summary per task)')
129
144
  .option('--full', 'With --json, include every task field')
130
145
  .action(async (targetDir, options) => {
131
- await runTaskList(targetDir, options);
146
+ await runTaskList(targetDir, { ...options, brief: !options.full });
132
147
  });
133
148
  task
134
149
  .command('add <title> [targetDir]')
@@ -142,7 +157,11 @@ export function createProgram() {
142
157
  .option('--deps <ids>', 'Comma-separated dependency task IDs')
143
158
  .option('--spec-refs <refs>', 'Comma-separated spec anchors, e.g. ui_specs.md#appointment-list')
144
159
  .option('--complexity <level>', 'Task complexity: simple, standard or complex')
145
- .option('--accept <text>', 'Acceptance ("done when") line; repeatable or "|"-separated', (value, prev = []) => [...prev, value])
160
+ .option('--kind <kind>', 'Verification kind: unit, integration, static-config, docs, probe or manual')
161
+ .option('--notes <text>', 'Task notes (required with --kind manual)')
162
+ .option('--accept <text>', 'Acceptance ("done when") line; repeatable, each value kept whole', (value, prev = []) => [...prev, value])
163
+ .option('--new-milestone <name>', 'Create the next milestone with this name and add the task to it')
164
+ .option('--force', 'With --new-milestone, allow it while a planned milestone is still open')
146
165
  .option('--json', 'Output the created task as JSON')
147
166
  .action(async (title, targetDir, options) => {
148
167
  await runTaskAdd(title, targetDir, options);
@@ -154,6 +173,13 @@ export function createProgram() {
154
173
  .action(async (targetDir, options) => {
155
174
  await runTaskNext(targetDir, options);
156
175
  });
176
+ task
177
+ .command('show <taskId> [targetDir]')
178
+ .description('Show one task in full, from the plan or from its archive file')
179
+ .option('--json', 'Output the task as JSON')
180
+ .action(async (taskId, targetDir, options) => {
181
+ await runTaskShow(taskId, targetDir, options);
182
+ });
157
183
  task
158
184
  .command('start <taskId> [targetDir]')
159
185
  .description('Claim a pending task whose dependencies are done and mark it in_progress')
@@ -168,8 +194,11 @@ export function createProgram() {
168
194
  .option('-n, --notes <notes>', 'Completion notes or summary')
169
195
  .option('--no-verify', 'Skip automated verification command execution')
170
196
  .option('--timeout <ms>', 'Verification execution timeout in milliseconds', parseInt)
197
+ .option('--isolation <mode>', 'Run the check in place (none) or in a clean copy (clean)')
198
+ .option('--clean', 'Short for --isolation clean')
171
199
  .action(async (taskId, targetDir, options) => {
172
200
  await runTaskComplete(taskId, targetDir, {
201
+ isolation: options.clean ? 'clean' : options.isolation,
173
202
  notes: options.notes,
174
203
  skipVerify: options.verify === false,
175
204
  timeout: options.timeout,
@@ -182,6 +211,8 @@ export function createProgram() {
182
211
  .option('-m, --milestone <id>', 'Run verification commands for all tasks in a specific milestone')
183
212
  .option('--json', 'Output verification telemetry as JSON')
184
213
  .option('--timeout <ms>', 'Execution timeout in milliseconds per command (default 120000)', parseInt)
214
+ .option('--isolation <mode>', 'Run checks in place (none) or in a clean copy (clean)')
215
+ .option('--clean', 'Short for --isolation clean')
185
216
  .action(async (taskId, targetDir, options) => {
186
217
  await runVerify(taskId, targetDir, options);
187
218
  });
@@ -241,6 +272,15 @@ export function createProgram() {
241
272
  .action(async (taskId, targetDir, options) => {
242
273
  await runTaskUnlock(taskId, targetDir, { reason: options.reason, revoke: options.revoke });
243
274
  });
275
+ registerMilestoneCommands(program);
276
+ const check = program.command('check').description('Checks on project files');
277
+ check
278
+ .command('docs <files...>')
279
+ .description('Check backticked paths, placeholders, settings and nativ flags in markdown files')
280
+ .option('--json', 'Output findings as JSON')
281
+ .action(async (files, options) => {
282
+ await runCheckDocs(files, options);
283
+ });
244
284
  const design = program.command('design').description('Design profile tools');
245
285
  design
246
286
  .command('check [paths...]')
@@ -10,6 +10,8 @@ import { runTriage } from '../commands/triage.js';
10
10
  import { runStatus } from '../commands/status.js';
11
11
  import { runDoctor } from '../commands/doctor.js';
12
12
  import { runValidate } from '../commands/validate.js';
13
+ import { runMilestoneArchive } from '../commands/milestone.js';
14
+ import { runTaskShow } from '../commands/task/task-show.js';
13
15
  import { runWorktreeCreate, runWorktreeList, runWorktreeMerge, runWorktreeRemove, } from '../commands/worktree.js';
14
16
  import { runDbStatus, runDbInspect, runDbDiff } from '../commands/db.js';
15
17
  import { runVerify } from '../commands/verify.js';
@@ -52,6 +54,22 @@ export function createMcpServer(targetDirArg) {
52
54
  full: z.boolean().optional().describe('Return every task field; the default is a short summary per task'),
53
55
  },
54
56
  }, ({ available, status, milestone, fastPath, full }) => captureOutput(() => runTaskList(targetDir, { available, status, milestone, fastPath, json: true, brief: !full })));
57
+ registerNativTool('task_show', {
58
+ description: 'Print one task by id, from the plan or from its milestone archive (JSON). Use it for old or archived tasks.',
59
+ inputSchema: {
60
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
61
+ },
62
+ }, ({ taskId }) => captureOutput(() => runTaskShow(taskId, targetDir, { json: true })));
63
+ registerNativTool('milestone_archive', {
64
+ description: 'Archive finished milestones (comma-separated ids) after their regression suite passes. Needs a summary or summaries file.',
65
+ inputSchema: {
66
+ ids: z.string().min(1).describe('Milestone ID, or comma-separated IDs'),
67
+ summary: z.string().optional().describe('One-line summary (required for one milestone)'),
68
+ summaries: z.string().optional().describe('Path to a JSON file of milestone ID to summary'),
69
+ check: z.string().optional().describe('Regression command; saved as regressionCommand'),
70
+ timeout: z.number().int().optional().describe('Suite timeout in ms (default 10 minutes)'),
71
+ },
72
+ }, ({ ids, summary, summaries, check, timeout }) => captureOutput(() => runMilestoneArchive(ids, targetDir, { summary, summaries, check, timeout })));
55
73
  registerNativTool('task_add', {
56
74
  description: 'Append a task to .ai/master_plan.json with an auto-incremented ID (JSON). Goes to the given milestone, else the active one; ' +
57
75
  'fastPath routes it to the fast-path milestone (created on first use).',
@@ -67,8 +85,11 @@ export function createMcpServer(targetDirArg) {
67
85
  specRefs: z.array(z.string()).optional().describe('Spec anchors, e.g. ui_specs.md#appointment-list'),
68
86
  complexity: z.string().optional().describe('simple, standard or complex'),
69
87
  acceptanceCriteria: z.array(z.string()).optional().describe('Plain-language "done when" lines'),
88
+ verificationKind: z.string().optional().describe('unit, integration, static-config, docs, probe or manual'),
89
+ notes: z.string().optional().describe('Task notes; required for a manual check'),
90
+ newMilestone: z.string().optional().describe('Create the next milestone with this name and add the task to it'),
70
91
  },
71
- }, ({ title, description, assignedSubagent, milestone, verificationCommand, targetFiles, dependencies, fastPath, specRefs, complexity, acceptanceCriteria }) => captureOutput(() => runTaskAdd(title, targetDir, {
92
+ }, ({ title, description, assignedSubagent, milestone, verificationCommand, targetFiles, dependencies, fastPath, specRefs, complexity, acceptanceCriteria, verificationKind, notes, newMilestone }) => captureOutput(() => runTaskAdd(title, targetDir, {
72
93
  description,
73
94
  agent: assignedSubagent,
74
95
  milestone,
@@ -78,7 +99,10 @@ export function createMcpServer(targetDirArg) {
78
99
  fastPath,
79
100
  specRefs,
80
101
  complexity,
102
+ kind: verificationKind,
103
+ notes,
81
104
  accept: acceptanceCriteria,
105
+ newMilestone,
82
106
  json: true,
83
107
  })));
84
108
  registerNativTool('task_start', {
@@ -107,8 +131,9 @@ export function createMcpServer(targetDirArg) {
107
131
  taskId: z.string().optional().describe('Task ID to verify. Defaults to active in_progress task.'),
108
132
  milestone: z.string().optional().describe('Filter by milestone ID or name'),
109
133
  all: z.boolean().optional().describe('Run verification commands for all completed tasks to detect regressions'),
134
+ isolation: z.enum(['none', 'clean']).optional().describe('In place or clean copy; default is the profile'),
110
135
  },
111
- }, ({ taskId, milestone, all }) => captureOutput(() => runVerify(taskId, targetDir, { milestone, all, json: true })));
136
+ }, ({ taskId, milestone, all, isolation }) => captureOutput(() => runVerify(taskId, targetDir, { milestone, all, isolation, json: true })));
112
137
  registerNativTool('task_block', {
113
138
  description: 'Mark a task as blocked with a required reason (e.g. verification failed after 3 attempts).',
114
139
  inputSchema: {
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import Anthropic from '@anthropic-ai/sdk';
7
7
  import { loadMasterPlan } from '../core/verifier.js';
8
8
  import { writeJsonAtomicSync } from '../core/lock-manager.js';
9
+ import { withArchivedAsDone } from '../core/milestone-archive.js';
9
10
  import { cacheHitRate, computeActualCostUsd, emptyActualUsage, recordRunnerUsage, } from '../core/telemetry.js';
10
11
  import { resolveProjectRoot, linkWorktreeAiDirectory, safeUnlinkWorktreeAiDirectory, linkWorktreeNodeModules, safeUnlinkWorktreeNodeModules, } from '../core/root-resolver.js';
11
12
  import { resolveWorkerModel, toNativeModelId } from '../core/model-routing.js';
@@ -316,10 +317,12 @@ export class AgentSupervisor extends EventEmitter {
316
317
  const { plan } = loadMasterPlan(this.rootDir);
317
318
  if (!plan)
318
319
  return;
320
+ // Archived tasks are gone from live milestones; count them as completed.
319
321
  const byId = new Map();
320
- for (const milestone of plan.milestones)
322
+ for (const milestone of withArchivedAsDone(plan).milestones) {
321
323
  for (const t of milestone.tasks)
322
324
  byId.set(t.id, t);
325
+ }
323
326
  const unmet = task.dependencies.filter((id) => byId.get(id)?.status !== 'completed');
324
327
  if (unmet.length) {
325
328
  throw new SupervisorError('UNMET_DEPENDENCIES', `Task '${task.id}' has unmet dependencies: ${unmet.join(', ')}`);
@@ -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,8 +50,22 @@ 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[];
55
+ /** Names that lift the human gate; a check command must never set them. */
56
+ export declare const CHECK_OVERRIDE_NAMES: RegExp;
51
57
  /** Lists every structural problem with a task; empty when the task is valid. */
52
58
  export declare function validateMasterPlanTask(task: unknown): string[];
59
+ /** What the plan keeps about a milestone whose tasks moved to an archive file. */
60
+ export interface MilestoneArchiveInfo {
61
+ summary: string;
62
+ commit: string | null;
63
+ check: string;
64
+ archivedAt: string;
65
+ taskCount: number;
66
+ taskIds: string[];
67
+ file: string;
68
+ }
53
69
  export interface MasterPlanMilestone {
54
70
  id: string;
55
71
  name: string;
@@ -57,6 +73,8 @@ export interface MasterPlanMilestone {
57
73
  tasks: MasterPlanTask[];
58
74
  /** True for the milestone that collects fast-path tasks. */
59
75
  fastPath?: boolean;
76
+ /** Set when the tasks live in an archive file; `tasks` is then empty. */
77
+ archived?: MilestoneArchiveInfo;
60
78
  }
61
79
  export interface MasterPlan {
62
80
  $schema?: string;
@@ -17,6 +17,11 @@ 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
+ ];
23
+ /** Names that lift the human gate; a check command must never set them. */
24
+ export const CHECK_OVERRIDE_NAMES = /NATIV_(CI|HEADLESS)_OVERRIDE/;
20
25
  /** Lists every structural problem with a task; empty when the task is valid. */
21
26
  export function validateMasterPlanTask(task) {
22
27
  if (!task || typeof task !== 'object' || Array.isArray(task))
@@ -33,6 +38,9 @@ export function validateMasterPlanTask(task) {
33
38
  errors.push(`${label}: "description" must be a string`);
34
39
  if (typeof t.verificationCommand !== 'string')
35
40
  errors.push(`${label}: "verificationCommand" must be a string`);
41
+ if (typeof t.verificationCommand === 'string' && CHECK_OVERRIDE_NAMES.test(t.verificationCommand)) {
42
+ errors.push(`${label}: "verificationCommand" must not set NATIV_CI_OVERRIDE or NATIV_HEADLESS_OVERRIDE`);
43
+ }
36
44
  if (typeof t.assignedSubagent !== 'string' || !isSubagentType(t.assignedSubagent)) {
37
45
  errors.push(`${label}: "assignedSubagent" must be one of ${SUBAGENT_TYPES.join(', ')}`);
38
46
  }
@@ -54,6 +62,9 @@ export function validateMasterPlanTask(task) {
54
62
  if (t.complexity !== undefined && !TASK_COMPLEXITIES.includes(t.complexity)) {
55
63
  errors.push(`${label}: "complexity" must be one of ${TASK_COMPLEXITIES.join(', ')}`);
56
64
  }
65
+ if (t.verificationKind !== undefined && !TASK_VERIFICATION_KINDS.includes(t.verificationKind)) {
66
+ errors.push(`${label}: "verificationKind" must be one of ${TASK_VERIFICATION_KINDS.join(', ')}`);
67
+ }
57
68
  return errors;
58
69
  }
59
70
  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;
@@ -78,6 +79,7 @@ export declare function handlePipelineTasks(root: string): {
78
79
  name: string;
79
80
  status: "pending" | "in_progress" | "completed";
80
81
  fastPath?: boolean;
82
+ archived?: import("../../scanner/types.js").MilestoneArchiveInfo;
81
83
  }[];
82
84
  };
83
85
  /** POST /api/pipeline/tasks/action: the same lock-guarded transitions as `nativ task start|complete|block`. */