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