@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,6 +1,9 @@
1
1
  import pc from 'picocolors';
2
2
  import { isSubagentType, SUBAGENT_TYPES, validateMasterPlanTask, } from '../../scanner/types.js';
3
+ import { configuredInvocation } from '../../core/setup/setup-planner.js';
4
+ import { cliString, resolveCliInvocation } from '../../core/setup/types.js';
3
5
  import { withPlanLock } from '../../core/lock-manager.js';
6
+ import { archivedTaskIds } from '../../core/milestone-archive.js';
4
7
  import { getPlanPath, getRoleGuide, } from './task-common.js';
5
8
  export const FAST_PATH_MILESTONE_NAME = 'Fast-Path Tasks';
6
9
  /** Next `<prefix><n>` after the highest existing number, keeping that ID's zero-padding. */
@@ -23,7 +26,8 @@ function nextNumberedId(ids, prefix, defaultWidth) {
23
26
  return candidate;
24
27
  }
25
28
  export function generateNextTaskId(plan) {
26
- return nextNumberedId(plan.milestones.flatMap((m) => m.tasks.map((t) => t.id)), 'task-', 2);
29
+ const live = plan.milestones.flatMap((m) => m.tasks.map((t) => t.id));
30
+ return nextNumberedId([...live, ...archivedTaskIds(plan)], 'task-', 2);
27
31
  }
28
32
  export function generateNextMilestoneId(plan) {
29
33
  return nextNumberedId(plan.milestones.map((m) => m.id), 'm', 1);
@@ -43,6 +47,15 @@ function failTaskAdd(code, message, json) {
43
47
  process.exitCode = 1;
44
48
  return null;
45
49
  }
50
+ /** Default check for a docs task: `nativ check docs` on its markdown files, or '' without any. */
51
+ function defaultDocsCheck(targetFiles) {
52
+ const docs = targetFiles.filter((f) => /\.md$/i.test(f));
53
+ if (!docs.length)
54
+ return '';
55
+ const root = process.cwd();
56
+ const cli = cliString(configuredInvocation(root) ?? resolveCliInvocation({}));
57
+ return `${cli} check docs ${docs.join(' ')}`;
58
+ }
46
59
  /** Resolves a milestone by exact ID, exact name, or a unique partial name (all case-insensitive). */
47
60
  function findMilestone(plan, query) {
48
61
  const q = query.trim().toLowerCase();
@@ -76,7 +89,10 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
76
89
  ctx.abort();
77
90
  return failTaskAdd('INVALID_AGENT', `Unknown sub-agent "${agent}". Expected one of: ${SUBAGENT_TYPES.join(', ')}`, options.json);
78
91
  }
79
- const existingIds = new Set(plan.milestones.flatMap((m) => m.tasks.map((t) => t.id)));
92
+ const existingIds = new Set([
93
+ ...plan.milestones.flatMap((m) => m.tasks.map((t) => t.id)),
94
+ ...archivedTaskIds(plan),
95
+ ]);
80
96
  const deps = [...new Set(splitList(options.deps))];
81
97
  const unknownDeps = deps.filter((d) => !existingIds.has(d));
82
98
  if (unknownDeps.length) {
@@ -86,7 +102,29 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
86
102
  const fastPath = options.fastPath === true;
87
103
  let milestone;
88
104
  let createdMilestone = false;
89
- if (options.milestone) {
105
+ const newName = options.newMilestone?.trim();
106
+ if (options.newMilestone !== undefined && !newName) {
107
+ ctx.abort();
108
+ return failTaskAdd('INVALID_MILESTONE_NAME', 'A new milestone needs a name', options.json);
109
+ }
110
+ if (newName && (options.milestone || fastPath)) {
111
+ ctx.abort();
112
+ return failTaskAdd('CONFLICTING_OPTIONS', '--new-milestone cannot be used with --milestone or --fast-path', options.json);
113
+ }
114
+ const stillOpen = plan.milestones.filter((m) => !m.fastPath && m.status !== 'completed');
115
+ if (newName && stillOpen.length && !options.force) {
116
+ ctx.abort();
117
+ const ids = stillOpen.map((m) => m.id).join(', ');
118
+ return failTaskAdd('MILESTONE_STILL_OPEN', `Milestone ${ids} is not completed yet. Finish it first, or pass --force`, options.json);
119
+ }
120
+ if (newName) {
121
+ milestone = { id: generateNextMilestoneId(plan), name: newName, status: 'in_progress', tasks: [] };
122
+ plan.milestones.push(milestone);
123
+ plan.activeMilestoneId = milestone.id;
124
+ plan.overallStatus = 'in_progress';
125
+ createdMilestone = true;
126
+ }
127
+ else if (options.milestone) {
90
128
  const found = findMilestone(plan, options.milestone);
91
129
  if (Array.isArray(found)) {
92
130
  ctx.abort();
@@ -99,7 +137,7 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
99
137
  milestone = found;
100
138
  }
101
139
  else if (fastPath) {
102
- milestone = plan.milestones.find((m) => m.fastPath === true);
140
+ milestone = plan.milestones.find((m) => m.fastPath === true && !m.archived);
103
141
  if (!milestone) {
104
142
  milestone = { id: generateNextMilestoneId(plan), name: FAST_PATH_MILESTONE_NAME, status: 'pending', fastPath: true, tasks: [] };
105
143
  plan.milestones.push(milestone);
@@ -114,25 +152,40 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
114
152
  return failTaskAdd('NO_OPEN_MILESTONE', 'Every milestone is completed. Pass --milestone <id> to reopen one, or --fast-path', options.json);
115
153
  }
116
154
  }
155
+ if (milestone.archived) {
156
+ ctx.abort();
157
+ const how = `Restore it first with: nativ milestone restore ${milestone.id}`;
158
+ return failTaskAdd('MILESTONE_ARCHIVED', `Milestone ${milestone.id} is archived. ${how}`, options.json);
159
+ }
117
160
  const specRefs = [...new Set(splitList(options.specRefs))];
118
161
  const acceptanceCriteria = (Array.isArray(options.accept) ? options.accept : options.accept ? [options.accept] : [])
119
- .flatMap((item) => item.split('|'))
120
162
  .map((item) => item.trim())
121
163
  .filter(Boolean);
122
164
  const complexity = options.complexity?.trim().toLowerCase();
165
+ const kind = options.kind?.trim().toLowerCase();
166
+ const notes = (options.notes ?? '').trim();
167
+ if (kind === 'manual' && !notes) {
168
+ ctx.abort();
169
+ return failTaskAdd('MANUAL_NEEDS_NOTES', 'A manual check needs --notes saying what the person must confirm', options.json);
170
+ }
171
+ const files = splitList(options.files);
172
+ let verify = (options.verify ?? '').trim();
173
+ if (kind === 'docs' && !verify)
174
+ verify = defaultDocsCheck(files);
123
175
  const task = {
124
176
  id: generateNextTaskId(plan),
125
177
  title: cleanTitle,
126
178
  description: (options.description ?? '').trim(),
127
179
  assignedSubagent: agent,
128
180
  dependencies: deps,
129
- targetFiles: splitList(options.files),
181
+ targetFiles: files,
130
182
  status: 'pending',
131
- verificationCommand: (options.verify ?? '').trim(),
132
- notes: (options.notes ?? '').trim(),
183
+ verificationCommand: verify,
184
+ notes,
133
185
  ...(fastPath ? { fastPath: true } : {}),
134
186
  ...(specRefs.length ? { specRefs } : {}),
135
187
  ...(complexity ? { complexity: complexity } : {}),
188
+ ...(kind ? { verificationKind: kind } : {}),
136
189
  ...(acceptanceCriteria.length ? { acceptanceCriteria } : {}),
137
190
  };
138
191
  const problems = validateMasterPlanTask(task);
@@ -154,7 +207,7 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
154
207
  const track = fastPath ? pc.magenta(' [fast path]') : '';
155
208
  console.log(pc.green(`\n✔ Task [${pc.bold(task.id)}] added to [${milestone.id}] ${milestone.name}`) + track);
156
209
  if (createdMilestone)
157
- console.log(pc.cyan(` Created fast-path milestone [${milestone.id}]`));
210
+ console.log(pc.cyan(` Created ${fastPath ? 'fast-path ' : ''}milestone [${milestone.id}]`));
158
211
  if (reopenedMilestone)
159
212
  console.log(pc.yellow(` Reopened completed milestone [${milestone.id}]`));
160
213
  console.log(pc.dim(' Title: ') + task.title);
@@ -34,8 +34,14 @@ export interface TaskAddOptions {
34
34
  specRefs?: string | string[];
35
35
  /** simple | standard | complex. */
36
36
  complexity?: string;
37
+ /** unit | integration | static-config | docs | probe | manual. */
38
+ kind?: string;
37
39
  /** "Done when" lines: an array, or one string with "|" separators. */
38
40
  accept?: string | string[];
41
+ /** Create the next milestone with this name and add the task to it. */
42
+ newMilestone?: string;
43
+ /** With newMilestone, allow it while a planned milestone is still open (CLI only). */
44
+ force?: boolean;
39
45
  json?: boolean;
40
46
  }
41
47
  export interface TaskAddResult {
@@ -7,5 +7,7 @@ export interface TaskStartOptions {
7
7
  }
8
8
  export declare function runTaskStart(taskId: string, targetDirArg?: string, options?: TaskStartOptions): Promise<void | null>;
9
9
  export declare function runTaskUnlock(taskId: string, targetDirArg?: string, options?: TaskUnlockOptions): Promise<void>;
10
- export declare function runTaskComplete(taskId: string, targetDirArg?: string, options?: TaskCompleteOptions): Promise<void>;
10
+ export declare function runTaskComplete(taskId: string, targetDirArg?: string, options?: TaskCompleteOptions & {
11
+ isolation?: string;
12
+ }): Promise<void>;
11
13
  export declare function runTaskBlock(taskId: string, reason: string, targetDirArg?: string): Promise<void | null>;
@@ -1,5 +1,12 @@
1
+ import path from 'node:path';
2
+ import { withArchivedAsDone } from '../../core/milestone-archive.js';
1
3
  import pc from 'picocolors';
4
+ import { archivedTaskMessage, findArchivedMilestoneId } from './task-show.js';
5
+ import { parseIsolation } from '../verify.js';
6
+ import { resolveActiveProfileSettings } from '../../core/profiles.js';
7
+ import { resolveMainRoot } from '../../core/root-resolver.js';
2
8
  import { runTaskVerification } from '../../core/verifier.js';
9
+ import { evaluateSkip } from '../../core/verifier/skip-policy.js';
3
10
  import { digestFailure } from '../../core/verifier/output-digest.js';
4
11
  import { CircuitBreaker, TestIntegrityGuard, resolveGitHead, closeEscalationsForTask, } from '../../governor/index.js';
5
12
  import { loadPlan, withPlanLock } from '../../core/lock-manager.js';
@@ -26,11 +33,15 @@ export async function runTaskStart(taskId, targetDirArg, options = {}) {
26
33
  }
27
34
  if (!foundTask) {
28
35
  ctx.abort();
29
- console.error(pc.red(`\n✖ Task [${taskId}] not found in .ai/master_plan.json\n`));
36
+ const archivedIn = findArchivedMilestoneId(plan, taskId);
37
+ const why = archivedIn
38
+ ? archivedTaskMessage(taskId, archivedIn)
39
+ : `Task [${taskId}] not found in .ai/master_plan.json`;
40
+ console.error(pc.red(`\n✖ ${why}\n`));
30
41
  process.exitCode = 1;
31
42
  return;
32
43
  }
33
- const refusal = options.force ? null : startRefusal(plan, foundTask, agent);
44
+ const refusal = options.force ? null : startRefusal(withArchivedAsDone(plan), foundTask, agent);
34
45
  if (refusal) {
35
46
  ctx.abort();
36
47
  console.error(pc.red(`\n✖ ${refusal}\n`));
@@ -91,6 +102,23 @@ export async function runTaskUnlock(taskId, targetDirArg, options = {}) {
91
102
  export async function runTaskComplete(taskId, targetDirArg, options = {}) {
92
103
  if (options.skipVerify && refuseHeadless('nativ task complete --no-verify'))
93
104
  return;
105
+ let isolation;
106
+ try {
107
+ isolation = parseIsolation(options);
108
+ }
109
+ catch (e) {
110
+ console.error(pc.red(`
111
+ ✖ ${e.message}
112
+ `));
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ const root = resolveMainRoot(path.resolve(targetDirArg || process.cwd()));
117
+ const profile = resolveActiveProfileSettings(root);
118
+ if (isolation === 'none' &&
119
+ profile.verifyIsolationFailure === 'block' &&
120
+ refuseHeadless('nativ task complete --isolation none'))
121
+ return;
94
122
  const { targetDir, planPath } = getPlanPath(targetDirArg);
95
123
  const plan = loadPlan(planPath);
96
124
  if (!plan) {
@@ -145,7 +173,7 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
145
173
  // ── Verification Gatekeeper ────────────────────────────────────────────────
146
174
  let vResult = null;
147
175
  if (!options.skipVerify) {
148
- vResult = await runTaskVerification(foundTask, { cwd: targetDir, timeout: options.timeout });
176
+ vResult = await runTaskVerification(foundTask, { cwd: targetDir, timeout: options.timeout, isolation });
149
177
  if (!vResult.success) {
150
178
  console.error(pc.red(`\n✖ Task [${pc.bold(taskId)}] verification FAILED with exit code ${vResult.exitCode}:`));
151
179
  const failedPhase = vResult.phases?.find((p) => !p.success);
@@ -173,11 +201,27 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
173
201
  return;
174
202
  }
175
203
  if (vResult.skipped) {
204
+ const skip = evaluateSkip(foundTask, profile.skippedCheck);
205
+ if (skip.action === 'block') {
206
+ console.error(pc.red(`
207
+ ✖ Task [${pc.bold(taskId)}] cannot complete: ${skip.message}`));
208
+ console.error(pc.yellow(` Task remains '${foundTask.status}'.
209
+ `));
210
+ process.exitCode = 1;
211
+ return;
212
+ }
213
+ if (skip.action === 'warn')
214
+ console.log(pc.yellow(`
215
+ ⚠ ${skip.message}`));
176
216
  console.log(pc.dim(`\n○ Verification skipped: no verification command specified for [${taskId}]`));
177
217
  }
178
218
  else {
179
219
  const elapsed = (vResult.durationMs / 1000).toFixed(2);
180
- console.log(pc.green(`\n✔ Verification passed (${elapsed}s): `) + pc.yellow(`\`${foundTask.verificationCommand}\``));
220
+ const used = vResult.isolation ?? 'none';
221
+ console.log(pc.green(`\n✔ Verification passed (${elapsed}s, isolation: ${used}): `) +
222
+ pc.yellow(`\`${foundTask.verificationCommand}\``));
223
+ for (const w of vResult.warnings ?? [])
224
+ console.log(pc.yellow(` ⚠ ${w}`));
181
225
  }
182
226
  }
183
227
  else {
@@ -5,6 +5,7 @@ import { resolveSpecSlices } from '../../core/spec-slices.js';
5
5
  import { resolveWorkerModel } from '../../core/model-routing.js';
6
6
  import { learningsForTask } from '../../core/learnings.js';
7
7
  import { taskEscalationHistory } from '../../governor/index.js';
8
+ import { archivedTaskIds } from '../../core/milestone-archive.js';
8
9
  import { getPlanPath, getRoleGuide, getRecommendedContractSlice } from './task-common.js';
9
10
  export async function runTaskList(targetDirArg, options = {}) {
10
11
  const { planPath } = getPlanPath(targetDirArg);
@@ -13,7 +14,7 @@ export async function runTaskList(targetDirArg, options = {}) {
13
14
  process.exitCode = 1;
14
15
  return;
15
16
  }
16
- const completedTaskIds = new Set();
17
+ const completedTaskIds = new Set(archivedTaskIds(plan));
17
18
  for (const m of plan.milestones) {
18
19
  for (const t of m.tasks) {
19
20
  if (t.status === 'completed') {
@@ -120,6 +121,7 @@ function briefTask(t, milestoneId, isAvailable) {
120
121
  isAvailable,
121
122
  ...(t.dependencies?.length ? { dependencies: t.dependencies } : {}),
122
123
  ...(t.targetFiles?.length ? { targetFiles: t.targetFiles } : {}),
124
+ ...(t.fastPath ? { fastPath: true } : {}),
123
125
  ...(t.status === 'blocked' && t.notes ? { notes: t.notes } : {}),
124
126
  };
125
127
  }
@@ -134,7 +136,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
134
136
  process.exitCode = 1;
135
137
  return;
136
138
  }
137
- const completedTaskIds = new Set();
139
+ const completedTaskIds = new Set(archivedTaskIds(plan));
138
140
  for (const m of plan.milestones) {
139
141
  for (const t of m.tasks) {
140
142
  if (t.status === 'completed') {
@@ -0,0 +1,9 @@
1
+ import { MasterPlan } from '../../scanner/types.js';
2
+ export interface TaskShowOptions {
3
+ json?: boolean;
4
+ }
5
+ /** The sentence shown when someone acts on a task that moved to an archive file. */
6
+ export declare function archivedTaskMessage(taskId: string, milestoneId: string): string;
7
+ export declare function findArchivedMilestoneId(plan: MasterPlan, taskId: string): string | undefined;
8
+ /** Prints one task from the plan or, when archived, from its archive file. */
9
+ export declare function runTaskShow(taskId: string, targetDirArg?: string, options?: TaskShowOptions): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { loadPlan } from '../../core/lock-manager.js';
5
+ import { getPlanPath } from './task-common.js';
6
+ /** The sentence shown when someone acts on a task that moved to an archive file. */
7
+ export function archivedTaskMessage(taskId, milestoneId) {
8
+ return `Task [${taskId}] is archived (milestone ${milestoneId}). `
9
+ + `Run \`nativ milestone restore ${milestoneId}\` to bring its tasks back first.`;
10
+ }
11
+ export function findArchivedMilestoneId(plan, taskId) {
12
+ return plan.milestones.find((m) => m.archived?.taskIds.includes(taskId))?.id;
13
+ }
14
+ function readFromArchive(targetDir, plan, taskId) {
15
+ const milestone = plan.milestones.find((m) => m.archived?.taskIds.includes(taskId));
16
+ if (!milestone?.archived)
17
+ return null;
18
+ const file = path.join(targetDir, milestone.archived.file);
19
+ const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
20
+ const task = saved.tasks.find((t) => t.id === taskId);
21
+ if (!task)
22
+ throw new Error(`${milestone.archived.file} has no task ${taskId}`);
23
+ return { task, milestoneId: milestone.id };
24
+ }
25
+ function findTask(targetDir, plan, taskId) {
26
+ for (const m of plan.milestones) {
27
+ const task = m.tasks.find((t) => t.id === taskId);
28
+ if (task)
29
+ return { task, milestoneId: m.id, archived: false };
30
+ }
31
+ const found = readFromArchive(targetDir, plan, taskId);
32
+ return found && { ...found, archived: true };
33
+ }
34
+ /** Prints one task from the plan or, when archived, from its archive file. */
35
+ export async function runTaskShow(taskId, targetDirArg, options = {}) {
36
+ const { targetDir, planPath } = getPlanPath(targetDirArg);
37
+ const plan = loadPlan(planPath);
38
+ if (!plan) {
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ let found;
43
+ try {
44
+ found = findTask(targetDir, plan, taskId);
45
+ }
46
+ catch (err) {
47
+ console.error(pc.red(`\n✖ The archive of task [${taskId}] cannot be read: ${err.message}\n`));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ if (!found) {
52
+ console.error(pc.red(`\n✖ Task [${taskId}] not found in .ai/master_plan.json\n`));
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ if (options.json) {
57
+ console.log(JSON.stringify({ ...found.task, milestoneId: found.milestoneId, archived: found.archived }, null, 2));
58
+ return;
59
+ }
60
+ const { task } = found;
61
+ const where = found.archived ? `${found.milestoneId}, archived` : found.milestoneId;
62
+ console.log(pc.bold(pc.cyan(`\n[${task.id}] ${task.title}`)) + pc.dim(` (${where})`));
63
+ console.log(` Status: ${task.status} | Agent: ${task.assignedSubagent}`);
64
+ console.log(` Verify: \`${task.verificationCommand || 'none'}\``);
65
+ if (task.dependencies?.length)
66
+ console.log(` Depends on: ${task.dependencies.join(', ')}`);
67
+ if (task.targetFiles?.length)
68
+ console.log(` Files: ${task.targetFiles.join(', ')}`);
69
+ if (task.description)
70
+ console.log(`\n${task.description}`);
71
+ if (task.notes)
72
+ console.log(pc.dim(`\nNotes: ${task.notes}`));
73
+ console.log('');
74
+ }
@@ -1,6 +1,7 @@
1
1
  export interface UpdateOptions {
2
2
  /** Also replace directive and role-guide files that were edited by hand. */
3
3
  force?: boolean;
4
+ gitVisibility?: 'hidden' | 'shared';
4
5
  }
5
6
  /**
6
7
  * `nativ update`: brings a project up to the current templates without destroying its own work.
@@ -110,7 +110,7 @@ export async function runUpdate(targetDirArg, options = {}) {
110
110
  kept += syncTasteBundle(templatesDir, aiDir, options.force);
111
111
  // 2. Everything `nativ setup` manages: AGENTS.md, .claude/agents/, settings, MCP server
112
112
  console.log(pc.bold('\nClaude Code configuration'));
113
- const setup = applySetup(targetDir, { force: options.force });
113
+ const setup = applySetup(targetDir, { force: options.force, gitVisibility: options.gitVisibility });
114
114
  printSetupResult(setup);
115
115
  // 3. Backfill missing core contracts safely (NEVER overwrite existing project state)
116
116
  console.log(pc.bold('\nContracts'));
@@ -33,6 +33,24 @@ export async function runValidate(targetDirArg) {
33
33
  console.log(pc.green(` ✔ Present: ${relPath}`));
34
34
  return content;
35
35
  }
36
+ function checkArchive(m) {
37
+ const rel = `.ai/archive/${m.id}.json`;
38
+ let saved;
39
+ try {
40
+ saved = JSON.parse(fs.readFileSync(path.join(targetDir, rel), 'utf8'));
41
+ }
42
+ catch (err) {
43
+ console.log(pc.red(` ✖ Archive file of ${m.id} is missing or unreadable: ${rel} (${err.message})`));
44
+ return false;
45
+ }
46
+ const ids = Array.isArray(saved?.tasks) ? saved.tasks.map((t) => t.id) : null;
47
+ if (saved?.id !== m.id || JSON.stringify(ids) !== JSON.stringify(m.archived.taskIds)) {
48
+ console.log(pc.red(` ✖ ${rel} does not match the archived stub of ${m.id}`));
49
+ return false;
50
+ }
51
+ console.log(pc.green(` ✔ Archive matches: ${rel}`));
52
+ return true;
53
+ }
36
54
  // Directives tell agents how to work; they are guidance, not contracts, and GEMINI.md is only an optional adapter guide.
37
55
  console.log(pc.bold('1. Directives:'));
38
56
  const directives = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].filter((f) => fs.existsSync(path.join(targetDir, f)));
@@ -66,6 +84,12 @@ export async function runValidate(targetDirArg) {
66
84
  for (const w of specWarnings)
67
85
  console.log(pc.yellow(` ⚠ ${w}`));
68
86
  }
87
+ if (plan && Array.isArray(plan.milestones)) {
88
+ for (const m of plan.milestones) {
89
+ if (m?.archived && !checkArchive(m))
90
+ hasErrors = true;
91
+ }
92
+ }
69
93
  const escalationPath = path.join(targetDir, '.ai/escalation.json');
70
94
  if (fs.existsSync(escalationPath)) {
71
95
  const escData = checkFile('.ai/escalation.json', true);
@@ -4,5 +4,12 @@ export interface VerifyOptions {
4
4
  milestone?: string;
5
5
  json?: boolean;
6
6
  timeout?: number;
7
+ isolation?: string;
8
+ clean?: boolean;
7
9
  }
10
+ /** Reads --isolation / --clean; undefined means use the profile. */
11
+ export declare function parseIsolation(o: {
12
+ isolation?: string;
13
+ clean?: boolean;
14
+ }): 'none' | 'clean' | undefined;
8
15
  export declare function runVerify(taskId?: string, targetDirArg?: string, options?: VerifyOptions): Promise<BatchVerificationResult>;
@@ -1,6 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import pc from 'picocolors';
3
3
  import { verifyBatch } from '../core/verifier.js';
4
+ import { loadPlan } from '../core/lock-manager.js';
5
+ import { evaluateSkip } from '../core/verifier/skip-policy.js';
6
+ import { resolveActiveProfileSettings } from '../core/profiles.js';
4
7
  import { digestFailure } from '../core/verifier/output-digest.js';
5
8
  /** Passing runs need no output; failing ones get a digest and the path to the full log. */
6
9
  function compactBatch(batch, root) {
@@ -14,6 +17,15 @@ function compactBatch(batch, root) {
14
17
  }),
15
18
  };
16
19
  }
20
+ /** Reads --isolation / --clean; undefined means use the profile. */
21
+ export function parseIsolation(o) {
22
+ const v = o.clean ? 'clean' : o.isolation;
23
+ if (v === undefined)
24
+ return undefined;
25
+ if (v !== 'none' && v !== 'clean')
26
+ throw new Error(`--isolation must be none or clean, got "${v}"`);
27
+ return v;
28
+ }
17
29
  export async function runVerify(taskId, targetDirArg, options = {}) {
18
30
  const targetDir = path.resolve(targetDirArg || process.cwd());
19
31
  const batch = await verifyBatch(targetDir, {
@@ -21,6 +33,7 @@ export async function runVerify(taskId, targetDirArg, options = {}) {
21
33
  milestoneId: options.milestone,
22
34
  all: options.all,
23
35
  timeout: options.timeout,
36
+ isolation: parseIsolation(options),
24
37
  });
25
38
  if (options.json) {
26
39
  console.log(JSON.stringify(compactBatch(batch, targetDir)));
@@ -35,24 +48,35 @@ export async function runVerify(taskId, targetDirArg, options = {}) {
35
48
  console.log(pc.yellow('No candidate tasks found to verify. Specify a taskId, --milestone, or --all.\n'));
36
49
  return batch;
37
50
  }
51
+ const skipMode = resolveActiveProfileSettings(targetDir).skippedCheck;
52
+ const plan = loadPlan(path.join(targetDir, '.ai', 'master_plan.json'), { silent: true });
53
+ const planTasks = plan?.milestones.flatMap((m) => m.tasks) ?? [];
38
54
  for (const t of batch.tasks) {
39
55
  const elapsed = (t.result.durationMs / 1000).toFixed(2);
40
56
  if (t.result.skipped) {
41
57
  console.log(pc.dim(` ○ [${t.taskId}] ${t.title} — no verification command (skipped)`));
58
+ const pt = planTasks.find((x) => x.id === t.taskId);
59
+ const skip = pt && evaluateSkip(pt, skipMode);
60
+ if (skip?.message)
61
+ console.log(pc.yellow(` ⚠ ${skip.message}`));
42
62
  continue;
43
63
  }
44
64
  if (t.result.success) {
45
65
  console.log(pc.green(` ✔ [${pc.bold(t.taskId)}] ${t.title}`) +
46
- pc.dim(` (${elapsed}s)`) +
66
+ pc.dim(` (${elapsed}s, isolation: ${t.result.isolation ?? 'none'})`) +
47
67
  pc.yellow(` \`${t.result.command}\``));
68
+ for (const w of t.result.warnings ?? [])
69
+ console.log(pc.yellow(` ⚠ ${w}`));
48
70
  if (t.result.phases?.length) {
49
71
  console.log(pc.dim(` Phases passed: ${t.result.phases.map((p) => p.name).join(', ')}`));
50
72
  }
51
73
  }
52
74
  else {
53
75
  console.log(pc.red(` ✖ [${pc.bold(t.taskId)}] ${t.title}`) +
54
- pc.dim(` (${elapsed}s, exit code: ${t.result.exitCode})`) +
76
+ pc.dim(` (${elapsed}s, exit ${t.result.exitCode}, isolation: ${t.result.isolation ?? 'none'})`) +
55
77
  pc.yellow(` \`${t.result.command}\``));
78
+ for (const w of t.result.warnings ?? [])
79
+ console.log(pc.yellow(` ⚠ ${w}`));
56
80
  const failedPhase = t.result.phases?.find((p) => !p.success);
57
81
  if (failedPhase)
58
82
  console.log(pc.red(` Failed in phase "${failedPhase.name}"`));
@@ -0,0 +1,18 @@
1
+ /** One nativ command: its own flags and its subcommands. */
2
+ export interface CommandSpec {
3
+ flags: string[];
4
+ subs: Record<string, CommandSpec>;
5
+ }
6
+ export interface DocFinding {
7
+ file: string;
8
+ line: number;
9
+ kind: 'path' | 'placeholder' | 'setting' | 'flag';
10
+ token: string;
11
+ message: string;
12
+ }
13
+ export interface DocsContext {
14
+ root: string;
15
+ envNames: Set<string>;
16
+ program: CommandSpec;
17
+ }
18
+ export declare function checkDocText(text: string, docFile: string, ctx: DocsContext): DocFinding[];