@njeybe/nativ 2.3.0 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +676 -190
  3. package/dist/commands/db.js +3 -0
  4. package/dist/commands/doctor.d.ts +2 -1
  5. package/dist/commands/doctor.js +4 -0
  6. package/dist/commands/init.js +1 -0
  7. package/dist/commands/learn.d.ts +19 -0
  8. package/dist/commands/learn.js +121 -0
  9. package/dist/commands/session.d.ts +22 -0
  10. package/dist/commands/session.js +106 -0
  11. package/dist/commands/task.js +34 -7
  12. package/dist/commands/verify.js +6 -0
  13. package/dist/core/code-shape.js +4 -2
  14. package/dist/core/enforcement.d.ts +1 -0
  15. package/dist/core/enforcement.js +22 -4
  16. package/dist/core/harness-integrity.d.ts +15 -0
  17. package/dist/core/harness-integrity.js +198 -0
  18. package/dist/core/human-gate.d.ts +7 -0
  19. package/dist/core/human-gate.js +16 -0
  20. package/dist/core/learnings.d.ts +63 -0
  21. package/dist/core/learnings.js +227 -0
  22. package/dist/core/root-resolver.d.ts +5 -0
  23. package/dist/core/root-resolver.js +46 -0
  24. package/dist/core/session-memory.d.ts +42 -0
  25. package/dist/core/session-memory.js +121 -0
  26. package/dist/core/setup-assets.d.ts +1 -1
  27. package/dist/core/setup-assets.js +5 -2
  28. package/dist/core/verifier.d.ts +36 -0
  29. package/dist/core/verifier.js +92 -4
  30. package/dist/db/introspector.js +59 -59
  31. package/dist/governor/store.d.ts +18 -0
  32. package/dist/governor/store.js +53 -0
  33. package/dist/index.js +41 -0
  34. package/dist/mcp/server.js +12 -0
  35. package/dist/runner/agent-supervisor.d.ts +21 -3
  36. package/dist/runner/agent-supervisor.js +80 -20
  37. package/dist/scanner/types.d.ts +1 -0
  38. package/dist/server/studio-server.js +1 -130
  39. package/dist/server/studio-ui/shell/dispatch.js +3 -9
  40. package/dist/server/studio-ui/shell/model.js +3 -1
  41. package/package.json +2 -2
  42. package/plugin/.claude-plugin/plugin.json +2 -2
  43. package/plugin/agents/architect.md +3 -3
  44. package/plugin/agents/explorer.md +49 -0
  45. package/plugin/agents/worker.md +2 -1
  46. package/plugin/skills/nativ/SKILL.md +15 -0
  47. package/templates/AGENTS.md +15 -0
  48. package/templates/CLAUDE.md +32 -32
  49. package/templates/GEMINI.md +2 -2
  50. package/templates/claude-agents/architect.md +3 -3
  51. package/templates/claude-agents/explorer.md +49 -0
  52. package/templates/claude-agents/worker.md +2 -1
  53. package/templates/dot-ai/api_contracts.json +66 -66
  54. package/templates/dot-ai/context.md +38 -38
  55. package/templates/dot-ai/db_schema.json +39 -39
  56. package/templates/dot-ai/subagents/frontend.md +33 -0
  57. package/templates/dot-ai/ui_specs.md +17 -0
@@ -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 = 'ok' | 'warn' | 'fail' | 'info';
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;
@@ -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 = [];
@@ -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
+ }
@@ -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 { applyCodeShape, executeVerification } from '../core/verifier.js';
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(path.dirname(path.dirname(planPath)), targetTask.specRefs)
267
+ ? resolveSpecSlices(projectRoot, targetTask.specRefs)
263
268
  : null;
264
- const recommendedModel = resolveWorkerModel(path.dirname(path.dirname(planPath)), targetTask);
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
- const rawResult = await executeVerification(foundTask.verificationCommand, targetDir, options.timeout);
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
- console.error(pc.yellow(` Command: \`${foundTask.verificationCommand}\``));
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
  }
@@ -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()));
@@ -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 = JSON.parse(raw)?.codeStyle;
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
  /**
@@ -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
- const mode = input.mode ?? loadEnforcementMode(root);
189
- const relPath = toProjectRelative(root, input.filePath, input.cwd);
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(root, t.id)))
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[];