@njeybe/nativ 2.8.0 → 2.9.0
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 +21 -0
- package/README.md +26 -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/setup.d.ts +1 -0
- package/dist/commands/setup.js +1 -0
- package/dist/commands/task/task-add.js +49 -6
- 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 +40 -2
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +1 -1
- 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/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/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 +68 -39
- 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 +19 -0
- package/dist/index.js +33 -4
- package/dist/mcp/server.js +9 -2
- package/dist/scanner/types.d.ts +4 -0
- package/dist/scanner/types.js +9 -0
- package/dist/server/routes/task-routes.d.ts +1 -0
- 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/skills/nativ/SKILL.md +11 -8
- package/templates/AGENTS.md +11 -8
- package/templates/claude-agents/architect.md +14 -7
- package/templates/claude-agents/verifier.md +16 -14
- package/templates/dot-ai/subagents/qa-tester.md +14 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.9.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Clean checks.** A task's check runs in a throwaway copy of the repo with only the files git ships and a minimal environment. Defaults: prototype off, solo on, enterprise on. Use `--isolation none|clean` (or `--clean`) on `nativ verify` and `nativ task complete`. Under enterprise, `--isolation none` on `task complete` needs an interactive terminal. Config keys: `verifyIsolation`, `verifyIsolationFailure`, `verifyEnv`, `verifyLinks`.
|
|
8
|
+
- **Portability warnings.** When a check passes only on your machine, a failure in the clean copy warns "Passes only on this machine". nativ names the test lines that read gitignored files or spawn `nativ`.
|
|
9
|
+
- **Check types.** Tasks take a `verificationKind` (unit, integration, static-config, docs, probe, manual) through `nativ task add --kind`, plus `--notes`. `--kind docs` defaults its check to `nativ check docs`. A manual task needs notes.
|
|
10
|
+
- **Skipped-check policy.** `skippedCheck` allows a skip in prototype, warns in solo and blocks in enterprise. A skip passes quietly only for a manual task with notes. Tasks with no kind only warn.
|
|
11
|
+
- **`nativ check docs <files>`.** Checks backticked file paths, `<PLACEHOLDER>` tokens against the file the paragraph names, setting names used in assignments or as variable references, and nativ subcommands and flags. Supports `--json`. Skip lines with `<!-- nativ-check-ignore -->`, or wrap them in the `-start` and `-end` pair.
|
|
12
|
+
- **Fail-loud config rule.** Code-shape reports "config" findings for reads of settings that silently fall back to empty, such as `?? ''`, `|| ''`, `?? null`, Python `os.getenv('X', '')` and unchecked Go `os.Getenv`. Setting: `codeStyle.configFallback` (off, warn or block; default warn). Opt out per line with `nativ-allow-fallback`.
|
|
13
|
+
- **nativ stays out of git.** `nativ init`, `setup` and `update` add a managed `# >>> nativ (managed) >>>` block to the project's `.gitignore`, listing only files nativ generated. `--git-visibility hidden|shared` sets it (default hidden); shared keeps only secret and machine files ignored. `nativ doctor` warns when the block is missing (`--fix` adds it) and prints the `git rm --cached` command for nativ files already committed. It never runs that command.
|
|
14
|
+
- **`nativ task add --new-milestone <name>`.** Starts the next milestone when the others are done. `--force` works only from the CLI.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Verification environment.** Checks get an allowlisted environment, and a timed-out check stops its whole process tree. `CI` inside a check no longer bypasses human-only steps. A check command may not set `NATIV_CI_OVERRIDE` or `NATIV_HEADLESS_OVERRIDE`; `nativ validate` and `task add` reject it.
|
|
19
|
+
- **`--accept` keeps each value whole.** A `|` no longer splits a criterion.
|
|
20
|
+
- **Guides.** AGENTS.md says required settings fail loudly and tests read only tracked files. The verifier has a "Weak verification" step and a config-fallback checklist item. The architect says what a real check looks like for each kind.
|
|
21
|
+
|
|
22
|
+
<!-- nativ-check-ignore-start -->
|
|
3
23
|
## 2.8.0
|
|
4
24
|
|
|
5
25
|
### Changed
|
|
@@ -262,3 +282,4 @@ nativ is now role-based and Claude-first. The roles (architect, project manager,
|
|
|
262
282
|
## 1.0.0
|
|
263
283
|
|
|
264
284
|
Initial release.
|
|
285
|
+
<!-- nativ-check-ignore-end -->
|
package/README.md
CHANGED
|
@@ -193,6 +193,7 @@ nativ init [targetDir] [-f, --force]
|
|
|
193
193
|
| :--- | :--- |
|
|
194
194
|
| `-f, --force` | Overwrite existing specification and directive files |
|
|
195
195
|
| `--design <taste-v1|taste-v2>` | Set the design profile in `.ai/ui_specs.md` and install the taste-skill bundle |
|
|
196
|
+
| `--git-visibility <mode>` | `hidden` (default) adds a managed `.gitignore` block listing only nativ's generated files; `shared` ignores only secret and machine files |
|
|
196
197
|
|
|
197
198
|
#### `nativ design check`
|
|
198
199
|
|
|
@@ -260,6 +261,8 @@ nativ doctor [targetDir] [options]
|
|
|
260
261
|
| `--no-deep` | Skip asking Claude Code (`claude mcp list`) whether the server connects |
|
|
261
262
|
| `--json` | Output the report as JSON |
|
|
262
263
|
|
|
264
|
+
It also warns when the managed `.gitignore` block is missing (`--fix` adds it) and prints the `git rm --cached` command for nativ files already committed. It never runs that command.
|
|
265
|
+
|
|
263
266
|
Run it after updating Claude Code or nativ.
|
|
264
267
|
|
|
265
268
|
#### `nativ update`
|
|
@@ -365,8 +368,11 @@ nativ task add <title> [targetDir] [options]
|
|
|
365
368
|
| `--deps <ids>` | Comma-separated dependency task IDs |
|
|
366
369
|
| `--spec-refs <refs>` | Contract anchors, e.g. `ui_specs.md#appointment-list` or `api_contracts.json#GET /appointments` |
|
|
367
370
|
| `--complexity <level>` | `simple`, `standard` or `complex` (routes the worker's model) |
|
|
368
|
-
| `--accept <text>` | "Done when" line; repeatable
|
|
371
|
+
| `--accept <text>` | "Done when" line; repeatable, each value kept whole |
|
|
369
372
|
| `--fast-path` | Route to the fast-path milestone for quick fixes |
|
|
373
|
+
| `--kind <kind>` | Verification kind: `unit`, `integration`, `static-config`, `docs`, `probe` or `manual`. `docs` defaults the check to `nativ check docs` |
|
|
374
|
+
| `--notes <text>` | Task notes; required with `--kind manual` |
|
|
375
|
+
| `--new-milestone <name>` | Create the next milestone with this name and add the task to it. Refused while another planned milestone is open, unless `--force` |
|
|
370
376
|
| `--json` | Output the created task as JSON |
|
|
371
377
|
|
|
372
378
|
```bash
|
|
@@ -404,6 +410,8 @@ nativ task complete <taskId> [targetDir] [options]
|
|
|
404
410
|
| :--- | :--- |
|
|
405
411
|
| `-n, --notes <notes>` | Completion notes |
|
|
406
412
|
| `--timeout <ms>` | Verification timeout (default 120000) |
|
|
413
|
+
| `--isolation <mode>` | Run the check in place (`none`) or in a clean copy of the repo (`clean`). Accepts `none` or `clean`. See [Verification](#verification) |
|
|
414
|
+
| `--clean` | Short for `--isolation clean` |
|
|
407
415
|
| `--no-verify` | Skip verification. *You only*: needs an interactive terminal, is denied to agents, and the MCP tool refuses it |
|
|
408
416
|
|
|
409
417
|
#### `nativ task block`
|
|
@@ -475,6 +483,8 @@ nativ verify --all --json
|
|
|
475
483
|
| `-a, --all` | Every completed task |
|
|
476
484
|
| `-m, --milestone <id>` | Every task in a milestone |
|
|
477
485
|
| `--timeout <ms>` | Per command (default 120000) |
|
|
486
|
+
| `--isolation <mode>` | In place, or in a clean copy with only the files git ships and a minimal environment |
|
|
487
|
+
| `--clean` | Short for `--isolation clean` |
|
|
478
488
|
| `--json` | Output as JSON |
|
|
479
489
|
|
|
480
490
|
**What runs, in order** (the same for `verify`, the `task complete` gatekeeper and Studio dispatch):
|
|
@@ -492,6 +502,16 @@ nativ verify --all --json
|
|
|
492
502
|
|
|
493
503
|
With no phases configured, verification is just the task's command and the style check. The settings always come from the main checkout, never a task worktree's copy. If `.nativ/config.json` cannot be read, or a phase has no `name` or `run`, verification fails with a clear message instead of skipping the checks.
|
|
494
504
|
|
|
505
|
+
#### `nativ check docs`
|
|
506
|
+
|
|
507
|
+
Checks markdown files for drift: backticked file paths, `<PLACEHOLDER>` tokens against the file the paragraph names, setting names used in assignments or as variable references, and nativ subcommands and flags.
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
nativ check docs README.md CHANGELOG.md [--json]
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Skip a line with `<!-- nativ-check-ignore -->`, or wrap a block in `<!-- nativ-check-ignore-start -->` and `<!-- nativ-check-ignore-end -->`.
|
|
514
|
+
|
|
495
515
|
[Back to contents](#contents)
|
|
496
516
|
|
|
497
517
|
---
|
|
@@ -722,6 +742,11 @@ Written by `setup`; every key is optional.
|
|
|
722
742
|
| `codeStyle.mode` | `warn` | `warn`, `block` or `off` for the line-length and comment check |
|
|
723
743
|
| `codeStyle.maxLineLength` | `120` | Longest allowed line |
|
|
724
744
|
| `codeStyle.maxCommentLines` | `2` | Longest allowed comment block |
|
|
745
|
+
| `codeStyle.configFallback` | `warn` | `off`, `warn` or `block` for reads of settings that silently fall back to empty. Opt out per line with `nativ-allow-fallback` |
|
|
746
|
+
| `verifyIsolation` | `none` (`clean` in solo and enterprise) | Run checks in a clean copy of the repo. See [`nativ verify`](#verification) |
|
|
747
|
+
| `verifyIsolationFailure` | `warn` | `warn` or `block` when a check fails in the clean copy but passes in place |
|
|
748
|
+
| `verifyEnv` | none | Extra environment variable names a check may read |
|
|
749
|
+
| `verifyLinks` | `node_modules`, `.venv`, `venv` | Files linked into the clean copy, for example dependency folders |
|
|
725
750
|
| `providers` | see [Providers](#providers) | Provider order per role: `architect`, `pm`, `worker`, `verifier`, `triage` |
|
|
726
751
|
| `models` | provider default | Model per provider |
|
|
727
752
|
| `notify` | off | `webhook`, `command` and `events` for human notifications. See [`nativ notify test`](#nativ-notify-test) |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveProjectRoot } from '../core/root-resolver.js';
|
|
4
|
+
import { listProjectEnvNames } from '../core/env-reads.js';
|
|
5
|
+
import { checkDocText } from '../core/checks/docs-check.js';
|
|
6
|
+
function specOf(cmd) {
|
|
7
|
+
const flags = cmd.options.flatMap((o) => [o.long, o.short].filter((f) => !!f));
|
|
8
|
+
const subs = {};
|
|
9
|
+
for (const sub of cmd.commands)
|
|
10
|
+
subs[sub.name()] = specOf(sub);
|
|
11
|
+
return { flags, subs };
|
|
12
|
+
}
|
|
13
|
+
export async function runCheckDocs(files, options) {
|
|
14
|
+
const root = resolveProjectRoot(process.cwd());
|
|
15
|
+
const { createProgram } = await import('../index.js');
|
|
16
|
+
const ctx = { root, envNames: new Set(listProjectEnvNames(root)), program: specOf(createProgram()) };
|
|
17
|
+
const findings = [];
|
|
18
|
+
for (const f of files) {
|
|
19
|
+
const full = path.resolve(process.cwd(), f);
|
|
20
|
+
if (!fs.existsSync(full)) {
|
|
21
|
+
console.error(`Cannot read ${f}`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
findings.push(...checkDocText(fs.readFileSync(full, 'utf8'), path.relative(root, full), ctx));
|
|
26
|
+
}
|
|
27
|
+
if (options.json)
|
|
28
|
+
console.log(JSON.stringify({ ok: findings.length === 0, findings }, null, 2));
|
|
29
|
+
else if (findings.length === 0)
|
|
30
|
+
console.log('Docs check: no problems found.');
|
|
31
|
+
else
|
|
32
|
+
for (const x of findings)
|
|
33
|
+
console.log(`${x.file}:${x.line} ${x.kind} ${x.message}`);
|
|
34
|
+
if (findings.length > 0)
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -100,7 +100,11 @@ export function collectChecks(root, options = {}) {
|
|
|
100
100
|
add('cli-runs', version.ok ? 'ok' : 'fail', version.ok ? `\`${cli.join(' ')}\` runs (${version.output})` : `\`${cli.join(' ')}\` does not run: ${version.output || 'no output'}. The hooks and MCP server would fail.`);
|
|
101
101
|
// Generated assets: whatever setup would still change is drift
|
|
102
102
|
for (const change of plan.changes) {
|
|
103
|
-
if (change.
|
|
103
|
+
if (change.path === '.gitignore' && change.action !== 'unchanged') {
|
|
104
|
+
const what = change.action === 'created' ? 'has no' : 'has an outdated';
|
|
105
|
+
add('gitignore', 'warn', `.gitignore ${what} nativ block, so generated files may be committed by accident`, true);
|
|
106
|
+
}
|
|
107
|
+
else if (change.action === 'created')
|
|
104
108
|
add(`asset:${change.path}`, 'fail', `${change.path} is missing`, true);
|
|
105
109
|
else if (change.action === 'updated')
|
|
106
110
|
add(`asset:${change.path}`, 'warn', `${change.path} is out of date${change.detail ? ` (${change.detail})` : ''}`, true);
|
|
@@ -109,6 +113,8 @@ export function collectChecks(root, options = {}) {
|
|
|
109
113
|
else if (change.detail)
|
|
110
114
|
add(`asset:${change.path}`, 'info', `${change.path}: ${change.detail}`);
|
|
111
115
|
}
|
|
116
|
+
if (plan.gitRmCommand)
|
|
117
|
+
add('gitignore-tracked', 'info', `nativ files are already tracked by git. To stop tracking them, run: ${plan.gitRmCommand}`);
|
|
112
118
|
if (!plan.changes.some((c) => c.action === 'created' || c.action === 'updated'))
|
|
113
119
|
add('assets', 'ok', 'MCP server, hooks, permissions, agents and directives are in place');
|
|
114
120
|
// Claude Code's own view (opt-in: it connects to every configured MCP server)
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -175,7 +175,7 @@ export async function runInit(targetDirArg, options = {}) {
|
|
|
175
175
|
}
|
|
176
176
|
// 10. Claude Code configuration: MCP server, hooks, permissions, agents, AGENTS.md. Merges; never overwrites.
|
|
177
177
|
console.log(pc.yellow('Configuring Claude Code (MCP server, hooks, agents)...'));
|
|
178
|
-
const setup = await runSetup(targetDir, { quiet: true });
|
|
178
|
+
const setup = await runSetup(targetDir, { quiet: true, gitVisibility: options.gitVisibility });
|
|
179
179
|
if (setup) {
|
|
180
180
|
printSetupResult(setup);
|
|
181
181
|
}
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function runSetup(targetDirArg, options = {}) {
|
|
|
32
32
|
command: options.command,
|
|
33
33
|
enforcement: options.enforcement,
|
|
34
34
|
force: options.force,
|
|
35
|
+
gitVisibility: options.gitVisibility,
|
|
35
36
|
dryRun: options.dryRun,
|
|
36
37
|
};
|
|
37
38
|
const result = applySetup(targetDir, setupOptions);
|
|
@@ -1,5 +1,7 @@
|
|
|
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';
|
|
4
6
|
import { getPlanPath, getRoleGuide, } from './task-common.js';
|
|
5
7
|
export const FAST_PATH_MILESTONE_NAME = 'Fast-Path Tasks';
|
|
@@ -43,6 +45,15 @@ function failTaskAdd(code, message, json) {
|
|
|
43
45
|
process.exitCode = 1;
|
|
44
46
|
return null;
|
|
45
47
|
}
|
|
48
|
+
/** Default check for a docs task: `nativ check docs` on its markdown files, or '' without any. */
|
|
49
|
+
function defaultDocsCheck(targetFiles) {
|
|
50
|
+
const docs = targetFiles.filter((f) => /\.md$/i.test(f));
|
|
51
|
+
if (!docs.length)
|
|
52
|
+
return '';
|
|
53
|
+
const root = process.cwd();
|
|
54
|
+
const cli = cliString(configuredInvocation(root) ?? resolveCliInvocation({}));
|
|
55
|
+
return `${cli} check docs ${docs.join(' ')}`;
|
|
56
|
+
}
|
|
46
57
|
/** Resolves a milestone by exact ID, exact name, or a unique partial name (all case-insensitive). */
|
|
47
58
|
function findMilestone(plan, query) {
|
|
48
59
|
const q = query.trim().toLowerCase();
|
|
@@ -86,7 +97,29 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
|
|
|
86
97
|
const fastPath = options.fastPath === true;
|
|
87
98
|
let milestone;
|
|
88
99
|
let createdMilestone = false;
|
|
89
|
-
|
|
100
|
+
const newName = options.newMilestone?.trim();
|
|
101
|
+
if (options.newMilestone !== undefined && !newName) {
|
|
102
|
+
ctx.abort();
|
|
103
|
+
return failTaskAdd('INVALID_MILESTONE_NAME', 'A new milestone needs a name', options.json);
|
|
104
|
+
}
|
|
105
|
+
if (newName && (options.milestone || fastPath)) {
|
|
106
|
+
ctx.abort();
|
|
107
|
+
return failTaskAdd('CONFLICTING_OPTIONS', '--new-milestone cannot be used with --milestone or --fast-path', options.json);
|
|
108
|
+
}
|
|
109
|
+
const stillOpen = plan.milestones.filter((m) => !m.fastPath && m.status !== 'completed');
|
|
110
|
+
if (newName && stillOpen.length && !options.force) {
|
|
111
|
+
ctx.abort();
|
|
112
|
+
const ids = stillOpen.map((m) => m.id).join(', ');
|
|
113
|
+
return failTaskAdd('MILESTONE_STILL_OPEN', `Milestone ${ids} is not completed yet. Finish it first, or pass --force`, options.json);
|
|
114
|
+
}
|
|
115
|
+
if (newName) {
|
|
116
|
+
milestone = { id: generateNextMilestoneId(plan), name: newName, status: 'in_progress', tasks: [] };
|
|
117
|
+
plan.milestones.push(milestone);
|
|
118
|
+
plan.activeMilestoneId = milestone.id;
|
|
119
|
+
plan.overallStatus = 'in_progress';
|
|
120
|
+
createdMilestone = true;
|
|
121
|
+
}
|
|
122
|
+
else if (options.milestone) {
|
|
90
123
|
const found = findMilestone(plan, options.milestone);
|
|
91
124
|
if (Array.isArray(found)) {
|
|
92
125
|
ctx.abort();
|
|
@@ -116,23 +149,33 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
|
|
|
116
149
|
}
|
|
117
150
|
const specRefs = [...new Set(splitList(options.specRefs))];
|
|
118
151
|
const acceptanceCriteria = (Array.isArray(options.accept) ? options.accept : options.accept ? [options.accept] : [])
|
|
119
|
-
.flatMap((item) => item.split('|'))
|
|
120
152
|
.map((item) => item.trim())
|
|
121
153
|
.filter(Boolean);
|
|
122
154
|
const complexity = options.complexity?.trim().toLowerCase();
|
|
155
|
+
const kind = options.kind?.trim().toLowerCase();
|
|
156
|
+
const notes = (options.notes ?? '').trim();
|
|
157
|
+
if (kind === 'manual' && !notes) {
|
|
158
|
+
ctx.abort();
|
|
159
|
+
return failTaskAdd('MANUAL_NEEDS_NOTES', 'A manual check needs --notes saying what the person must confirm', options.json);
|
|
160
|
+
}
|
|
161
|
+
const files = splitList(options.files);
|
|
162
|
+
let verify = (options.verify ?? '').trim();
|
|
163
|
+
if (kind === 'docs' && !verify)
|
|
164
|
+
verify = defaultDocsCheck(files);
|
|
123
165
|
const task = {
|
|
124
166
|
id: generateNextTaskId(plan),
|
|
125
167
|
title: cleanTitle,
|
|
126
168
|
description: (options.description ?? '').trim(),
|
|
127
169
|
assignedSubagent: agent,
|
|
128
170
|
dependencies: deps,
|
|
129
|
-
targetFiles:
|
|
171
|
+
targetFiles: files,
|
|
130
172
|
status: 'pending',
|
|
131
|
-
verificationCommand:
|
|
132
|
-
notes
|
|
173
|
+
verificationCommand: verify,
|
|
174
|
+
notes,
|
|
133
175
|
...(fastPath ? { fastPath: true } : {}),
|
|
134
176
|
...(specRefs.length ? { specRefs } : {}),
|
|
135
177
|
...(complexity ? { complexity: complexity } : {}),
|
|
178
|
+
...(kind ? { verificationKind: kind } : {}),
|
|
136
179
|
...(acceptanceCriteria.length ? { acceptanceCriteria } : {}),
|
|
137
180
|
};
|
|
138
181
|
const problems = validateMasterPlanTask(task);
|
|
@@ -154,7 +197,7 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
|
|
|
154
197
|
const track = fastPath ? pc.magenta(' [fast path]') : '';
|
|
155
198
|
console.log(pc.green(`\n✔ Task [${pc.bold(task.id)}] added to [${milestone.id}] ${milestone.name}`) + track);
|
|
156
199
|
if (createdMilestone)
|
|
157
|
-
console.log(pc.cyan(` Created fast-path milestone [${milestone.id}]`));
|
|
200
|
+
console.log(pc.cyan(` Created ${fastPath ? 'fast-path ' : ''}milestone [${milestone.id}]`));
|
|
158
201
|
if (reopenedMilestone)
|
|
159
202
|
console.log(pc.yellow(` Reopened completed milestone [${milestone.id}]`));
|
|
160
203
|
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,10 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import pc from 'picocolors';
|
|
3
|
+
import { parseIsolation } from '../verify.js';
|
|
4
|
+
import { resolveActiveProfileSettings } from '../../core/profiles.js';
|
|
5
|
+
import { resolveMainRoot } from '../../core/root-resolver.js';
|
|
2
6
|
import { runTaskVerification } from '../../core/verifier.js';
|
|
7
|
+
import { evaluateSkip } from '../../core/verifier/skip-policy.js';
|
|
3
8
|
import { digestFailure } from '../../core/verifier/output-digest.js';
|
|
4
9
|
import { CircuitBreaker, TestIntegrityGuard, resolveGitHead, closeEscalationsForTask, } from '../../governor/index.js';
|
|
5
10
|
import { loadPlan, withPlanLock } from '../../core/lock-manager.js';
|
|
@@ -91,6 +96,23 @@ export async function runTaskUnlock(taskId, targetDirArg, options = {}) {
|
|
|
91
96
|
export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
92
97
|
if (options.skipVerify && refuseHeadless('nativ task complete --no-verify'))
|
|
93
98
|
return;
|
|
99
|
+
let isolation;
|
|
100
|
+
try {
|
|
101
|
+
isolation = parseIsolation(options);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
console.error(pc.red(`
|
|
105
|
+
✖ ${e.message}
|
|
106
|
+
`));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const root = resolveMainRoot(path.resolve(targetDirArg || process.cwd()));
|
|
111
|
+
const profile = resolveActiveProfileSettings(root);
|
|
112
|
+
if (isolation === 'none' &&
|
|
113
|
+
profile.verifyIsolationFailure === 'block' &&
|
|
114
|
+
refuseHeadless('nativ task complete --isolation none'))
|
|
115
|
+
return;
|
|
94
116
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
95
117
|
const plan = loadPlan(planPath);
|
|
96
118
|
if (!plan) {
|
|
@@ -145,7 +167,7 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
145
167
|
// ── Verification Gatekeeper ────────────────────────────────────────────────
|
|
146
168
|
let vResult = null;
|
|
147
169
|
if (!options.skipVerify) {
|
|
148
|
-
vResult = await runTaskVerification(foundTask, { cwd: targetDir, timeout: options.timeout });
|
|
170
|
+
vResult = await runTaskVerification(foundTask, { cwd: targetDir, timeout: options.timeout, isolation });
|
|
149
171
|
if (!vResult.success) {
|
|
150
172
|
console.error(pc.red(`\n✖ Task [${pc.bold(taskId)}] verification FAILED with exit code ${vResult.exitCode}:`));
|
|
151
173
|
const failedPhase = vResult.phases?.find((p) => !p.success);
|
|
@@ -173,11 +195,27 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
173
195
|
return;
|
|
174
196
|
}
|
|
175
197
|
if (vResult.skipped) {
|
|
198
|
+
const skip = evaluateSkip(foundTask, profile.skippedCheck);
|
|
199
|
+
if (skip.action === 'block') {
|
|
200
|
+
console.error(pc.red(`
|
|
201
|
+
✖ Task [${pc.bold(taskId)}] cannot complete: ${skip.message}`));
|
|
202
|
+
console.error(pc.yellow(` Task remains '${foundTask.status}'.
|
|
203
|
+
`));
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (skip.action === 'warn')
|
|
208
|
+
console.log(pc.yellow(`
|
|
209
|
+
⚠ ${skip.message}`));
|
|
176
210
|
console.log(pc.dim(`\n○ Verification skipped: no verification command specified for [${taskId}]`));
|
|
177
211
|
}
|
|
178
212
|
else {
|
|
179
213
|
const elapsed = (vResult.durationMs / 1000).toFixed(2);
|
|
180
|
-
|
|
214
|
+
const used = vResult.isolation ?? 'none';
|
|
215
|
+
console.log(pc.green(`\n✔ Verification passed (${elapsed}s, isolation: ${used}): `) +
|
|
216
|
+
pc.yellow(`\`${foundTask.verificationCommand}\``));
|
|
217
|
+
for (const w of vResult.warnings ?? [])
|
|
218
|
+
console.log(pc.yellow(` ⚠ ${w}`));
|
|
181
219
|
}
|
|
182
220
|
}
|
|
183
221
|
else {
|
|
@@ -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'));
|
|
@@ -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[];
|