@njeybe/nativ 2.5.0 → 2.6.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 +38 -0
- package/README.md +29 -4
- package/dist/commands/init.js +2 -10
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +25 -0
- package/dist/commands/session.d.ts +22 -0
- package/dist/commands/session.js +106 -0
- package/dist/commands/task/task-add.js +1 -1
- package/dist/commands/task/task-common.js +6 -6
- package/dist/commands/task/task-escalate.js +8 -10
- package/dist/commands/task/task-lifecycle.d.ts +7 -1
- package/dist/commands/task/task-lifecycle.js +30 -5
- package/dist/commands/task/task-query.d.ts +2 -0
- package/dist/commands/task/task-query.js +27 -12
- package/dist/commands/task/task-reclaim.d.ts +20 -0
- package/dist/commands/task/task-reclaim.js +87 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +1 -0
- package/dist/commands/triage.js +3 -3
- package/dist/commands/validate.js +1 -0
- package/dist/commands/verify.js +21 -5
- package/dist/core/enforcement.js +18 -17
- package/dist/core/lock-manager.d.ts +6 -0
- package/dist/core/lock-manager.js +50 -41
- package/dist/core/notify.d.ts +38 -0
- package/dist/core/notify.js +133 -0
- package/dist/core/profiles.js +7 -16
- package/dist/core/session-memory.d.ts +42 -0
- package/dist/core/session-memory.js +121 -0
- package/dist/core/setup/pm-playbook.d.ts +5 -0
- package/dist/core/setup/pm-playbook.js +26 -0
- package/dist/core/setup/session-context.js +35 -2
- package/dist/core/setup/setup-planner.js +2 -2
- package/dist/core/setup/types.d.ts +2 -0
- package/dist/core/setup/types.js +5 -1
- package/dist/core/task-claims.d.ts +15 -0
- package/dist/core/task-claims.js +32 -0
- package/dist/core/tier1-liaison.d.ts +1 -0
- package/dist/core/tier1-liaison.js +25 -27
- package/dist/core/verifier/output-digest.d.ts +19 -0
- package/dist/core/verifier/output-digest.js +48 -0
- package/dist/db/data-engine.js +33 -32
- package/dist/db/introspector.js +61 -60
- package/dist/db/sqlite.d.ts +3 -0
- package/dist/db/sqlite.js +10 -0
- package/dist/governor/circuit-breaker.js +12 -0
- package/dist/governor/patch-applier.d.ts +1 -0
- package/dist/governor/patch-applier.js +3 -7
- package/dist/index.js +29 -6
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +53 -14
- package/dist/providers/claude-api.js +3 -0
- package/dist/providers/gemini.js +4 -1
- package/dist/providers/registry.d.ts +6 -2
- package/dist/providers/registry.js +28 -16
- package/dist/providers/types.d.ts +4 -1
- package/dist/runner/agent-supervisor.js +2 -2
- package/dist/runner/cli-config.js +1 -1
- package/dist/scanner/types.d.ts +5 -1
- package/dist/scanner/types.js +7 -0
- package/dist/server/routes/task-routes.d.ts +2 -0
- package/dist/server/routes/triage-routes.js +3 -18
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +1 -1
- package/plugin/agents/explorer.md +1 -1
- package/plugin/agents/verifier.md +2 -2
- package/plugin/agents/worker.md +5 -7
- package/plugin/skills/nativ/SKILL.md +36 -49
- package/templates/AGENTS.md +36 -49
- package/templates/CLAUDE.md +1 -41
- package/templates/claude-agents/architect.md +1 -1
- package/templates/claude-agents/explorer.md +1 -1
- package/templates/claude-agents/verifier.md +2 -2
- package/templates/claude-agents/worker.md +5 -7
- package/templates/dot-ai/api_contracts.json +66 -66
- package/templates/dot-ai/context.md +38 -38
- package/templates/dot-ai/db_schema.json +39 -39
- package/templates/dot-ai/subagents/architect.md +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.0
|
|
4
|
+
|
|
5
|
+
### Improved: token usage and autonomy
|
|
6
|
+
|
|
7
|
+
- **Leaner directives.** `AGENTS.md` (-17%), `CLAUDE.md` (-20%) and the worker and verifier agents are shorter. Subagents no longer re-read `AGENTS.md`, which already reaches them through `CLAUDE.md`. The human-communication rules now live in one place.
|
|
8
|
+
- **Compact JSON.** `task next`, `task list` and `triage --json` print single-line JSON. `recommendedContractSlice` is a bare file list.
|
|
9
|
+
- **Brief task list over MCP.** `nativ_task_list` returns a short summary per task; pass `full: true` for every field.
|
|
10
|
+
- **Self-healing ladder.** `AGENTS.md` and `CLAUDE.md` tell agents to retry, patch additively, then run `nativ triage --all --apply --json`; only `REQUIRE_HUMAN_DECISION` items, destructive changes, credentials and repeated failures reach the human.
|
|
11
|
+
- **Resumable sessions.** The SessionStart orientation lists blocked tasks and pending escalations.
|
|
12
|
+
- **Budget guard.** `tests/test-token-budget.mjs` fails when an always-loaded file outgrows its token budget.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Self-healing guide.** `docs/self-healing.md` explains the hands-free loop end to end: claims, the escalation ladder, reclaiming stuck tasks and notifications.
|
|
17
|
+
- **`nativ_validate` MCP tool.** A read-only contract check for MCP hosts that returns only the problems and a verdict, not every passing line.
|
|
18
|
+
- **Notifications when nativ needs you.** A `notify` webhook (Slack, Discord, ntfy, any JSON endpoint) or command in `.nativ/config.json` is called when triage returns `REQUIRE_HUMAN_DECISION` or the circuit breaker blocks a task, once per escalation. Webhooks must be https. `nativ notify test` sends a sample. Failures are recorded, never fatal.
|
|
19
|
+
- **`nativ_triage` MCP tool.** Hosts that only speak MCP can now run the self-healing step: it evaluates pending escalations (or one), applies sandbox-proven additive fixes, and returns the decision card for items a person must decide. The directives and session orientation point to it.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Tasks are claimed, not just started.** `task start` refuses a completed or blocked task and one whose dependencies are unfinished, and records `claimedBy` (`--agent` or `NATIV_AGENT_ID`) and `claimedAt`. Another agent cannot start a task with a fresh claim; a claim older than 4 hours can be taken over. Complete and block release the claim. A person can override with `--force`. `nativ_task_start` takes an optional `agent`.
|
|
24
|
+
- **Stuck tasks recover.** `nativ task reclaim` (and `nativ_task_reclaim`) returns `in_progress` tasks with a claim older than 4 hours (`--older-than`) to pending, so a crashed agent no longer leaves a task stuck forever. `--dry-run` reports only. The SessionStart orientation lists stale tasks, and the self-healing ladder covers them.
|
|
25
|
+
- **Provider calls survive network blips.** A network failure or a 5xx answer from Claude API or Gemini is now a `TRANSIENT` error, retried on the same provider twice (about 0.5s and 1s, with jitter) before the chain moves to the next provider. Rejected requests and timeouts are not retried.
|
|
26
|
+
- **Shared JSON files are written safely.** Contract patches, `.nativ/config.json`, task unlocks, provider cooldowns and runner records now go through one atomic writer that retries Windows sharing violations; contract patches no longer fail silently on Windows. Config and unlock changes are read-modify-written under a lock, so parallel writers keep each other's keys, and a config file that does not parse is left untouched instead of being wiped. Studio reports a failed triage-config save instead of claiming success.
|
|
27
|
+
- **Locks wait as long everywhere and leave no empty files.** The plan lock now waits about 5s like the other locks (it gave up after about 0.4s), and locking a file that does not exist no longer creates an empty one that JSON readers cannot parse.
|
|
28
|
+
- **Unknown escalation types are refused.** `nativ task escalate --type` used to turn a typo into `architectural_ambiguity` silently; it now lists the valid types and writes nothing. The CLI and MCP share one list.
|
|
29
|
+
- **`nativ init` writes the architect guide.** It copied a fixed list of eight guides and skipped `architect.md`; it now copies every guide in the templates. `nativ validate` checks the architect guide when present.
|
|
30
|
+
- **Windows paths in `.mcp.json` count as machine-specific on every OS.** A `C:/...` command was treated as portable when setup ran on Linux or macOS.
|
|
31
|
+
- **Continuous integration.** `.github/workflows/ci.yml` builds, checks the plugin sync and runs every suite on Ubuntu (Node 20 and 22) and Windows (Node 22) for pushes to `main` and `dev` and for pull requests. The test runner clears `CI` and the headless overrides for each suite, so tests behave the same on a laptop and in CI.
|
|
32
|
+
- **SQLite on Node 20 explains itself.** SQLite support uses `node:sqlite`, which ships with Node.js 22.5+. On Node 20 the database commands and Studio now say so plainly instead of failing with an unknown-module error. The contract-generator tests no longer depend on `node --test` glob support, which Node 20 lacks.
|
|
33
|
+
- **`npm test` runs every suite.** `tests/run-all.mjs` keeps going after a failure and lists every failed suite at the end.
|
|
34
|
+
|
|
35
|
+
## 2.5.1
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Added missing `architect` role guide (`templates/dot-ai/subagents/architect.md` and `.ai/subagents/architect.md`). `nativ doctor` no longer warns about the architect role having no guide.
|
|
40
|
+
|
|
3
41
|
## 2.5.0
|
|
4
42
|
|
|
5
43
|
### Added
|
package/README.md
CHANGED
|
@@ -152,6 +152,7 @@ Every command takes an optional `[targetDir]` (default: the current directory) a
|
|
|
152
152
|
| [`task list`](#nativ-task-list) / `tasks` | List tasks with filters | You, Agent |
|
|
153
153
|
| [`task add`](#nativ-task-add) | Add a task | Architect |
|
|
154
154
|
| [`task start`](#nativ-task-start) | Mark a task in progress | Agent |
|
|
155
|
+
| [`task reclaim`](#nativ-task-reclaim) | Free tasks left in progress by a stopped agent | You, Agent |
|
|
155
156
|
| [`task complete`](#nativ-task-complete) | Complete a task through the gatekeeper | Agent |
|
|
156
157
|
| [`task block`](#nativ-task-block) | Block a task with a reason | Agent |
|
|
157
158
|
| [`task escalate`](#nativ-task-escalate) | Escalate a contract gap to the architect | Agent |
|
|
@@ -162,6 +163,7 @@ Every command takes an optional `[targetDir]` (default: the current directory) a
|
|
|
162
163
|
| [`learn list`](#nativ-learn-list) | List lessons | You, Agent |
|
|
163
164
|
| [`learn approve` / `reject`](#nativ-learn-approve--reject) | Decide on a lesson | *You only* |
|
|
164
165
|
| [`triage`](#nativ-triage) | Evaluate pending escalations; decision cards for you | You |
|
|
166
|
+
| [`notify test`](#nativ-notify-test) | Check the webhook or command that tells you a decision is waiting | You |
|
|
165
167
|
| [`db status` / `inspect` / `diff`](#nativ-db-status--inspect--diff) | Masked, structure-only database checks | You, Agent |
|
|
166
168
|
| [`db sync`](#nativ-db-sync) | Copy a live schema into `.ai/db_schema.json` | *You only* |
|
|
167
169
|
| [`worktree create` / `list` / `merge` / `remove`](#nativ-worktree) | Isolated worktrees for parallel tasks | You, Agent |
|
|
@@ -361,10 +363,18 @@ nativ task add "Appointment list" -a frontend --complexity standard \
|
|
|
361
363
|
#### `nativ task start`
|
|
362
364
|
|
|
363
365
|
```bash
|
|
364
|
-
nativ task start <taskId> [targetDir]
|
|
366
|
+
nativ task start <taskId> [targetDir] [--agent <id>] [--force]
|
|
365
367
|
```
|
|
366
368
|
|
|
367
|
-
|
|
369
|
+
Claims the task, marks it `in_progress` and records the baseline the test-integrity guard compares against. The task must be pending and its dependencies completed; a blocked task goes through `nativ triage` first, and only a person can override with `--force`. The claim records `claimedBy` (`--agent` or `$NATIV_AGENT_ID`) and `claimedAt`, so a parallel agent cannot take a task another agent holds. Running `task start` again on your own task refreshes the claim; a claim older than 4 hours counts as abandoned.
|
|
370
|
+
|
|
371
|
+
#### `nativ task reclaim`
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
nativ task reclaim [taskId] [targetDir] [--older-than <hours>] [--dry-run] [--json]
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Puts `in_progress` tasks whose agent stopped (a crash or a closed session) back to `pending`. Without a task id it reclaims every claim older than `--older-than` hours (default 4). Tasks started before claims were recorded have no claim time; name them to reclaim them. A fresh claim is refused unless a person passes `--force`. The SessionStart orientation lists stale tasks. The previous agent may have left partial changes, so review the working tree before restarting.
|
|
368
378
|
|
|
369
379
|
#### `nativ task complete`
|
|
370
380
|
|
|
@@ -517,7 +527,7 @@ If the lesson changed while you were reviewing it, nothing is decided and you ar
|
|
|
517
527
|
|
|
518
528
|
### Escalations and triage
|
|
519
529
|
|
|
520
|
-
A worker that finds a contract gap escalates (`nativ task escalate`) and stops. The gap is then resolved by the architect, or first assessed by triage.
|
|
530
|
+
A worker that finds a contract gap escalates (`nativ task escalate`) and stops. The gap is then resolved by the architect, or first assessed by triage. [docs/self-healing.md](docs/self-healing.md) walks through the whole hands-free loop: claims, retries, triage, reclaiming stuck tasks and notifications.
|
|
521
531
|
|
|
522
532
|
#### `nativ triage`
|
|
523
533
|
|
|
@@ -536,6 +546,16 @@ nativ triage [escalationId] [targetDir] [options]
|
|
|
536
546
|
|
|
537
547
|
When an escalation is settled, its resolution notes reach the next worker on that task as `priorEscalations`.
|
|
538
548
|
|
|
549
|
+
#### `nativ notify test`
|
|
550
|
+
|
|
551
|
+
nativ can tell you when it needs you: when triage returns `REQUIRE_HUMAN_DECISION`, and when the circuit breaker blocks a task. Each escalation is announced once. Add a webhook (Slack, Discord, ntfy or any JSON endpoint) or a local command to `.nativ/config.json`, which git ignores, so a webhook token stays on your machine:
|
|
552
|
+
|
|
553
|
+
```json
|
|
554
|
+
{ "notify": { "webhook": "https://hooks.slack.com/services/...", "events": ["human_decision", "circuit_breaker_tripped"] } }
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The webhook receives JSON with `text` (Slack, ntfy), `content` (Discord), and the task, escalation, question, options and next step as fields. Webhooks must use `https://` (plain `http://` only to `localhost`). A `"command"` receives the same JSON on stdin and in `NATIV_NOTIFY_JSON`, e.g. `"command": "notify-send nativ \"$(jq -r .text)\""`. `nativ notify test` sends a sample; delivery failures never stop the workflow and are recorded in `.nativ/notify-state.json`.
|
|
558
|
+
|
|
539
559
|
[Back to contents](#contents)
|
|
540
560
|
|
|
541
561
|
---
|
|
@@ -665,12 +685,15 @@ Written by `setup`; every key is optional.
|
|
|
665
685
|
| `codeStyle.maxCommentLines` | `2` | Longest allowed comment block |
|
|
666
686
|
| `providers` | see [Providers](#providers) | Provider order per role: `architect`, `pm`, `worker`, `verifier`, `triage` |
|
|
667
687
|
| `models` | provider default | Model per provider |
|
|
688
|
+
| `notify` | off | `webhook`, `command` and `events` for human notifications. See [`nativ notify test`](#nativ-notify-test) |
|
|
668
689
|
|
|
669
690
|
### Environment variables
|
|
670
691
|
|
|
671
692
|
| Variable | Used for |
|
|
672
693
|
| :--- | :--- |
|
|
673
694
|
| `NATIV_ROLE` | Session role; `NATIV_ROLE=architect` lets a whole session write `.ai/` |
|
|
695
|
+
| `NATIV_AGENT_ID` | Who claims a task on `task start`, so parallel agents never take the same task |
|
|
696
|
+
| `NATIV_NOTIFY_WEBHOOK`, `NATIV_NOTIFY_COMMAND` | Override the `notify` webhook or command from `.nativ/config.json` |
|
|
674
697
|
| `ANTHROPIC_API_KEY` | The `claude-api` provider and the Studio native runner |
|
|
675
698
|
| `GEMINI_API_KEY` or `GOOGLE_API_KEY` | The `gemini` provider |
|
|
676
699
|
| `NATIV_DEV_DATABASE_URL`, `DEV_DATABASE_URL`, `DATABASE_URL` | Dev database for `nativ db`, checked in that order, read from your `.env*` files. MongoDB (`MONGODB_URI`) and Firestore variants work too, and names declared in `.env.example` are checked first |
|
|
@@ -777,7 +800,9 @@ Files nativ wrote carry a marker, so `update` and `doctor` can tell untouched fi
|
|
|
777
800
|
| Tools | Purpose |
|
|
778
801
|
| :--- | :--- |
|
|
779
802
|
| `nativ_task_next`, `nativ_task_list`, `nativ_task_add` | Find and add work |
|
|
780
|
-
| `nativ_task_start`, `nativ_task_complete`, `nativ_task_block` | Move a task through its lifecycle (complete always runs the gatekeeper) |
|
|
803
|
+
| `nativ_task_start`, `nativ_task_complete`, `nativ_task_block`, `nativ_task_reclaim` | Move a task through its lifecycle (complete always runs the gatekeeper; reclaim frees stale claims) |
|
|
804
|
+
| `nativ_validate` | Check the `.ai/` contracts, plan and role guides; returns only problems and a verdict |
|
|
805
|
+
| `nativ_triage` | Self-heal: evaluate pending escalations, apply sandbox-proven safe fixes, return decision cards for the rest |
|
|
781
806
|
| `nativ_task_escalate`, `nativ_task_propose_patch` | Escalate a gap or propose a governed patch |
|
|
782
807
|
| `nativ_learn_propose` | Propose a lesson; you approve it in a terminal |
|
|
783
808
|
| `nativ_verify`, `nativ_status`, `nativ_bench`, `nativ_test_gen` | Verification, progress, benchmarks, test generation |
|
package/dist/commands/init.js
CHANGED
|
@@ -96,16 +96,8 @@ export async function runInit(targetDirArg, options = {}) {
|
|
|
96
96
|
console.log(pc.green('✔ Created .ai/master_plan.json'));
|
|
97
97
|
}
|
|
98
98
|
// 7. Write Sub-agent specifications
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
'frontend.md',
|
|
102
|
-
'database.md',
|
|
103
|
-
'qa-tester.md',
|
|
104
|
-
'flutter-developer.md',
|
|
105
|
-
'devops-agent.md',
|
|
106
|
-
'security-auditor.md',
|
|
107
|
-
'db-migration.md',
|
|
108
|
-
];
|
|
99
|
+
const guidesDir = path.join(templatesDir, 'dot-ai/subagents');
|
|
100
|
+
const subagentFiles = fs.existsSync(guidesDir) ? fs.readdirSync(guidesDir).filter((f) => f.endsWith('.md')).sort() : [];
|
|
109
101
|
for (const file of subagentFiles) {
|
|
110
102
|
const src = path.join(templatesDir, 'dot-ai/subagents', file);
|
|
111
103
|
const dest = path.join(subagentsDir, file);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { resolveProjectRoot } from '../core/root-resolver.js';
|
|
3
|
+
import { loadNotifyConfig, notifyHuman } from '../core/notify.js';
|
|
4
|
+
/** `nativ notify test`: sends a sample notification so a person can check the webhook or command works. */
|
|
5
|
+
export async function runNotifyTest(targetDirArg) {
|
|
6
|
+
const root = resolveProjectRoot(targetDirArg);
|
|
7
|
+
const config = loadNotifyConfig(root);
|
|
8
|
+
if (!config) {
|
|
9
|
+
console.log(pc.yellow('\nNotifications are off. Add to .nativ/config.json:'));
|
|
10
|
+
console.log(pc.white(' { "notify": { "webhook": "https://hooks.slack.com/services/..." } }'));
|
|
11
|
+
console.log(pc.dim(' or "command": "notify-send nativ \\"$(jq -r .text)\\"", or set NATIV_NOTIFY_WEBHOOK / NATIV_NOTIFY_COMMAND.\n'));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const result = await notifyHuman(root, { event: config.events[0], taskId: 'task-test', summary: 'This is a test notification from `nativ notify test`', question: 'Did it arrive?', options: ['Yes', 'No'] }, { force: true });
|
|
16
|
+
for (const e of result.errors)
|
|
17
|
+
console.error(pc.red(`✖ ${e}`));
|
|
18
|
+
if (result.sent) {
|
|
19
|
+
const via = [config.webhook && 'webhook', config.command && 'command'].filter(Boolean).join(' and ');
|
|
20
|
+
console.log(pc.green(`\n✔ Test notification sent via ${via}. Events: ${config.events.join(', ')}.\n`));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SessionCheckpoint, SessionSummaryEntry } from '../core/session-memory.js';
|
|
2
|
+
export interface SessionSaveOptions {
|
|
3
|
+
summary: string;
|
|
4
|
+
decision?: string[];
|
|
5
|
+
obstacle?: string[];
|
|
6
|
+
next?: string[];
|
|
7
|
+
activeFiles?: string;
|
|
8
|
+
task?: string;
|
|
9
|
+
role?: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionListOptions {
|
|
14
|
+
limit?: number;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface SessionGetOptions {
|
|
18
|
+
json?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function runSessionSave(targetDirArg?: string, options?: SessionSaveOptions): Promise<SessionCheckpoint>;
|
|
21
|
+
export declare function runSessionList(targetDirArg?: string, options?: SessionListOptions): Promise<SessionSummaryEntry[]>;
|
|
22
|
+
export declare function runSessionGet(sessionId?: string, targetDirArg?: string, options?: SessionGetOptions): Promise<SessionCheckpoint | null>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { saveSessionCheckpoint, getLatestSession, getSession, listSessions, } from '../core/session-memory.js';
|
|
4
|
+
export async function runSessionSave(targetDirArg, options = { summary: '' }) {
|
|
5
|
+
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
6
|
+
const files = options.activeFiles
|
|
7
|
+
? options.activeFiles.split(',').map((f) => f.trim()).filter(Boolean)
|
|
8
|
+
: undefined;
|
|
9
|
+
const checkpoint = saveSessionCheckpoint(targetDir, {
|
|
10
|
+
sessionId: options.sessionId,
|
|
11
|
+
summary: options.summary,
|
|
12
|
+
decisions: options.decision,
|
|
13
|
+
obstacles: options.obstacle,
|
|
14
|
+
nextSteps: options.next,
|
|
15
|
+
activeFiles: files,
|
|
16
|
+
taskId: options.task,
|
|
17
|
+
role: options.role,
|
|
18
|
+
});
|
|
19
|
+
if (options.json) {
|
|
20
|
+
console.log(JSON.stringify(checkpoint, null, 2));
|
|
21
|
+
return checkpoint;
|
|
22
|
+
}
|
|
23
|
+
console.log(pc.bold(pc.green(`\n✔ Saved Session Checkpoint: `)) + pc.yellow(`[${checkpoint.sessionId}]`));
|
|
24
|
+
console.log(pc.bold(' Summary: ') + pc.white(checkpoint.summary));
|
|
25
|
+
if (checkpoint.taskId) {
|
|
26
|
+
console.log(pc.bold(' Task: ') + pc.yellow(checkpoint.taskId));
|
|
27
|
+
}
|
|
28
|
+
if (checkpoint.role) {
|
|
29
|
+
console.log(pc.bold(' Role: ') + pc.magenta(checkpoint.role));
|
|
30
|
+
}
|
|
31
|
+
if (checkpoint.decisions && checkpoint.decisions.length > 0) {
|
|
32
|
+
console.log(pc.bold(' Decisions: ') + pc.dim(checkpoint.decisions.join(' | ')));
|
|
33
|
+
}
|
|
34
|
+
if (checkpoint.obstacles && checkpoint.obstacles.length > 0) {
|
|
35
|
+
console.log(pc.bold(' Obstacles: ') + pc.red(checkpoint.obstacles.join(' | ')));
|
|
36
|
+
}
|
|
37
|
+
if (checkpoint.nextSteps && checkpoint.nextSteps.length > 0) {
|
|
38
|
+
console.log(pc.bold(' Next Steps: ') + pc.cyan(checkpoint.nextSteps.join(' | ')));
|
|
39
|
+
}
|
|
40
|
+
if (checkpoint.activeFiles && checkpoint.activeFiles.length > 0) {
|
|
41
|
+
console.log(pc.bold(' Files: ') + pc.dim(checkpoint.activeFiles.join(', ')));
|
|
42
|
+
}
|
|
43
|
+
console.log();
|
|
44
|
+
return checkpoint;
|
|
45
|
+
}
|
|
46
|
+
export async function runSessionList(targetDirArg, options = {}) {
|
|
47
|
+
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
48
|
+
const sessions = listSessions(targetDir, options.limit || 10);
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
51
|
+
return sessions;
|
|
52
|
+
}
|
|
53
|
+
console.log(pc.bold(pc.cyan('\nSession Memory Checkpoints (.ai/memory/sessions/)')));
|
|
54
|
+
console.log(pc.dim(`Workspace: ${targetDir}\n`));
|
|
55
|
+
if (sessions.length === 0) {
|
|
56
|
+
console.log(pc.dim(' No saved sessions found.\n'));
|
|
57
|
+
console.log(pc.dim(' Save current progress with: ') + pc.white('nativ session save --summary "..."\n'));
|
|
58
|
+
return sessions;
|
|
59
|
+
}
|
|
60
|
+
for (const s of sessions) {
|
|
61
|
+
const roleTag = s.role ? pc.magenta(`[${s.role}] `) : '';
|
|
62
|
+
const taskTag = s.taskId ? pc.yellow(`[${s.taskId}] `) : '';
|
|
63
|
+
console.log(` ${pc.bold(pc.yellow(s.sessionId))} ${pc.dim(s.timestamp)} ${roleTag}${taskTag}\n ${pc.white(s.summary)}`);
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
return sessions;
|
|
67
|
+
}
|
|
68
|
+
export async function runSessionGet(sessionId, targetDirArg, options = {}) {
|
|
69
|
+
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
70
|
+
const session = sessionId ? getSession(targetDir, sessionId) : getLatestSession(targetDir);
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log(JSON.stringify(session, null, 2));
|
|
73
|
+
return session;
|
|
74
|
+
}
|
|
75
|
+
if (!session) {
|
|
76
|
+
console.log(pc.yellow(`\nNo session found${sessionId ? ` matching [${sessionId}]` : ''}.\n`));
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
console.log(pc.bold(pc.cyan(`\nSession Details: `)) + pc.yellow(`[${session.sessionId}]`));
|
|
80
|
+
console.log(pc.bold(' Timestamp: ') + pc.dim(session.timestamp));
|
|
81
|
+
if (session.taskId)
|
|
82
|
+
console.log(pc.bold(' Task ID: ') + pc.yellow(session.taskId));
|
|
83
|
+
if (session.role)
|
|
84
|
+
console.log(pc.bold(' Role: ') + pc.magenta(session.role));
|
|
85
|
+
console.log(pc.bold(' Summary: ') + pc.white(session.summary));
|
|
86
|
+
if (session.decisions && session.decisions.length > 0) {
|
|
87
|
+
console.log(pc.bold('\n Key Decisions:'));
|
|
88
|
+
for (const d of session.decisions)
|
|
89
|
+
console.log(pc.dim(' • ') + pc.white(d));
|
|
90
|
+
}
|
|
91
|
+
if (session.obstacles && session.obstacles.length > 0) {
|
|
92
|
+
console.log(pc.bold('\n Obstacles / Blockers:'));
|
|
93
|
+
for (const o of session.obstacles)
|
|
94
|
+
console.log(pc.red(' ✖ ') + pc.white(o));
|
|
95
|
+
}
|
|
96
|
+
if (session.nextSteps && session.nextSteps.length > 0) {
|
|
97
|
+
console.log(pc.bold('\n Next Steps:'));
|
|
98
|
+
for (const n of session.nextSteps)
|
|
99
|
+
console.log(pc.cyan(' → ') + pc.white(n));
|
|
100
|
+
}
|
|
101
|
+
if (session.activeFiles && session.activeFiles.length > 0) {
|
|
102
|
+
console.log(pc.bold('\n Active Files: ') + pc.dim(session.activeFiles.join(', ')));
|
|
103
|
+
}
|
|
104
|
+
console.log();
|
|
105
|
+
return session;
|
|
106
|
+
}
|
|
@@ -148,7 +148,7 @@ export async function runTaskAdd(title, targetDirArg, options = {}) {
|
|
|
148
148
|
plan.overallStatus = 'in_progress';
|
|
149
149
|
const result = { task, milestoneId: milestone.id, milestoneName: milestone.name, createdMilestone, reopenedMilestone };
|
|
150
150
|
if (options.json) {
|
|
151
|
-
console.log(JSON.stringify(result
|
|
151
|
+
console.log(JSON.stringify(result));
|
|
152
152
|
return result;
|
|
153
153
|
}
|
|
154
154
|
const track = fastPath ? pc.magenta(' [fast path]') : '';
|
|
@@ -24,22 +24,22 @@ export function getRoleGuide(assignedSubagent) {
|
|
|
24
24
|
export function getRecommendedContractSlice(assignedSubagent) {
|
|
25
25
|
const sub = assignedSubagent.toLowerCase();
|
|
26
26
|
if (sub.includes('frontend') || sub.includes('flutter')) {
|
|
27
|
-
return '.ai/ui_specs.md
|
|
27
|
+
return '.ai/ui_specs.md + .ai/api_contracts.json';
|
|
28
28
|
}
|
|
29
29
|
if (sub.includes('backend')) {
|
|
30
|
-
return '.ai/api_contracts.json
|
|
30
|
+
return '.ai/api_contracts.json + .ai/db_schema.json';
|
|
31
31
|
}
|
|
32
32
|
if (sub.includes('database') || sub.includes('db-migration')) {
|
|
33
|
-
return '.ai/db_schema.json
|
|
33
|
+
return '.ai/db_schema.json';
|
|
34
34
|
}
|
|
35
35
|
if (sub.includes('devops')) {
|
|
36
|
-
return '.ai/context.md
|
|
36
|
+
return '.ai/context.md';
|
|
37
37
|
}
|
|
38
38
|
if (sub.includes('security')) {
|
|
39
|
-
return '
|
|
39
|
+
return 'targetFiles + dependency manifests + .ai/context.md';
|
|
40
40
|
}
|
|
41
41
|
if (sub.includes('qa')) {
|
|
42
|
-
return '
|
|
42
|
+
return 'targetFiles + .ai/api_contracts.json';
|
|
43
43
|
}
|
|
44
44
|
return '.ai/context.md';
|
|
45
45
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
+
import { ESCALATION_TYPES, } from '../../scanner/types.js';
|
|
3
4
|
import { ContractGovernor, appendEscalation, } from '../../governor/index.js';
|
|
4
5
|
import { withPlanLock } from '../../core/lock-manager.js';
|
|
5
6
|
import { getPlanPath } from './task-common.js';
|
|
6
7
|
export async function runTaskEscalate(taskId, targetDirArg, options = {}) {
|
|
8
|
+
const requestedType = options.type?.trim();
|
|
9
|
+
if (requestedType && !ESCALATION_TYPES.includes(requestedType)) {
|
|
10
|
+
console.error(pc.red(`\n✖ Unknown escalation type "${requestedType}". Use one of: ${ESCALATION_TYPES.join(', ')}\n`));
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const escType = (requestedType || 'architectural_ambiguity');
|
|
7
15
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
8
16
|
return withPlanLock(planPath, (plan, ctx) => {
|
|
9
17
|
let foundTask = null;
|
|
@@ -20,16 +28,6 @@ export async function runTaskEscalate(taskId, targetDirArg, options = {}) {
|
|
|
20
28
|
process.exitCode = 1;
|
|
21
29
|
return;
|
|
22
30
|
}
|
|
23
|
-
const validTypes = [
|
|
24
|
-
'contract_drift',
|
|
25
|
-
'schema_flaw',
|
|
26
|
-
'missing_credential',
|
|
27
|
-
'dependency_conflict',
|
|
28
|
-
'architectural_ambiguity',
|
|
29
|
-
];
|
|
30
|
-
const escType = (options.type && validTypes.includes(options.type))
|
|
31
|
-
? options.type
|
|
32
|
-
: 'architectural_ambiguity';
|
|
33
31
|
const affectedContracts = options.affected
|
|
34
32
|
? options.affected.split(',').map((s) => s.trim())
|
|
35
33
|
: ['.ai/db_schema.json', '.ai/api_contracts.json'];
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { TaskUnlockOptions, TaskCompleteOptions } from './task-common.js';
|
|
2
|
-
export
|
|
2
|
+
export interface TaskStartOptions {
|
|
3
|
+
/** Who claims the task; defaults to NATIV_AGENT_ID. */
|
|
4
|
+
agent?: string;
|
|
5
|
+
/** Human override: start a blocked task or one whose dependencies are unfinished. */
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function runTaskStart(taskId: string, targetDirArg?: string, options?: TaskStartOptions): Promise<void | null>;
|
|
3
9
|
export declare function runTaskUnlock(taskId: string, targetDirArg?: string, options?: TaskUnlockOptions): Promise<void>;
|
|
4
10
|
export declare function runTaskComplete(taskId: string, targetDirArg?: string, options?: TaskCompleteOptions): Promise<void>;
|
|
5
11
|
export declare function runTaskBlock(taskId: string, reason: string, targetDirArg?: string): Promise<void | null>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { runTaskVerification } from '../../core/verifier.js';
|
|
3
|
+
import { digestFailure } from '../../core/verifier/output-digest.js';
|
|
3
4
|
import { CircuitBreaker, TestIntegrityGuard, resolveGitHead, closeEscalationsForTask, } from '../../governor/index.js';
|
|
4
5
|
import { loadPlan, withPlanLock } from '../../core/lock-manager.js';
|
|
5
6
|
import { recordTaskStart, recordTaskComplete } from '../../core/telemetry.js';
|
|
6
7
|
import { addTaskUnlock, removeTaskUnlock } from '../../core/enforcement.js';
|
|
7
8
|
import { refuseHeadless } from '../../core/human-gate.js';
|
|
9
|
+
import { resolveAgentId, startRefusal } from '../../core/task-claims.js';
|
|
8
10
|
import { getPlanPath, } from './task-common.js';
|
|
9
|
-
export async function runTaskStart(taskId, targetDirArg) {
|
|
11
|
+
export async function runTaskStart(taskId, targetDirArg, options = {}) {
|
|
12
|
+
if (options.force && refuseHeadless('nativ task start --force'))
|
|
13
|
+
return;
|
|
10
14
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
15
|
+
const agent = resolveAgentId(options.agent);
|
|
11
16
|
return withPlanLock(planPath, async (plan, ctx) => {
|
|
12
17
|
let foundTask = null;
|
|
13
18
|
let foundMilestone = null;
|
|
@@ -25,7 +30,20 @@ export async function runTaskStart(taskId, targetDirArg) {
|
|
|
25
30
|
process.exitCode = 1;
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
33
|
+
const refusal = options.force ? null : startRefusal(plan, foundTask, agent);
|
|
34
|
+
if (refusal) {
|
|
35
|
+
ctx.abort();
|
|
36
|
+
console.error(pc.red(`\n✖ ${refusal}\n`));
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const resumed = foundTask.status === 'in_progress';
|
|
28
41
|
foundTask.status = 'in_progress';
|
|
42
|
+
foundTask.claimedAt = new Date().toISOString();
|
|
43
|
+
if (agent)
|
|
44
|
+
foundTask.claimedBy = agent;
|
|
45
|
+
else if (!resumed)
|
|
46
|
+
delete foundTask.claimedBy;
|
|
29
47
|
if (foundMilestone.status === 'pending') {
|
|
30
48
|
foundMilestone.status = 'in_progress';
|
|
31
49
|
}
|
|
@@ -134,17 +152,20 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
134
152
|
if (failedPhase)
|
|
135
153
|
console.error(pc.yellow(` Phase: ${failedPhase.name}`));
|
|
136
154
|
console.error(pc.yellow(` Command: \`${vResult.command}\``));
|
|
137
|
-
|
|
155
|
+
const digest = digestFailure(targetDir, taskId, vResult);
|
|
156
|
+
if (digest.stderr) {
|
|
138
157
|
console.error(pc.red('\n--- stderr ---'));
|
|
139
|
-
console.error(pc.red(
|
|
158
|
+
console.error(pc.red(digest.stderr));
|
|
140
159
|
}
|
|
141
|
-
if (
|
|
160
|
+
if (digest.stdout) {
|
|
142
161
|
console.error(pc.dim('\n--- stdout ---'));
|
|
143
|
-
console.error(pc.dim(
|
|
162
|
+
console.error(pc.dim(digest.stdout));
|
|
144
163
|
}
|
|
145
164
|
if (vResult.error && !vResult.stderr?.includes(vResult.error)) {
|
|
146
165
|
console.error(pc.red(`\nError: ${vResult.error}`));
|
|
147
166
|
}
|
|
167
|
+
if (digest.logFile)
|
|
168
|
+
console.error(pc.dim(`\n Full output: ${digest.logFile}`));
|
|
148
169
|
console.error(pc.yellow(`\n⚠ Task remains '${foundTask.status}'. Fix the issue and retry:`));
|
|
149
170
|
console.error(pc.white(` nativ task complete ${taskId}`));
|
|
150
171
|
console.error(pc.dim(' (or pass --no-verify to bypass verification check)\n'));
|
|
@@ -189,6 +210,8 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
189
210
|
return;
|
|
190
211
|
}
|
|
191
212
|
taskToComplete.status = 'completed';
|
|
213
|
+
delete taskToComplete.claimedBy;
|
|
214
|
+
delete taskToComplete.claimedAt;
|
|
192
215
|
if (options.notes) {
|
|
193
216
|
taskToComplete.notes = options.notes;
|
|
194
217
|
}
|
|
@@ -246,6 +269,8 @@ export async function runTaskBlock(taskId, reason, targetDirArg) {
|
|
|
246
269
|
}
|
|
247
270
|
foundTask.status = 'blocked';
|
|
248
271
|
foundTask.notes = reason.trim();
|
|
272
|
+
delete foundTask.claimedBy;
|
|
273
|
+
delete foundTask.claimedAt;
|
|
249
274
|
console.log(pc.yellow(`\n⚠ Task [${pc.bold(taskId)}] marked as `) + pc.red('✖ blocked'));
|
|
250
275
|
console.log(pc.dim(` Reason: ${foundTask.notes}\n`));
|
|
251
276
|
});
|
|
@@ -4,6 +4,8 @@ export declare function runTaskList(targetDirArg?: string, options?: {
|
|
|
4
4
|
milestone?: string;
|
|
5
5
|
fastPath?: boolean;
|
|
6
6
|
json?: boolean;
|
|
7
|
+
/** JSON only: id, title, status, role, dependencies and files; leaves out long prose fields. */
|
|
8
|
+
brief?: boolean;
|
|
7
9
|
}): Promise<void>;
|
|
8
10
|
export declare function runTaskNext(targetDirArg?: string, options?: {
|
|
9
11
|
json?: boolean;
|
|
@@ -48,15 +48,17 @@ export async function runTaskList(targetDirArg, options = {}) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
if (options.json) {
|
|
51
|
-
const jsonOutput = results.map(({ task, milestoneId, milestoneName, isAvailable }) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
const jsonOutput = results.map(({ task, milestoneId, milestoneName, isAvailable }) => options.brief
|
|
52
|
+
? briefTask(task, milestoneId, isAvailable)
|
|
53
|
+
: {
|
|
54
|
+
...task,
|
|
55
|
+
milestoneId,
|
|
56
|
+
milestoneName,
|
|
57
|
+
isAvailable,
|
|
58
|
+
roleGuide: getRoleGuide(task.assignedSubagent),
|
|
59
|
+
contractSlice: getRecommendedContractSlice(task.assignedSubagent),
|
|
60
|
+
});
|
|
61
|
+
console.log(JSON.stringify(jsonOutput));
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
console.log(pc.bold(pc.cyan(`\n📋 nativ Tasks: ${plan.projectName}`)));
|
|
@@ -108,6 +110,19 @@ export async function runTaskList(targetDirArg, options = {}) {
|
|
|
108
110
|
pc.gray(`${pendingCount} pending`) + pc.dim(', ') +
|
|
109
111
|
pc.red(`${blockedCount} blocked`) + '\n');
|
|
110
112
|
}
|
|
113
|
+
function briefTask(t, milestoneId, isAvailable) {
|
|
114
|
+
return {
|
|
115
|
+
id: t.id,
|
|
116
|
+
title: t.title,
|
|
117
|
+
status: t.status,
|
|
118
|
+
role: t.assignedSubagent,
|
|
119
|
+
milestoneId,
|
|
120
|
+
isAvailable,
|
|
121
|
+
...(t.dependencies?.length ? { dependencies: t.dependencies } : {}),
|
|
122
|
+
...(t.targetFiles?.length ? { targetFiles: t.targetFiles } : {}),
|
|
123
|
+
...(t.status === 'blocked' && t.notes ? { notes: t.notes } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
111
126
|
/** What a worker needs from a lesson; status and audit fields stay in .ai/learnings.json. */
|
|
112
127
|
function toTaskLearning({ id, insight, details, files }) {
|
|
113
128
|
return { id, insight, ...(details ? { details } : {}), ...(files ? { files } : {}) };
|
|
@@ -168,7 +183,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
168
183
|
}
|
|
169
184
|
if (!activeMilestone) {
|
|
170
185
|
if (options.json) {
|
|
171
|
-
console.log(JSON.stringify({ status: 'all_milestones_completed', activeTask: null }
|
|
186
|
+
console.log(JSON.stringify({ status: 'all_milestones_completed', activeTask: null }));
|
|
172
187
|
}
|
|
173
188
|
else {
|
|
174
189
|
console.log(pc.bold(pc.green('\n🎉 All milestones and tasks are completed!\n')));
|
|
@@ -196,7 +211,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
196
211
|
milestoneId: activeMilestone.id,
|
|
197
212
|
milestoneName: activeMilestone.name,
|
|
198
213
|
activeTask: null,
|
|
199
|
-
}
|
|
214
|
+
}));
|
|
200
215
|
}
|
|
201
216
|
else {
|
|
202
217
|
if (hasBlocked) {
|
|
@@ -233,7 +248,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
233
248
|
...(learnings.length ? { learnings: learnings.map(toTaskLearning) } : {}),
|
|
234
249
|
...(priorEscalations.length ? { priorEscalations } : {}),
|
|
235
250
|
}
|
|
236
|
-
}
|
|
251
|
+
}));
|
|
237
252
|
return;
|
|
238
253
|
}
|
|
239
254
|
const fastTag = targetTask.fastPath ? pc.cyan(' [FAST-PATH PRIORITY]') : '';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface TaskReclaimOptions {
|
|
2
|
+
/** Claims at least this many hours old are abandoned (default 4). */
|
|
3
|
+
olderThan?: number;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
/** Human override: reclaim a named task even though its claim is fresh. */
|
|
7
|
+
force?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ReclaimedTask {
|
|
10
|
+
id: string;
|
|
11
|
+
claimedBy?: string;
|
|
12
|
+
ageHours: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface TaskReclaimResult {
|
|
15
|
+
reclaimed: ReclaimedTask[];
|
|
16
|
+
/** in_progress tasks with no claim time (started before claims existed): reclaim them by id. */
|
|
17
|
+
unknownAge: string[];
|
|
18
|
+
}
|
|
19
|
+
/** Puts in_progress tasks whose agent stopped (crash, closed session) back to pending so another agent can take them. */
|
|
20
|
+
export declare function runTaskReclaim(taskId?: string, targetDirArg?: string, options?: TaskReclaimOptions): Promise<TaskReclaimResult | null>;
|