@njeybe/nativ 2.9.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 (36) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +36 -5
  3. package/bin/cli.js +1 -1
  4. package/dist/commands/milestone.d.ts +10 -0
  5. package/dist/commands/milestone.js +84 -0
  6. package/dist/commands/status.d.ts +1 -0
  7. package/dist/commands/status.js +39 -8
  8. package/dist/commands/task/task-add.js +13 -3
  9. package/dist/commands/task/task-lifecycle.js +8 -2
  10. package/dist/commands/task/task-query.js +4 -2
  11. package/dist/commands/task/task-show.d.ts +9 -0
  12. package/dist/commands/task/task-show.js +74 -0
  13. package/dist/commands/validate.js +24 -0
  14. package/dist/core/milestone-archive.d.ts +40 -0
  15. package/dist/core/milestone-archive.js +299 -0
  16. package/dist/core/setup/pm-playbook.d.ts +1 -1
  17. package/dist/core/setup/pm-playbook.js +7 -1
  18. package/dist/core/setup/session-context.d.ts +9 -1
  19. package/dist/core/setup/session-context.js +24 -2
  20. package/dist/core/verifier/task-verifier.js +25 -0
  21. package/dist/core/verifier/verify-env.js +4 -1
  22. package/dist/index.js +14 -3
  23. package/dist/mcp/server.js +18 -0
  24. package/dist/runner/agent-supervisor.js +4 -1
  25. package/dist/scanner/types.d.ts +14 -0
  26. package/dist/scanner/types.js +3 -1
  27. package/dist/server/routes/task-routes.d.ts +1 -0
  28. package/dist/server/routes/task-routes.js +13 -5
  29. package/dist/server/studio-ui/shell/model.js +16 -9
  30. package/package.json +1 -1
  31. package/plugin/.claude-plugin/plugin.json +1 -1
  32. package/plugin/agents/worker.md +3 -3
  33. package/plugin/hooks/hooks.json +1 -1
  34. package/plugin/skills/nativ/SKILL.md +4 -3
  35. package/templates/AGENTS.md +4 -3
  36. package/templates/claude-agents/worker.md +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.9.1
4
+
5
+ ### Added
6
+
7
+ - **Plan compaction.** `nativ milestone archive <ids> --summary "..."` moves a finished milestone into `.ai/archive/` once the regression suite passes, and `nativ milestone restore <id>` brings it back. `nativ task show <id>` reads one task, archived or not. MCP tools `nativ_milestone_archive` and `nativ_task_show` cover archive and show; restore stays CLI only.
8
+ - **Archived milestones count everywhere.** `nativ status` and the Studio keep archived tasks in their totals, dependencies on archived tasks count as met, `nativ validate` fails when an archive file is missing or does not match, and `verify --all` skips archived tasks.
9
+ - **`regressionCommand` setting.** The test suite `nativ milestone archive` runs, saved by `--check`.
10
+ - **Project manager guide.** After the verifier passes a milestone: archive it, tell the human, suggest `/clear`.
11
+
12
+ ### Changed
13
+
14
+ - **Smaller JSON by default.** `nativ status --json` returns full tasks only for the active milestone, and `nativ task list --json` (and `nativ tasks --json`) returns a short summary per task. Pass `--full` for the old output. Scripts that read these may need to change.
15
+
16
+ ### Fixed
17
+
18
+ - **`--json` on `task list`.** The help promised a summary, but the CLI always printed every field. It now matches the MCP tool.
19
+ - **Checks ran twice.** The worker guide told workers to run a task's check before `nativ task complete`, which runs it anyway.
20
+ - **Startup instructions loaded twice.** With nativ installed as both a plugin and in the project, the plugin's hook now stays silent.
21
+ - **Git Bash found by clean checks on Windows.** Clean check environments now pass `ProgramFiles` and `ProgramFiles(x86)` on Windows, so native runner checks find Git Bash without a `verifyEnv` workaround.
22
+
3
23
  ## 2.9.0
4
24
 
5
25
  ### Added
package/README.md CHANGED
@@ -128,6 +128,7 @@ concrete result to the next.
128
128
  | 5. Check | let the worker complete the task | The gatekeeper runs your verify phases, then the task's command, then a code-style check. |
129
129
  | 6. Review | ask for a **verifier** pass | An independent read of the diff against the contracts. |
130
130
  | 7. Improve | approve lessons agents proposed | `nativ learn list --status proposed`, then `approve` or `reject`. |
131
+ | 8. Compact | archive the finished milestone, then `/clear` | `nativ milestone archive` shrinks the plan to a one-line summary. The plan holds the state, so a fresh session loses nothing and costs fewer tokens. |
131
132
 
132
133
  ---
133
134
 
@@ -152,6 +153,7 @@ Every command takes an optional `[targetDir]` (default: the current directory) a
152
153
  | [`hook status`](#nativ-hook) | Enforcement mode, role and active task scope | You, Agent |
153
154
  | [`task next`](#nativ-task-next) | The next executable task with its context | Agent |
154
155
  | [`task list`](#nativ-task-list) / `tasks` | List tasks with filters | You, Agent |
156
+ | [`task show`](#nativ-task-show) | One task from the plan or its archive | You, Agent |
155
157
  | [`task add`](#nativ-task-add) | Add a task | Architect |
156
158
  | [`task start`](#nativ-task-start) | Mark a task in progress | Agent |
157
159
  | [`task reclaim`](#nativ-task-reclaim) | Free tasks left in progress by a stopped agent | You, Agent |
@@ -160,6 +162,7 @@ Every command takes an optional `[targetDir]` (default: the current directory) a
160
162
  | [`task escalate`](#nativ-task-escalate) | Escalate a contract gap to the architect | Agent |
161
163
  | [`task propose-patch`](#nativ-task-propose-patch) | Propose a governed contract change | Agent |
162
164
  | [`task unlock`](#nativ-task-unlock) | Lift a task's file scope in an emergency | *You only* |
165
+ | [`milestone archive` / `restore`](#nativ-milestone-archive--restore) | Move a finished milestone out of the plan, or bring it back | You, Agent (`restore` by CLI) |
163
166
  | [`verify`](#nativ-verify) | Run verification for a task, a milestone or all | You, Agent |
164
167
  | [`learn propose`](#nativ-learn-propose) | Propose a lesson for later workers | Agent |
165
168
  | [`learn list`](#nativ-learn-list) | List lessons | You, Agent |
@@ -290,9 +293,11 @@ nativ validate [targetDir]
290
293
  Progress per milestone; with `--telemetry`, durations, token and cost estimates, pass rates and role violations.
291
294
 
292
295
  ```bash
293
- nativ status [targetDir] [-t, --telemetry] [--json]
296
+ nativ status [targetDir] [-t, --telemetry] [--json] [--full]
294
297
  ```
295
298
 
299
+ `--json` returns full tasks only for the active milestone, one line for each other milestone and the telemetry summary. Add `--full` for every task and per-task telemetry. Archived milestones count toward the totals.
300
+
296
301
  #### `nativ hook`
297
302
 
298
303
  ```bash
@@ -348,7 +353,8 @@ nativ task list [targetDir] [options] # also available as: nativ tasks
348
353
  | `-s, --status <status>` | `pending`, `in_progress`, `completed` or `blocked` |
349
354
  | `-m, --milestone <id>` | Milestone ID or name |
350
355
  | `--fast-path` | Only fast-path tasks |
351
- | `--json` | Output as JSON |
356
+ | `--json` | Output a short summary per task as JSON |
357
+ | `--full` | With `--json`, include every task field |
352
358
 
353
359
  #### `nativ task add`
354
360
 
@@ -390,6 +396,26 @@ nativ task start <taskId> [targetDir] [--agent <id>] [--force]
390
396
 
391
397
  Claims the task, marks it `in_progress` and records the baseline the test-integrity guard compares against. The task must be pending and its dependencies completed; a blocked task goes through `nativ triage` first, and only a person can override with `--force`. The claim records `claimedBy` (`--agent` or `$NATIV_AGENT_ID`) and `claimedAt`, so a parallel agent cannot take a task another agent holds. Running `task start` again on your own task refreshes the claim; a claim older than 4 hours counts as abandoned.
392
398
 
399
+ #### `nativ task show`
400
+
401
+ ```bash
402
+ nativ task show <taskId> [targetDir] [--json]
403
+ ```
404
+
405
+ Prints one task from the plan, or from its milestone archive when the milestone is archived. Use it for an old task instead of opening the plan.
406
+
407
+ #### `nativ milestone archive` / `restore`
408
+
409
+ ```bash
410
+ nativ milestone archive <ids> --summary "Studio redesign: shell, routing, pages"
411
+ nativ milestone archive m1,m2 --summaries summaries.json [--check "npm test"] [--timeout 600000]
412
+ nativ milestone restore <id>
413
+ ```
414
+
415
+ Archives finished milestones after the regression suite passes. Every task must be completed, and the working tree must be clean. The archive is written to `.ai/archive/<id>.json` and read back before the plan changes. The suite is the `regressionCommand` in `.nativ/config.json`; `--check` overrides and saves it. Default timeout is 10 minutes. Archived task ids still count for numbering and dependencies. `restore` is CLI only.
416
+
417
+ A summaries file maps each milestone id to its one-line summary, and the suite runs once for the whole batch. `.ai/` is usually not in git, so keep archive files: they are the only full copy of those tasks.
418
+
393
419
  #### `nativ task reclaim`
394
420
 
395
421
  ```bash
@@ -470,7 +496,7 @@ nativ task unlock <taskId> --revoke
470
496
 
471
497
  #### `nativ verify`
472
498
 
473
- Re-run verification for one task, a milestone or every completed task. Also available as `nativ task verify`.
499
+ Re-run verification for one task, a milestone or every completed task. Also available as `nativ task verify`. `--all` skips archived tasks, because their old checks may no longer apply; naming an archived task or milestone explains how to restore it.
474
500
 
475
501
  ```bash
476
502
  nativ verify task-01
@@ -747,6 +773,7 @@ Written by `setup`; every key is optional.
747
773
  | `verifyIsolationFailure` | `warn` | `warn` or `block` when a check fails in the clean copy but passes in place |
748
774
  | `verifyEnv` | none | Extra environment variable names a check may read |
749
775
  | `verifyLinks` | `node_modules`, `.venv`, `venv` | Files linked into the clean copy, for example dependency folders |
776
+ | `regressionCommand` | none | The test suite `nativ milestone archive` runs before archiving; `--check` sets it |
750
777
  | `providers` | see [Providers](#providers) | Provider order per role: `architect`, `pm`, `worker`, `verifier`, `triage` |
751
778
  | `models` | provider default | Model per provider |
752
779
  | `notify` | off | `webhook`, `command` and `events` for human notifications. See [`nativ notify test`](#nativ-notify-test) |
@@ -846,6 +873,7 @@ A deterministic firewall between agents and the contracts:
846
873
  | `.ai/db_schema.json`, `.ai/api_contracts.json`, `.ai/ui_specs.md` | architect | The contracts |
847
874
  | `.ai/design/style-tile.html` | architect | The chosen visual direction |
848
875
  | `.ai/master_plan.json` | `nativ task` commands | Milestones and tasks |
876
+ | `.ai/archive/<id>.json` | `nativ milestone archive` | Full copy of an archived milestone; the plan keeps a one-line summary |
849
877
  | `.ai/subagents/*.md` | init, update | One guide per worker role |
850
878
  | `.ai/escalation.json` | `task escalate`, triage | Escalations and how they were settled |
851
879
  | `.ai/learnings.json` | `nativ learn` | Proposed, approved and rejected lessons |
@@ -863,7 +891,8 @@ Files nativ wrote carry a marker, so `update` and `doctor` can tell untouched fi
863
891
 
864
892
  | Tools | Purpose |
865
893
  | :--- | :--- |
866
- | `nativ_task_next`, `nativ_task_list`, `nativ_task_add` | Find and add work |
894
+ | `nativ_task_next`, `nativ_task_list`, `nativ_task_show`, `nativ_task_add` | Find and add work (`task_show` also reads archived tasks) |
895
+ | `nativ_milestone_archive` | Archive finished milestones after the regression suite passes. A slow suite can outlast a client's tool timeout; use the CLI for those |
867
896
  | `nativ_task_start`, `nativ_task_complete`, `nativ_task_block`, `nativ_task_reclaim` | Move a task through its lifecycle (complete always runs the gatekeeper; reclaim frees stale claims) |
868
897
  | `nativ_validate` | Check the `.ai/` contracts, plan and role guides; returns only problems and a verdict |
869
898
  | `nativ_triage` | Self-heal: evaluate pending escalations, apply sandbox-proven safe fixes, return decision cards for the rest |
@@ -878,7 +907,7 @@ Files nativ wrote carry a marker, so `update` and `doctor` can tell untouched fi
878
907
  | :--- |
879
908
  | `nativ://context`, `nativ://master-plan`, `nativ://db-schema`, `nativ://api-contracts`, `nativ://escalation`, `nativ://telemetry` |
880
909
 
881
- There is deliberately no MCP tool to unlock a task, approve a lesson, write a contract, sync a database schema or run setup.
910
+ There is deliberately no MCP tool to unlock a task, approve a lesson, write a contract, sync a database schema, restore an archived milestone or run setup.
882
911
 
883
912
  **Manual registration:**
884
913
 
@@ -966,6 +995,8 @@ npm test
966
995
  npm run sync-plugin # after changing a template or the setup generator; a test fails if plugin/ is stale
967
996
  ```
968
997
 
998
+ Some suites need a temp folder outside any git repository. If your home folder is a git repository and temp sits inside it, `learnings`, `verify-gatekeeper` and `clean-tree` fail; point `TEMP`, `TMP` and `TMPDIR` elsewhere when you run `npm test`.
999
+
969
1000
  ---
970
1001
 
971
1002
  ## License
package/bin/cli.js CHANGED
@@ -14,7 +14,7 @@ if (group === 'hook' && (subcommand === 'check' || subcommand === 'context')) {
14
14
  await runHookCheck(targetDir);
15
15
  } else {
16
16
  const { runSessionContext } = await import('../dist/core/setup-assets.js');
17
- runSessionContext(targetDir);
17
+ runSessionContext(targetDir, undefined, { fromPlugin: rest.includes('--from-plugin') });
18
18
  }
19
19
  } catch (err) {
20
20
  // A hook must never fail the tool call it guards.
@@ -0,0 +1,10 @@
1
+ import type { Command } from 'commander';
2
+ export interface MilestoneArchiveOptions {
3
+ summary?: string;
4
+ summaries?: string;
5
+ check?: string;
6
+ timeout?: number;
7
+ }
8
+ export declare function runMilestoneArchive(idList: string, targetDirArg: string | undefined, options: MilestoneArchiveOptions): Promise<void>;
9
+ export declare function runMilestoneRestore(id: string, targetDirArg?: string): Promise<void>;
10
+ export declare function registerMilestoneCommands(program: Command): void;
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { archiveMilestones, ArchiveRefusal, restoreMilestone, DEFAULT_SUITE_TIMEOUT_MS, } from '../core/milestone-archive.js';
5
+ function readSummaries(file) {
6
+ let parsed;
7
+ try {
8
+ parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
9
+ }
10
+ catch (err) {
11
+ throw new ArchiveRefusal(`--summaries file cannot be read (${err?.message}).`);
12
+ }
13
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
14
+ throw new ArchiveRefusal('--summaries file must be a JSON object of milestone id to summary.');
15
+ }
16
+ return parsed;
17
+ }
18
+ function collectSummaries(ids, options) {
19
+ if (options.summary !== undefined && options.summaries) {
20
+ throw new ArchiveRefusal('Use --summary or --summaries, not both.');
21
+ }
22
+ if (options.summaries)
23
+ return readSummaries(options.summaries);
24
+ if (options.summary === undefined)
25
+ return {};
26
+ if (ids.length > 1)
27
+ throw new ArchiveRefusal('Several milestones need --summaries <file.json>.');
28
+ return { [ids[0]]: options.summary };
29
+ }
30
+ async function report(work, onDone) {
31
+ try {
32
+ onDone(await work());
33
+ }
34
+ catch (err) {
35
+ if (!(err instanceof ArchiveRefusal))
36
+ throw err;
37
+ console.error(pc.red(`\n✖ ${err.message}\n`));
38
+ process.exitCode = 1;
39
+ }
40
+ }
41
+ export async function runMilestoneArchive(idList, targetDirArg, options) {
42
+ const targetDir = path.resolve(targetDirArg || process.cwd());
43
+ const ids = idList.split(',').map((id) => id.trim()).filter(Boolean);
44
+ await report(async () => archiveMilestones(targetDir, {
45
+ ids,
46
+ summaries: collectSummaries(ids, options),
47
+ check: options.check,
48
+ timeout: options.timeout,
49
+ }), (result) => {
50
+ for (const m of result.archived) {
51
+ console.log(pc.green(`\n✔ Archived [${m.id}]: ${m.taskCount} tasks moved to ${m.file}`));
52
+ }
53
+ console.log(pc.dim(` Active milestone: ${result.activeMilestoneId}\n`));
54
+ result.warnings.forEach((w) => console.log(pc.yellow(`⚠ ${w}`)));
55
+ });
56
+ }
57
+ export async function runMilestoneRestore(id, targetDirArg) {
58
+ const targetDir = path.resolve(targetDirArg || process.cwd());
59
+ await report(() => restoreMilestone(targetDir, id), (result) => {
60
+ console.log(pc.green(`\n✔ Restored [${result.id}]: ${result.taskCount} tasks are back\n`));
61
+ result.warnings.forEach((w) => console.log(pc.yellow(`⚠ ${w}`)));
62
+ });
63
+ }
64
+ export function registerMilestoneCommands(program) {
65
+ const milestone = program
66
+ .command('milestone')
67
+ .description('Move finished milestones out of .ai/master_plan.json and back');
68
+ milestone
69
+ .command('archive <ids> [targetDir]')
70
+ .description('Archive finished milestones (comma-separated ids) after the test suite passes')
71
+ .option('--summary <text>', 'What the milestone delivered (required for one milestone)')
72
+ .option('--summaries <file>', 'JSON file of milestone id to summary, for several milestones')
73
+ .option('--check <command>', 'Suite to run; saved as regressionCommand in .nativ/config.json')
74
+ .option('--timeout <ms>', `Suite timeout in ms (default ${DEFAULT_SUITE_TIMEOUT_MS})`, parseInt)
75
+ .action(async (ids, targetDir, options) => {
76
+ await runMilestoneArchive(ids, targetDir, options);
77
+ });
78
+ milestone
79
+ .command('restore <id> [targetDir]')
80
+ .description('Put an archived milestone and its tasks back into the plan')
81
+ .action(async (id, targetDir) => {
82
+ await runMilestoneRestore(id, targetDir);
83
+ });
84
+ }
@@ -1,5 +1,6 @@
1
1
  export interface StatusOptions {
2
2
  telemetry?: boolean;
3
3
  json?: boolean;
4
+ full?: boolean;
4
5
  }
5
6
  export declare function runStatus(targetDirArg?: string, options?: StatusOptions): Promise<void>;
@@ -17,14 +17,7 @@ export async function runStatus(targetDirArg, options = {}) {
17
17
  const plan = JSON.parse(raw);
18
18
  const telemetry = loadTelemetry(telemetryPath, plan.projectName);
19
19
  if (options.json) {
20
- console.log(JSON.stringify({
21
- projectName: plan.projectName,
22
- overallStatus: plan.overallStatus,
23
- activeMilestoneId: plan.activeMilestoneId,
24
- milestones: plan.milestones,
25
- telemetry: telemetry.summary,
26
- tasksTelemetry: telemetry.tasks,
27
- }, null, 2));
20
+ console.log(JSON.stringify(buildStatusJson(plan, telemetry, !!options.full), null, 2));
28
21
  return;
29
22
  }
30
23
  console.log(pc.bold(pc.cyan(`\n📊 AI Agent Workflow Status: ${plan.projectName}`)));
@@ -34,6 +27,14 @@ export async function runStatus(targetDirArg, options = {}) {
34
27
  let inProgressTasks = 0;
35
28
  let blockedTasks = 0;
36
29
  for (const m of plan.milestones) {
30
+ if (m.archived) {
31
+ // Archived work still counts, so progress does not drop after archiving.
32
+ totalTasks += m.archived.taskCount;
33
+ completedTasks += m.archived.taskCount;
34
+ const line = `🚩 Milestone [${m.id}]: ${m.name} (archived, ${m.archived.taskCount} tasks)`;
35
+ console.log(pc.bold(`\n${line}`) + pc.dim(` - ${m.archived.summary}`));
36
+ continue;
37
+ }
37
38
  console.log(pc.bold(`\n🚩 Milestone [${m.id}]: ${m.name} (${m.status})`));
38
39
  for (const t of m.tasks) {
39
40
  totalTasks++;
@@ -73,3 +74,33 @@ export async function runStatus(targetDirArg, options = {}) {
73
74
  process.exitCode = 1;
74
75
  }
75
76
  }
77
+ function buildStatusJson(plan, telemetry, full) {
78
+ const base = {
79
+ projectName: plan.projectName,
80
+ overallStatus: plan.overallStatus,
81
+ activeMilestoneId: plan.activeMilestoneId,
82
+ };
83
+ if (full) {
84
+ return { ...base, milestones: plan.milestones, telemetry: telemetry.summary, tasksTelemetry: telemetry.tasks };
85
+ }
86
+ const milestones = plan.milestones.map((m) => m.archived
87
+ ? {
88
+ id: m.id,
89
+ name: m.name,
90
+ status: m.status,
91
+ taskCount: m.archived.taskCount,
92
+ completedCount: m.archived.taskCount,
93
+ archived: true,
94
+ summary: m.archived.summary,
95
+ }
96
+ : m.id === plan.activeMilestoneId
97
+ ? m
98
+ : {
99
+ id: m.id,
100
+ name: m.name,
101
+ status: m.status,
102
+ taskCount: m.tasks.length,
103
+ completedCount: m.tasks.filter((t) => t.status === 'completed').length,
104
+ });
105
+ return { ...base, milestones, telemetry: telemetry.summary };
106
+ }
@@ -3,6 +3,7 @@ import { isSubagentType, SUBAGENT_TYPES, validateMasterPlanTask, } from '../../s
3
3
  import { configuredInvocation } from '../../core/setup/setup-planner.js';
4
4
  import { cliString, resolveCliInvocation } from '../../core/setup/types.js';
5
5
  import { withPlanLock } from '../../core/lock-manager.js';
6
+ import { archivedTaskIds } from '../../core/milestone-archive.js';
6
7
  import { getPlanPath, getRoleGuide, } from './task-common.js';
7
8
  export const FAST_PATH_MILESTONE_NAME = 'Fast-Path Tasks';
8
9
  /** Next `<prefix><n>` after the highest existing number, keeping that ID's zero-padding. */
@@ -25,7 +26,8 @@ function nextNumberedId(ids, prefix, defaultWidth) {
25
26
  return candidate;
26
27
  }
27
28
  export function generateNextTaskId(plan) {
28
- 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);
29
31
  }
30
32
  export function generateNextMilestoneId(plan) {
31
33
  return nextNumberedId(plan.milestones.map((m) => m.id), 'm', 1);
@@ -87,7 +89,10 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
87
89
  ctx.abort();
88
90
  return failTaskAdd('INVALID_AGENT', `Unknown sub-agent "${agent}". Expected one of: ${SUBAGENT_TYPES.join(', ')}`, options.json);
89
91
  }
90
- 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
+ ]);
91
96
  const deps = [...new Set(splitList(options.deps))];
92
97
  const unknownDeps = deps.filter((d) => !existingIds.has(d));
93
98
  if (unknownDeps.length) {
@@ -132,7 +137,7 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
132
137
  milestone = found;
133
138
  }
134
139
  else if (fastPath) {
135
- milestone = plan.milestones.find((m) => m.fastPath === true);
140
+ milestone = plan.milestones.find((m) => m.fastPath === true && !m.archived);
136
141
  if (!milestone) {
137
142
  milestone = { id: generateNextMilestoneId(plan), name: FAST_PATH_MILESTONE_NAME, status: 'pending', fastPath: true, tasks: [] };
138
143
  plan.milestones.push(milestone);
@@ -147,6 +152,11 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
147
152
  return failTaskAdd('NO_OPEN_MILESTONE', 'Every milestone is completed. Pass --milestone <id> to reopen one, or --fast-path', options.json);
148
153
  }
149
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
+ }
150
160
  const specRefs = [...new Set(splitList(options.specRefs))];
151
161
  const acceptanceCriteria = (Array.isArray(options.accept) ? options.accept : options.accept ? [options.accept] : [])
152
162
  .map((item) => item.trim())
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
+ import { withArchivedAsDone } from '../../core/milestone-archive.js';
2
3
  import pc from 'picocolors';
4
+ import { archivedTaskMessage, findArchivedMilestoneId } from './task-show.js';
3
5
  import { parseIsolation } from '../verify.js';
4
6
  import { resolveActiveProfileSettings } from '../../core/profiles.js';
5
7
  import { resolveMainRoot } from '../../core/root-resolver.js';
@@ -31,11 +33,15 @@ export async function runTaskStart(taskId, targetDirArg, options = {}) {
31
33
  }
32
34
  if (!foundTask) {
33
35
  ctx.abort();
34
- 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`));
35
41
  process.exitCode = 1;
36
42
  return;
37
43
  }
38
- const refusal = options.force ? null : startRefusal(plan, foundTask, agent);
44
+ const refusal = options.force ? null : startRefusal(withArchivedAsDone(plan), foundTask, agent);
39
45
  if (refusal) {
40
46
  ctx.abort();
41
47
  console.error(pc.red(`\n✖ ${refusal}\n`));
@@ -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
+ }
@@ -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);
@@ -0,0 +1,40 @@
1
+ import { type MasterPlan } from '../scanner/types.js';
2
+ export declare const DEFAULT_SUITE_TIMEOUT_MS = 600000;
3
+ /** An expected "no": the message is written for the person running the command. */
4
+ export declare class ArchiveRefusal extends Error {
5
+ }
6
+ /** Seams so tests can make the read-back, the removal or the plan save fail. */
7
+ export interface ArchiveDeps {
8
+ readBack?: (file: string) => string;
9
+ save?: (planPath: string, plan: MasterPlan) => boolean;
10
+ remove?: (file: string) => void;
11
+ }
12
+ export interface ArchiveRequest {
13
+ ids: string[];
14
+ summaries: Record<string, string>;
15
+ check?: string;
16
+ timeout?: number;
17
+ }
18
+ export interface ArchiveResult {
19
+ archived: Array<{
20
+ id: string;
21
+ file: string;
22
+ taskCount: number;
23
+ }>;
24
+ activeMilestoneId: string;
25
+ commit: string | null;
26
+ warnings: string[];
27
+ }
28
+ export interface RestoreResult {
29
+ id: string;
30
+ taskCount: number;
31
+ warnings: string[];
32
+ }
33
+ /** Ids of tasks moved out to archive files; they count as existing, completed tasks. */
34
+ export declare function archivedTaskIds(plan: MasterPlan): string[];
35
+ /** The plan as dependency checks should see it: archived tasks appear as completed. */
36
+ export declare function withArchivedAsDone(plan: MasterPlan): MasterPlan;
37
+ /** Refuses without side effects, runs the suite once outside the lock, then re-checks and commits. */
38
+ export declare function archiveMilestones(targetDir: string, request: ArchiveRequest, deps?: ArchiveDeps): Promise<ArchiveResult>;
39
+ /** Puts the tasks back; the archive file goes only after the plan is safely saved. */
40
+ export declare function restoreMilestone(targetDir: string, id: string, deps?: ArchiveDeps): Promise<RestoreResult>;