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