@njeybe/nativ 2.3.1 → 2.4.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 +44 -1
- package/README.md +676 -190
- package/dist/commands/db.js +3 -0
- package/dist/commands/doctor.d.ts +2 -1
- package/dist/commands/doctor.js +4 -0
- package/dist/commands/init.js +1 -0
- package/dist/commands/learn.d.ts +19 -0
- package/dist/commands/learn.js +121 -0
- package/dist/commands/session.d.ts +22 -0
- package/dist/commands/session.js +106 -0
- package/dist/commands/task.js +34 -7
- package/dist/commands/verify.js +6 -0
- package/dist/core/code-shape.js +4 -2
- package/dist/core/enforcement.d.ts +1 -0
- package/dist/core/enforcement.js +22 -4
- package/dist/core/harness-integrity.d.ts +15 -0
- package/dist/core/harness-integrity.js +198 -0
- package/dist/core/human-gate.d.ts +7 -0
- package/dist/core/human-gate.js +16 -0
- package/dist/core/learnings.d.ts +63 -0
- package/dist/core/learnings.js +227 -0
- package/dist/core/root-resolver.d.ts +5 -0
- package/dist/core/root-resolver.js +46 -0
- package/dist/core/session-memory.d.ts +42 -0
- package/dist/core/session-memory.js +121 -0
- package/dist/core/setup-assets.d.ts +1 -1
- package/dist/core/setup-assets.js +5 -2
- package/dist/core/verifier.d.ts +36 -0
- package/dist/core/verifier.js +92 -4
- package/dist/db/introspector.js +59 -59
- package/dist/governor/store.d.ts +18 -0
- package/dist/governor/store.js +53 -0
- package/dist/index.js +41 -0
- package/dist/mcp/server.js +12 -0
- package/dist/runner/agent-supervisor.d.ts +16 -0
- package/dist/runner/agent-supervisor.js +63 -8
- package/dist/scanner/types.d.ts +1 -0
- package/dist/server/studio-ui/shell/model.js +3 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/agents/architect.md +3 -3
- package/plugin/agents/explorer.md +49 -0
- package/plugin/agents/worker.md +2 -1
- package/plugin/skills/nativ/SKILL.md +15 -0
- package/templates/AGENTS.md +15 -0
- package/templates/CLAUDE.md +32 -32
- package/templates/GEMINI.md +2 -2
- package/templates/claude-agents/architect.md +3 -3
- package/templates/claude-agents/explorer.md +49 -0
- package/templates/claude-agents/worker.md +2 -1
- 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/frontend.md +33 -0
- package/templates/dot-ai/ui_specs.md +17 -0
package/dist/commands/db.js
CHANGED
|
@@ -7,6 +7,7 @@ import { disconnectedStatus, introspectDatabase } from '../db/introspector.js';
|
|
|
7
7
|
import { diffSchemas, loadContractTables } from '../db/diff.js';
|
|
8
8
|
import { DEFAULT_STUDIO_PORT, startStudioServer, writeContractSchema } from '../server/studio-server.js';
|
|
9
9
|
import { renderStudioHtml } from '../server/studio-ui.js';
|
|
10
|
+
import { refuseHeadless } from '../core/human-gate.js';
|
|
10
11
|
/**
|
|
11
12
|
* `nativ db` command handlers.
|
|
12
13
|
* Air-gap: raw connection URLs are resolved in-process and never printed; all output
|
|
@@ -343,6 +344,8 @@ export async function runDbSync(targetDirArg, options = {}) {
|
|
|
343
344
|
console.log(pc.yellow('Dry run: no files were changed. Re-run with --yes to write .ai/db_schema.json.\n'));
|
|
344
345
|
return;
|
|
345
346
|
}
|
|
347
|
+
if (refuseHeadless('nativ db sync --yes'))
|
|
348
|
+
return;
|
|
346
349
|
const filePath = writeContractSchema(targetDir, result.status, result.tables);
|
|
347
350
|
const count = result.tables.length;
|
|
348
351
|
const isCollection = result.status.entityType === 'collection';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SetupOptions } from '../core/setup-assets.js';
|
|
2
2
|
import type { ProviderDeps } from '../providers/index.js';
|
|
3
|
-
export type CheckStatus
|
|
3
|
+
export type { CheckStatus } from '../core/harness-integrity.js';
|
|
4
|
+
import type { CheckStatus } from '../core/harness-integrity.js';
|
|
4
5
|
export interface DoctorCheck {
|
|
5
6
|
id: string;
|
|
6
7
|
status: CheckStatus;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { loadEnforcementMode, parseJsonLoose } from '../core/enforcement.js';
|
|
|
6
6
|
import { resolveProjectRoot } from '../core/root-resolver.js';
|
|
7
7
|
import { applySetup, defaultHasBinary, planSetup } from '../core/setup-assets.js';
|
|
8
8
|
import { createProviders, providerCooldownRemaining } from '../providers/index.js';
|
|
9
|
+
import { checkHarnessIntegrity } from '../core/harness-integrity.js';
|
|
9
10
|
function defaultRunVersion(command, args) {
|
|
10
11
|
try {
|
|
11
12
|
// The Windows shims (nativ.cmd, npx.cmd) only start through a shell.
|
|
@@ -124,6 +125,9 @@ export function collectChecks(root, options = {}) {
|
|
|
124
125
|
// Enforcement
|
|
125
126
|
const mode = loadEnforcementMode(root);
|
|
126
127
|
add('enforcement', mode === 'off' ? 'warn' : 'ok', mode === 'off' ? 'Role enforcement is off: nothing stops an agent editing outside its task.' : `Role enforcement is ${mode}`);
|
|
128
|
+
// The harness itself: guardrail overrides, secrets in agent-read files, role guides, waiting lessons
|
|
129
|
+
for (const f of checkHarnessIntegrity(root, { templatesDir: options.templatesDir }))
|
|
130
|
+
add(f.id, f.status, f.message);
|
|
127
131
|
// Providers
|
|
128
132
|
const providers = createProviders(options.providerDeps);
|
|
129
133
|
const usable = [];
|
package/dist/commands/init.js
CHANGED
|
@@ -140,5 +140,6 @@ export async function runInit(targetDirArg, options = {}) {
|
|
|
140
140
|
console.log(pc.white(` 1. Run ${pc.green('claude')} in this directory and ask the ${pc.magenta('architect')} agent to design your contracts (database, API, UI). You approve each step.`));
|
|
141
141
|
console.log(pc.white(` 2. The architect writes ${pc.cyan('.ai/db_schema.json')}, ${pc.cyan('.ai/api_contracts.json')}, ${pc.cyan('.ai/ui_specs.md')}, and ${pc.cyan('.ai/master_plan.json')}.`));
|
|
142
142
|
console.log(pc.white(` 3. Ask Claude to run ${pc.green('nativ task next')} and delegate each task to a ${pc.magenta('worker')} agent; use ${pc.magenta('verifier')} for an independent check.`));
|
|
143
|
+
console.log(pc.white(` On existing code, ask for the ${pc.magenta('explorer')} agent first: it maps the code for the architect.`));
|
|
143
144
|
console.log(pc.dim(` Run ${pc.white('nativ doctor')} any time, and after updating Claude Code, to confirm the integration is intact.\n`));
|
|
144
145
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type LearningDecision } from '../core/learnings.js';
|
|
2
|
+
export interface LearnProposeOptions {
|
|
3
|
+
role?: string;
|
|
4
|
+
files?: string;
|
|
5
|
+
task?: string;
|
|
6
|
+
details?: string;
|
|
7
|
+
by?: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function runLearnPropose(insight: string, targetDirArg?: string, options?: LearnProposeOptions): Promise<void>;
|
|
11
|
+
export declare function runLearnDecide(id: string, decision: LearningDecision, targetDirArg?: string, options?: {
|
|
12
|
+
note?: string;
|
|
13
|
+
yes?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export declare function runLearnList(targetDirArg?: string, options?: {
|
|
16
|
+
status?: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
json?: boolean;
|
|
19
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { resolveMainRoot } from '../core/root-resolver.js';
|
|
5
|
+
import { refuseHeadless } from '../core/human-gate.js';
|
|
6
|
+
import { LEARNING_STATUSES, proposeLearning, decideLearning, listLearnings, loadLearnings, lessonContent, } from '../core/learnings.js';
|
|
7
|
+
function rootOf(targetDirArg) {
|
|
8
|
+
return resolveMainRoot(path.resolve(targetDirArg || process.cwd()));
|
|
9
|
+
}
|
|
10
|
+
function fail(message) {
|
|
11
|
+
console.error(pc.red(`✖ ${message}`));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
14
|
+
function headline(item) {
|
|
15
|
+
return `${pc.bold(item.id)} ${pc.dim(`[${item.status}]`)} ${item.insight}`;
|
|
16
|
+
}
|
|
17
|
+
/** Everything a worker would receive, so the human approves exactly that. */
|
|
18
|
+
function fullText(item) {
|
|
19
|
+
const lines = [headline(item)];
|
|
20
|
+
if (item.details)
|
|
21
|
+
lines.push(...item.details.split('\n').map((l) => pc.white(` ${l}`)));
|
|
22
|
+
const scope = [
|
|
23
|
+
item.role ? `role ${item.role}` : 'every role',
|
|
24
|
+
item.files?.length ? `files ${item.files.join(', ')}` : 'any file',
|
|
25
|
+
];
|
|
26
|
+
lines.push(pc.dim(` for ${scope.join('; ')}${item.proposedBy ? `; proposed by ${item.proposedBy}` : ''}`));
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
export async function runLearnPropose(insight, targetDirArg, options = {}) {
|
|
30
|
+
let item;
|
|
31
|
+
try {
|
|
32
|
+
item = proposeLearning(rootOf(targetDirArg), {
|
|
33
|
+
insight,
|
|
34
|
+
details: options.details,
|
|
35
|
+
role: options.role,
|
|
36
|
+
files: options.files?.split(','),
|
|
37
|
+
taskId: options.task,
|
|
38
|
+
proposedBy: options.by ?? process.env.NATIV_ROLE,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
fail(err.message);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify(item, null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(pc.green('\n✔ Proposed'));
|
|
50
|
+
for (const line of fullText(item))
|
|
51
|
+
console.log(` ${line}`);
|
|
52
|
+
console.log(pc.dim(` A human approves it in a terminal: nativ learn approve ${item.id}\n`));
|
|
53
|
+
}
|
|
54
|
+
async function confirm(question) {
|
|
55
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
try {
|
|
57
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
rl.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function runLearnDecide(id, decision, targetDirArg, options = {}) {
|
|
64
|
+
if (refuseHeadless(`nativ learn ${decision === 'approved' ? 'approve' : 'reject'}`))
|
|
65
|
+
return;
|
|
66
|
+
const root = rootOf(targetDirArg);
|
|
67
|
+
const current = loadLearnings(root).learnings.find((l) => l.id === id);
|
|
68
|
+
if (!current) {
|
|
69
|
+
fail(`No learning ${id}.`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log('');
|
|
73
|
+
for (const line of fullText(current))
|
|
74
|
+
console.log(` ${line}`);
|
|
75
|
+
const verb = decision === 'approved' ? 'Approve' : 'Reject';
|
|
76
|
+
if (!options.yes && !(await confirm(`\n ${verb} ${id}? [y/N] `))) {
|
|
77
|
+
console.log(pc.dim(' Nothing changed.\n'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let result;
|
|
81
|
+
try {
|
|
82
|
+
result = decideLearning(root, id, decision, options.note, lessonContent(current));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
fail(err.message);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (typeof result === 'string') {
|
|
89
|
+
fail(result);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const after = decision === 'approved'
|
|
93
|
+
? 'Workers whose task matches its scope will now see it.'
|
|
94
|
+
: 'It stays on record.';
|
|
95
|
+
console.log(pc.green(`\n✔ ${headline(result)}`));
|
|
96
|
+
console.log(pc.dim(` ${after}\n`));
|
|
97
|
+
}
|
|
98
|
+
export async function runLearnList(targetDirArg, options = {}) {
|
|
99
|
+
if (options.status && !LEARNING_STATUSES.includes(options.status)) {
|
|
100
|
+
fail(`Unknown status "${options.status}". Use one of: ${LEARNING_STATUSES.join(', ')}.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const status = options.status;
|
|
104
|
+
const items = listLearnings(rootOf(targetDirArg), { status, role: options.role });
|
|
105
|
+
if (options.json) {
|
|
106
|
+
console.log(JSON.stringify(items, null, 2));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!items.length) {
|
|
110
|
+
console.log(pc.dim('\nNo learnings match.\n'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log('');
|
|
114
|
+
for (const item of items)
|
|
115
|
+
for (const line of fullText(item))
|
|
116
|
+
console.log(` ${line}`);
|
|
117
|
+
const waiting = items.filter((i) => i.status === 'proposed').length;
|
|
118
|
+
if (waiting)
|
|
119
|
+
console.log(pc.yellow(`\n ${waiting} waiting for approval: nativ learn approve <id> or reject <id>`));
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/commands/task.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { isSubagentType, SUBAGENT_TYPES, validateMasterPlanTask, } from '../scanner/types.js';
|
|
4
|
-
import {
|
|
5
|
-
import { ContractGovernor, CircuitBreaker, TestIntegrityGuard, resolveGitHead, appendEscalation, } from '../governor/index.js';
|
|
4
|
+
import { runTaskVerification } from '../core/verifier.js';
|
|
5
|
+
import { ContractGovernor, CircuitBreaker, TestIntegrityGuard, resolveGitHead, appendEscalation, taskEscalationHistory, closeEscalationsForTask, } from '../governor/index.js';
|
|
6
6
|
import { loadPlan, savePlan, withPlanLock } from '../core/lock-manager.js';
|
|
7
7
|
import { recordTaskStart, recordTaskComplete } from '../core/telemetry.js';
|
|
8
8
|
import { resolveSpecSlices } from '../core/spec-slices.js';
|
|
9
9
|
import { resolveWorkerModel } from '../core/model-routing.js';
|
|
10
10
|
import { addTaskUnlock, removeTaskUnlock } from '../core/enforcement.js';
|
|
11
|
+
import { refuseHeadless } from '../core/human-gate.js';
|
|
12
|
+
import { learningsForTask } from '../core/learnings.js';
|
|
11
13
|
export { loadPlan, savePlan, withPlanLock };
|
|
12
14
|
function getPlanPath(targetDirArg) {
|
|
13
15
|
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
@@ -257,11 +259,14 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
257
259
|
}
|
|
258
260
|
const roleGuide = getRoleGuide(targetTask.assignedSubagent);
|
|
259
261
|
const contractSlice = getRecommendedContractSlice(targetTask.assignedSubagent);
|
|
262
|
+
const projectRoot = path.dirname(path.dirname(planPath));
|
|
263
|
+
const learnings = learningsForTask(projectRoot, targetTask);
|
|
264
|
+
const priorEscalations = taskEscalationHistory(projectRoot, targetTask.id);
|
|
260
265
|
if (options.json) {
|
|
261
266
|
const specResult = targetTask.specRefs?.length
|
|
262
|
-
? resolveSpecSlices(
|
|
267
|
+
? resolveSpecSlices(projectRoot, targetTask.specRefs)
|
|
263
268
|
: null;
|
|
264
|
-
const recommendedModel = resolveWorkerModel(
|
|
269
|
+
const recommendedModel = resolveWorkerModel(projectRoot, targetTask);
|
|
265
270
|
console.log(JSON.stringify({
|
|
266
271
|
status: 'ready',
|
|
267
272
|
milestoneId: activeMilestone.id,
|
|
@@ -272,6 +277,8 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
272
277
|
recommendedContractSlice: contractSlice,
|
|
273
278
|
...(recommendedModel ? { recommendedModel } : {}),
|
|
274
279
|
...(specResult ? { specSlices: specResult.slices, ...(specResult.warnings.length ? { specWarnings: specResult.warnings } : {}) } : {}),
|
|
280
|
+
...(learnings.length ? { learnings: learnings.map(toTaskLearning) } : {}),
|
|
281
|
+
...(priorEscalations.length ? { priorEscalations } : {}),
|
|
275
282
|
}
|
|
276
283
|
}, null, 2));
|
|
277
284
|
return;
|
|
@@ -294,12 +301,23 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
294
301
|
if (targetTask.notes) {
|
|
295
302
|
console.log(pc.bold(` Notes: `) + pc.red(targetTask.notes));
|
|
296
303
|
}
|
|
304
|
+
for (const l of learnings) {
|
|
305
|
+
console.log(pc.bold(` Learning: `) + pc.white(`${l.id} ${l.insight}`));
|
|
306
|
+
}
|
|
307
|
+
for (const e of priorEscalations) {
|
|
308
|
+
const outcome = e.resolutionNotes ? ` -> ${e.resolutionNotes}` : '';
|
|
309
|
+
console.log(pc.bold(` Past escalation: `) + pc.white(`${e.id} ${e.status}: ${e.summary}${outcome}`));
|
|
310
|
+
}
|
|
297
311
|
console.log(pc.dim('\n─── Quick CLI Actions ──────────────────────────────────────────'));
|
|
298
312
|
console.log(pc.dim(' Start task: ') + pc.white(`nativ task start ${targetTask.id}`));
|
|
299
313
|
console.log(pc.dim(' Complete task:') + pc.white(`nativ task complete ${targetTask.id}`));
|
|
300
314
|
console.log(pc.dim(' Block task: ') + pc.white(`nativ task block ${targetTask.id} --reason "..."`));
|
|
301
315
|
console.log(pc.dim(' Escalate task:') + pc.white(`nativ task escalate ${targetTask.id} --type schema_flaw --details "..."\n`));
|
|
302
316
|
}
|
|
317
|
+
/** What a worker needs from a lesson; status and audit fields stay in .ai/learnings.json. */
|
|
318
|
+
function toTaskLearning({ id, insight, details, files }) {
|
|
319
|
+
return { id, insight, ...(details ? { details } : {}), ...(files ? { files } : {}) };
|
|
320
|
+
}
|
|
303
321
|
export async function runTaskStart(taskId, targetDirArg) {
|
|
304
322
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
305
323
|
return withPlanLock(planPath, async (plan, ctx) => {
|
|
@@ -347,6 +365,8 @@ export async function runTaskStart(taskId, targetDirArg) {
|
|
|
347
365
|
* tool: an agent must not be able to remove its own guardrail.
|
|
348
366
|
*/
|
|
349
367
|
export async function runTaskUnlock(taskId, targetDirArg, options = {}) {
|
|
368
|
+
if (!options.revoke && refuseHeadless('nativ task unlock'))
|
|
369
|
+
return;
|
|
350
370
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
351
371
|
const plan = loadPlan(planPath);
|
|
352
372
|
if (!plan) {
|
|
@@ -368,6 +388,8 @@ export async function runTaskUnlock(taskId, targetDirArg, options = {}) {
|
|
|
368
388
|
console.log(pc.dim(' .ai/ contracts and secret files stay protected. Re-lock with `nativ task unlock ' + taskId + ' --revoke`.\n'));
|
|
369
389
|
}
|
|
370
390
|
export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
391
|
+
if (options.skipVerify && refuseHeadless('nativ task complete --no-verify'))
|
|
392
|
+
return;
|
|
371
393
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
372
394
|
const plan = loadPlan(planPath);
|
|
373
395
|
if (!plan) {
|
|
@@ -425,11 +447,13 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
425
447
|
// ── Verification Gatekeeper ────────────────────────────────────────────────
|
|
426
448
|
let vResult = null;
|
|
427
449
|
if (!options.skipVerify) {
|
|
428
|
-
|
|
429
|
-
vResult = applyCodeShape(rawResult, targetDir, foundTask);
|
|
450
|
+
vResult = await runTaskVerification(foundTask, { cwd: targetDir, timeout: options.timeout });
|
|
430
451
|
if (!vResult.success) {
|
|
431
452
|
console.error(pc.red(`\n✖ Task [${pc.bold(taskId)}] verification FAILED with exit code ${vResult.exitCode}:`));
|
|
432
|
-
|
|
453
|
+
const failedPhase = vResult.phases?.find((p) => !p.success);
|
|
454
|
+
if (failedPhase)
|
|
455
|
+
console.error(pc.yellow(` Phase: ${failedPhase.name}`));
|
|
456
|
+
console.error(pc.yellow(` Command: \`${vResult.command}\``));
|
|
433
457
|
if (vResult.stderr && vResult.stderr.trim()) {
|
|
434
458
|
console.error(pc.red('\n--- stderr ---'));
|
|
435
459
|
console.error(pc.red(vResult.stderr.trim()));
|
|
@@ -507,6 +531,9 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
507
531
|
});
|
|
508
532
|
CircuitBreaker.recordSuccess(targetDir, taskId);
|
|
509
533
|
CircuitBreaker.clearBaseline(targetDir, taskId);
|
|
534
|
+
const closed = closeEscalationsForTask(targetDir, taskId, 'Closed automatically: the task was completed.');
|
|
535
|
+
if (closed.length)
|
|
536
|
+
console.log(pc.dim(` Closed its open escalations: ${closed.join(', ')}`));
|
|
510
537
|
try {
|
|
511
538
|
await recordTaskComplete(targetDir, foundTask, vResult, options.notes);
|
|
512
539
|
}
|
package/dist/commands/verify.js
CHANGED
|
@@ -32,11 +32,17 @@ export async function runVerify(taskId, targetDirArg, options = {}) {
|
|
|
32
32
|
console.log(pc.green(` ✔ [${pc.bold(t.taskId)}] ${t.title}`) +
|
|
33
33
|
pc.dim(` (${elapsed}s)`) +
|
|
34
34
|
pc.yellow(` \`${t.result.command}\``));
|
|
35
|
+
if (t.result.phases?.length) {
|
|
36
|
+
console.log(pc.dim(` Phases passed: ${t.result.phases.map((p) => p.name).join(', ')}`));
|
|
37
|
+
}
|
|
35
38
|
}
|
|
36
39
|
else {
|
|
37
40
|
console.log(pc.red(` ✖ [${pc.bold(t.taskId)}] ${t.title}`) +
|
|
38
41
|
pc.dim(` (${elapsed}s, exit code: ${t.result.exitCode})`) +
|
|
39
42
|
pc.yellow(` \`${t.result.command}\``));
|
|
43
|
+
const failedPhase = t.result.phases?.find((p) => !p.success);
|
|
44
|
+
if (failedPhase)
|
|
45
|
+
console.log(pc.red(` Failed in phase "${failedPhase.name}"`));
|
|
40
46
|
if (t.result.stderr && t.result.stderr.trim()) {
|
|
41
47
|
console.log(pc.red(`\n--- [${t.taskId}] stderr ---`));
|
|
42
48
|
console.log(pc.red(t.result.stderr.trim()));
|
package/dist/core/code-shape.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parseJsonLoose } from './enforcement.js';
|
|
4
|
+
import { resolveMainRoot } from './root-resolver.js';
|
|
3
5
|
const DEFAULTS = { mode: 'warn', maxLineLength: 120, maxCommentLines: 2 };
|
|
4
6
|
const SOURCE_EXTENSIONS = new Set([
|
|
5
7
|
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.dart',
|
|
@@ -10,8 +12,8 @@ const MAX_SHOWN = 10;
|
|
|
10
12
|
export function loadCodeShapeSettings(targetDir) {
|
|
11
13
|
const settings = { ...DEFAULTS };
|
|
12
14
|
try {
|
|
13
|
-
const raw = fs.readFileSync(path.join(targetDir, '.nativ', 'config.json'), 'utf8');
|
|
14
|
-
const style =
|
|
15
|
+
const raw = fs.readFileSync(path.join(resolveMainRoot(targetDir), '.nativ', 'config.json'), 'utf8');
|
|
16
|
+
const style = parseJsonLoose(raw)?.codeStyle;
|
|
15
17
|
if (!style || typeof style !== 'object')
|
|
16
18
|
return settings;
|
|
17
19
|
if (['warn', 'block', 'off'].includes(style.mode))
|
|
@@ -55,6 +55,7 @@ export declare function toProjectRelative(root: string, filePath: string, cwd: s
|
|
|
55
55
|
/** A target entry matches an exact file, a directory prefix (`src/x` or `src/x/`), or a glob. */
|
|
56
56
|
export declare function matchesTarget(relPath: string, target: string): boolean;
|
|
57
57
|
export declare function isSecretPath(relPath: string): boolean;
|
|
58
|
+
export declare function isNativSettingsPath(relPath: string): boolean;
|
|
58
59
|
/** Everything under `.ai/` is written by nativ commands or the architect, never by a worker's file tools. */
|
|
59
60
|
export declare function isProtectedContractPath(relPath: string): boolean;
|
|
60
61
|
/**
|
package/dist/core/enforcement.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { EXAMPLE_ENV_FILES } from '../db/env-parser.js';
|
|
15
|
-
import { resolveProjectRoot } from './root-resolver.js';
|
|
15
|
+
import { resolveMainRoot, resolveProjectRoot } from './root-resolver.js';
|
|
16
16
|
export const DEFAULT_ENFORCEMENT_MODE = 'warn';
|
|
17
17
|
/** Roles other than these are treated as ordinary workers. The architect is the only role that may edit contracts. */
|
|
18
18
|
export const ARCHITECT_ROLE = 'architect';
|
|
@@ -151,6 +151,9 @@ export function isSecretPath(relPath) {
|
|
|
151
151
|
return true;
|
|
152
152
|
return base.endsWith('.pem') || base.endsWith('.key');
|
|
153
153
|
}
|
|
154
|
+
export function isNativSettingsPath(relPath) {
|
|
155
|
+
return relPath === '.nativ' || relPath.toLowerCase().startsWith('.nativ/');
|
|
156
|
+
}
|
|
154
157
|
/** Everything under `.ai/` is written by nativ commands or the architect, never by a worker's file tools. */
|
|
155
158
|
export function isProtectedContractPath(relPath) {
|
|
156
159
|
return relPath === '.ai' || relPath.toLowerCase().startsWith('.ai/');
|
|
@@ -185,8 +188,18 @@ function verdict(mode, root, relPath, taskIds, rule, reason) {
|
|
|
185
188
|
*/
|
|
186
189
|
export function checkWrite(input) {
|
|
187
190
|
const root = resolveProjectRoot(input.cwd);
|
|
188
|
-
|
|
189
|
-
const
|
|
191
|
+
// Settings and unlocks live in the main checkout; a worktree's own copy must not decide them.
|
|
192
|
+
const mainRoot = resolveMainRoot(input.cwd);
|
|
193
|
+
const mode = input.mode ?? loadEnforcementMode(mainRoot);
|
|
194
|
+
let relPath = toProjectRelative(root, input.filePath, input.cwd);
|
|
195
|
+
if (relPath === null && mainRoot !== root) {
|
|
196
|
+
// A worktree session can reach the main checkout through ../..; judge those paths as its own.
|
|
197
|
+
const mainRel = toProjectRelative(mainRoot, input.filePath, input.cwd);
|
|
198
|
+
const guarded = (p) => isSecretPath(p) || isNativSettingsPath(p) || isProtectedContractPath(p);
|
|
199
|
+
if (mainRel !== null && guarded(mainRel)) {
|
|
200
|
+
relPath = mainRel;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
190
203
|
const allow = { decision: 'allow', mode, root, relPath, taskIds: [] };
|
|
191
204
|
if (mode === 'off' || relPath === null)
|
|
192
205
|
return allow;
|
|
@@ -195,6 +208,11 @@ export function checkWrite(input) {
|
|
|
195
208
|
}
|
|
196
209
|
const role = (input.role ?? process.env[ROLE_ENV] ?? '').toLowerCase();
|
|
197
210
|
const isArchitect = ARCHITECT_ROLES.has(role);
|
|
211
|
+
// The settings decide what the gate runs and how strict the hook is, so no agent edits them.
|
|
212
|
+
if (isNativSettingsPath(relPath)) {
|
|
213
|
+
const reason = `${relPath} holds nativ settings, which only the operator changes. Ask the human to edit it.`;
|
|
214
|
+
return verdict(mode, root, relPath, [], 'protected_path', reason);
|
|
215
|
+
}
|
|
198
216
|
if (isProtectedContractPath(relPath)) {
|
|
199
217
|
if (isArchitect)
|
|
200
218
|
return allow;
|
|
@@ -212,7 +230,7 @@ export function checkWrite(input) {
|
|
|
212
230
|
tasks = own;
|
|
213
231
|
}
|
|
214
232
|
const taskIds = tasks.map((t) => t.id);
|
|
215
|
-
if (tasks.some((t) => isTaskUnlocked(
|
|
233
|
+
if (tasks.some((t) => isTaskUnlocked(mainRoot, t.id)))
|
|
216
234
|
return { ...allow, taskIds };
|
|
217
235
|
if (tasks.some((t) => t.targetFiles.some((target) => matchesTarget(relPath, target))))
|
|
218
236
|
return { ...allow, taskIds };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nativ · Checks that the harness itself is intact: nothing switches the guardrails off, no secret
|
|
3
|
+
* sits in a file agents read, every role a task names has a guide. Findings never include a secret.
|
|
4
|
+
*/
|
|
5
|
+
export type CheckStatus = 'ok' | 'warn' | 'fail' | 'info';
|
|
6
|
+
export interface IntegrityFinding {
|
|
7
|
+
id: string;
|
|
8
|
+
status: CheckStatus;
|
|
9
|
+
message: string;
|
|
10
|
+
/** `guardrail` and `secret` findings decide whether doctor may report the harness as clean. */
|
|
11
|
+
category: 'guardrail' | 'secret' | 'roles' | 'learnings' | 'specs';
|
|
12
|
+
}
|
|
13
|
+
export declare function checkHarnessIntegrity(root: string, options?: {
|
|
14
|
+
templatesDir?: string;
|
|
15
|
+
}): IntegrityFinding[];
|