@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.
- package/CHANGELOG.md +20 -0
- package/README.md +36 -5
- package/bin/cli.js +1 -1
- package/dist/commands/milestone.d.ts +10 -0
- package/dist/commands/milestone.js +84 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +39 -8
- package/dist/commands/task/task-add.js +13 -3
- package/dist/commands/task/task-lifecycle.js +8 -2
- package/dist/commands/task/task-query.js +4 -2
- package/dist/commands/task/task-show.d.ts +9 -0
- package/dist/commands/task/task-show.js +74 -0
- package/dist/commands/validate.js +24 -0
- package/dist/core/milestone-archive.d.ts +40 -0
- package/dist/core/milestone-archive.js +299 -0
- package/dist/core/setup/pm-playbook.d.ts +1 -1
- package/dist/core/setup/pm-playbook.js +7 -1
- package/dist/core/setup/session-context.d.ts +9 -1
- package/dist/core/setup/session-context.js +24 -2
- package/dist/core/verifier/task-verifier.js +25 -0
- package/dist/core/verifier/verify-env.js +4 -1
- package/dist/index.js +14 -3
- package/dist/mcp/server.js +18 -0
- package/dist/runner/agent-supervisor.js +4 -1
- package/dist/scanner/types.d.ts +14 -0
- package/dist/scanner/types.js +3 -1
- package/dist/server/routes/task-routes.d.ts +1 -0
- package/dist/server/routes/task-routes.js +13 -5
- package/dist/server/studio-ui/shell/model.js +16 -9
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/worker.md +3 -3
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/skills/nativ/SKILL.md +4 -3
- package/templates/AGENTS.md +4 -3
- 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
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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>;
|