@njeybe/nativ 2.0.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 (140) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/bin/cli.js +31 -0
  5. package/dist/commands/bench.d.ts +8 -0
  6. package/dist/commands/bench.js +52 -0
  7. package/dist/commands/db.d.ts +22 -0
  8. package/dist/commands/db.js +415 -0
  9. package/dist/commands/doctor.d.ts +38 -0
  10. package/dist/commands/doctor.js +169 -0
  11. package/dist/commands/hook.d.ts +31 -0
  12. package/dist/commands/hook.js +105 -0
  13. package/dist/commands/init.d.ts +3 -0
  14. package/dist/commands/init.js +144 -0
  15. package/dist/commands/mcp.d.ts +5 -0
  16. package/dist/commands/mcp.js +16 -0
  17. package/dist/commands/setup.d.ts +17 -0
  18. package/dist/commands/setup.js +51 -0
  19. package/dist/commands/status.d.ts +5 -0
  20. package/dist/commands/status.js +75 -0
  21. package/dist/commands/task.d.ts +83 -0
  22. package/dist/commands/task.js +807 -0
  23. package/dist/commands/test-gen.d.ts +14 -0
  24. package/dist/commands/test-gen.js +52 -0
  25. package/dist/commands/triage.d.ts +70 -0
  26. package/dist/commands/triage.js +343 -0
  27. package/dist/commands/update.d.ts +14 -0
  28. package/dist/commands/update.js +112 -0
  29. package/dist/commands/validate.d.ts +1 -0
  30. package/dist/commands/validate.js +89 -0
  31. package/dist/commands/verify.d.ts +8 -0
  32. package/dist/commands/verify.js +65 -0
  33. package/dist/commands/worktree.d.ts +47 -0
  34. package/dist/commands/worktree.js +396 -0
  35. package/dist/core/benchmark.d.ts +57 -0
  36. package/dist/core/benchmark.js +772 -0
  37. package/dist/core/enforcement.d.ts +80 -0
  38. package/dist/core/enforcement.js +271 -0
  39. package/dist/core/legacy-templates.d.ts +24 -0
  40. package/dist/core/legacy-templates.js +60 -0
  41. package/dist/core/lock-manager.d.ts +54 -0
  42. package/dist/core/lock-manager.js +260 -0
  43. package/dist/core/root-resolver.d.ts +32 -0
  44. package/dist/core/root-resolver.js +149 -0
  45. package/dist/core/setup-assets.d.ts +123 -0
  46. package/dist/core/setup-assets.js +438 -0
  47. package/dist/core/telemetry.d.ts +156 -0
  48. package/dist/core/telemetry.js +416 -0
  49. package/dist/core/test-generator-types.d.ts +227 -0
  50. package/dist/core/test-generator-types.js +179 -0
  51. package/dist/core/test-generator.d.ts +34 -0
  52. package/dist/core/test-generator.js +1690 -0
  53. package/dist/core/tier1-liaison.d.ts +139 -0
  54. package/dist/core/tier1-liaison.js +320 -0
  55. package/dist/core/verifier.d.ts +52 -0
  56. package/dist/core/verifier.js +249 -0
  57. package/dist/core/version.d.ts +5 -0
  58. package/dist/core/version.js +14 -0
  59. package/dist/core/worktree-merge.d.ts +30 -0
  60. package/dist/core/worktree-merge.js +98 -0
  61. package/dist/db/data-engine.d.ts +46 -0
  62. package/dist/db/data-engine.js +802 -0
  63. package/dist/db/diff.d.ts +39 -0
  64. package/dist/db/diff.js +420 -0
  65. package/dist/db/env-parser.d.ts +86 -0
  66. package/dist/db/env-parser.js +549 -0
  67. package/dist/db/introspector.d.ts +21 -0
  68. package/dist/db/introspector.js +835 -0
  69. package/dist/db/types.d.ts +217 -0
  70. package/dist/db/types.js +9 -0
  71. package/dist/governor/circuit-breaker.d.ts +100 -0
  72. package/dist/governor/circuit-breaker.js +231 -0
  73. package/dist/governor/evaluator.d.ts +67 -0
  74. package/dist/governor/evaluator.js +851 -0
  75. package/dist/governor/index.d.ts +6 -0
  76. package/dist/governor/index.js +6 -0
  77. package/dist/governor/rules/api-rules.d.ts +2 -0
  78. package/dist/governor/rules/api-rules.js +140 -0
  79. package/dist/governor/rules/db-rules.d.ts +2 -0
  80. package/dist/governor/rules/db-rules.js +180 -0
  81. package/dist/governor/store.d.ts +33 -0
  82. package/dist/governor/store.js +84 -0
  83. package/dist/governor/types.d.ts +64 -0
  84. package/dist/governor/types.js +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.js +358 -0
  87. package/dist/mcp/server.d.ts +3 -0
  88. package/dist/mcp/server.js +309 -0
  89. package/dist/providers/claude-api.d.ts +5 -0
  90. package/dist/providers/claude-api.js +75 -0
  91. package/dist/providers/claude-cli.d.ts +13 -0
  92. package/dist/providers/claude-cli.js +134 -0
  93. package/dist/providers/gemini.d.ts +8 -0
  94. package/dist/providers/gemini.js +78 -0
  95. package/dist/providers/index.d.ts +5 -0
  96. package/dist/providers/index.js +5 -0
  97. package/dist/providers/registry.d.ts +38 -0
  98. package/dist/providers/registry.js +121 -0
  99. package/dist/providers/types.d.ts +74 -0
  100. package/dist/providers/types.js +25 -0
  101. package/dist/runner/agent-supervisor.d.ts +363 -0
  102. package/dist/runner/agent-supervisor.js +1672 -0
  103. package/dist/scanner/context-builder.d.ts +2 -0
  104. package/dist/scanner/context-builder.js +29 -0
  105. package/dist/scanner/detector.d.ts +2 -0
  106. package/dist/scanner/detector.js +760 -0
  107. package/dist/scanner/types.d.ts +80 -0
  108. package/dist/scanner/types.js +50 -0
  109. package/dist/server/studio-server.d.ts +39 -0
  110. package/dist/server/studio-server.js +1931 -0
  111. package/dist/server/studio-ui.d.ts +15 -0
  112. package/dist/server/studio-ui.js +5321 -0
  113. package/package.json +70 -0
  114. package/plugin/.claude-plugin/plugin.json +18 -0
  115. package/plugin/.mcp.json +10 -0
  116. package/plugin/agents/architect.md +28 -0
  117. package/plugin/agents/verifier.md +28 -0
  118. package/plugin/agents/worker.md +26 -0
  119. package/plugin/hooks/hooks.json +28 -0
  120. package/plugin/skills/nativ/SKILL.md +75 -0
  121. package/templates/.claudeignore +45 -0
  122. package/templates/AGENTS.md +67 -0
  123. package/templates/CLAUDE.md +32 -0
  124. package/templates/GEMINI.md +66 -0
  125. package/templates/claude-agents/architect.md +28 -0
  126. package/templates/claude-agents/verifier.md +28 -0
  127. package/templates/claude-agents/worker.md +26 -0
  128. package/templates/dot-ai/api_contracts.json +66 -0
  129. package/templates/dot-ai/context.md +38 -0
  130. package/templates/dot-ai/db_schema.json +39 -0
  131. package/templates/dot-ai/master_plan.json +28 -0
  132. package/templates/dot-ai/subagents/backend.md +11 -0
  133. package/templates/dot-ai/subagents/database.md +10 -0
  134. package/templates/dot-ai/subagents/db-migration.md +11 -0
  135. package/templates/dot-ai/subagents/devops-agent.md +11 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +11 -0
  137. package/templates/dot-ai/subagents/frontend.md +126 -0
  138. package/templates/dot-ai/subagents/qa-tester.md +13 -0
  139. package/templates/dot-ai/subagents/security-auditor.md +11 -0
  140. package/templates/dot-ai/ui_specs.md +78 -0
@@ -0,0 +1,14 @@
1
+ import { type TestGenResult } from '../core/test-generator-types.js';
2
+ /**
3
+ * `nativ test gen`: synthesizes contract and database integrity tests from .ai/ contracts.
4
+ * Generated suites read live structure through `nativ db inspect --json`, never credentials.
5
+ */
6
+ export interface TestGenCommandOptions {
7
+ output?: string;
8
+ framework?: string;
9
+ baseUrl?: string;
10
+ dryRun?: boolean;
11
+ json?: boolean;
12
+ }
13
+ export declare function runTestGen(targetDirArg?: string, options?: TestGenCommandOptions): Promise<TestGenResult | null>;
14
+ export declare const TEST_GEN_FRAMEWORK_HELP: string;
@@ -0,0 +1,52 @@
1
+ import path from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { generateContractTests, TestGenError } from '../core/test-generator.js';
4
+ import { TEST_FRAMEWORKS } from '../core/test-generator-types.js';
5
+ const SUITE_LABEL = {
6
+ 'api-contract': 'API contract',
7
+ 'db-integrity': 'DB integrity',
8
+ support: 'support',
9
+ };
10
+ export async function runTestGen(targetDirArg, options = {}) {
11
+ const targetDir = path.resolve(targetDirArg || process.cwd());
12
+ let result;
13
+ try {
14
+ result = generateContractTests({
15
+ targetDir,
16
+ outputDir: options.output,
17
+ framework: options.framework,
18
+ baseUrl: options.baseUrl,
19
+ dryRun: options.dryRun,
20
+ });
21
+ }
22
+ catch (err) {
23
+ const code = err instanceof TestGenError ? err.code : 'TEST_GEN_FAILED';
24
+ const message = err instanceof Error ? err.message : String(err);
25
+ if (options.json)
26
+ console.log(JSON.stringify({ error: { code, message } }, null, 2));
27
+ else
28
+ console.error(pc.red(`\n✖ ${message}\n`));
29
+ process.exitCode = 1;
30
+ return null;
31
+ }
32
+ if (options.json) {
33
+ console.log(JSON.stringify(result, null, 2));
34
+ return result;
35
+ }
36
+ const mode = options.dryRun ? pc.yellow(' (dry run, nothing written)') : '';
37
+ console.log(pc.bold(pc.cyan(`\nContract Test Generator: ${result.projectName}`)) + mode);
38
+ console.log(pc.dim(`Framework: ${result.framework} · Output: ${result.outputDir}\n`));
39
+ if (!result.files.length) {
40
+ console.log(pc.yellow('No endpoints or tables in .ai/ contracts produced tests.\n'));
41
+ return result;
42
+ }
43
+ for (const file of result.files) {
44
+ const label = SUITE_LABEL[file.suiteType] ?? file.suiteType;
45
+ console.log(` ${options.dryRun ? pc.dim('○') : pc.green('✔')} ${file.relativePath} ${pc.dim(`[${label}]`)}`);
46
+ }
47
+ console.log(`\n${pc.bold(String(result.endpointsCovered))} endpoints and ${pc.bold(String(result.tablesCovered))} tables covered` +
48
+ pc.dim(` in ${result.durationMs}ms`));
49
+ console.log(pc.dim('Set NATIV_TEST_BASE_URL to point the API suites at another server; DB suites skip when the database is unreachable.\n'));
50
+ return result;
51
+ }
52
+ export const TEST_GEN_FRAMEWORK_HELP = `Test framework (${TEST_FRAMEWORKS.join(', ')})`;
@@ -0,0 +1,70 @@
1
+ import type { CandidatePatch, SelfHealingEscalationRecord } from '../governor/circuit-breaker.js';
2
+ import { type HumanDecisionCard, type Tier1LiaisonOptions, type TriageClassification, type TriageEvaluation, type TriageFailure, type TriageSource } from '../core/tier1-liaison.js';
3
+ /** Latest Tier 1 verdict, stored on the escalation so Studio and the architect can show it after a reload. */
4
+ export interface TriageSnapshot {
5
+ evaluatedAt: string;
6
+ provider: string;
7
+ model: string;
8
+ source: TriageSource;
9
+ latencyMs: number;
10
+ classification: TriageClassification;
11
+ reasoning: string;
12
+ autoPatchApplied: boolean;
13
+ unblockedTaskId: string | null;
14
+ guardrails: string[];
15
+ resolution?: {
16
+ summary: string;
17
+ patch: CandidatePatch;
18
+ proofPassed: boolean;
19
+ proposalId?: string;
20
+ };
21
+ humanCard?: HumanDecisionCard;
22
+ }
23
+ /** The product owner's pick from a decision card. Acting on it stays with the approve/reject gate. */
24
+ export interface HumanDecision {
25
+ optionId: string;
26
+ label: string;
27
+ outcome?: string;
28
+ instructions?: string;
29
+ decidedAt: string;
30
+ decidedVia: 'cli';
31
+ }
32
+ export type TriagedEscalationRecord = SelfHealingEscalationRecord & {
33
+ triage?: TriageSnapshot;
34
+ humanDecision?: HumanDecision;
35
+ };
36
+ export interface TriageFinalization {
37
+ taskId: string | null;
38
+ unblockedTaskId: string | null;
39
+ persisted: boolean;
40
+ }
41
+ /**
42
+ * Only pending escalations may be triaged: re-running a resolved one could re-apply its proposal.
43
+ * Returns an error message, or null when the escalation can be evaluated.
44
+ */
45
+ export declare function triageBlocker(root: string, escalationId: string): string | null;
46
+ /**
47
+ * After an evaluation: unblocks the task and closes the escalation when a patch was auto-applied,
48
+ * then stores the verdict on the escalation. Never throws; a bookkeeping failure must not hide an
49
+ * applied patch.
50
+ */
51
+ export declare function finalizeTriage(root: string, evaluation: TriageEvaluation): Promise<TriageFinalization>;
52
+ export declare function recordHumanDecision(root: string, escalationId: string, decision: HumanDecision): boolean;
53
+ export interface RunTriageOptions {
54
+ all?: boolean;
55
+ apply?: boolean;
56
+ threshold?: string;
57
+ json?: boolean;
58
+ /** Streams, prompt mode and the provider transport are injectable for tests. */
59
+ input?: NodeJS.ReadableStream;
60
+ output?: NodeJS.WritableStream;
61
+ interactive?: boolean;
62
+ liaison?: Tier1LiaisonOptions;
63
+ }
64
+ export interface TriageRunResult {
65
+ escalationId: string;
66
+ evaluation: TriageEvaluation | TriageFailure;
67
+ unblockedTaskId: string | null;
68
+ decision?: HumanDecision;
69
+ }
70
+ export declare function runTriage(escalationIdArg?: string, targetDirArg?: string, options?: RunTriageOptions): Promise<TriageRunResult[]>;
@@ -0,0 +1,343 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import pc from 'picocolors';
5
+ import { resolveProjectRoot } from '../core/root-resolver.js';
6
+ import { withFileLockSync, withPlanLock, writeJsonAtomicSync } from '../core/lock-manager.js';
7
+ import { escalationPath, loadEscalationFile } from '../governor/store.js';
8
+ import { Tier1Liaison, } from '../core/tier1-liaison.js';
9
+ /**
10
+ * `nativ triage`: runs the Tier 1 AI Strategist on pending escalations from the terminal and
11
+ * presents human decision cards with numbered choices. Also home of the helpers that persist a
12
+ * verdict onto its escalation record, shared with the Studio triage endpoints.
13
+ */
14
+ const ESCALATION_ID = /^[A-Za-z0-9_-]{1,64}$/;
15
+ const MAX_INSTRUCTIONS = 1000;
16
+ const MAX_PROMPT_ATTEMPTS = 3;
17
+ function mutateEscalation(root, escalationId, mutate) {
18
+ const file = escalationPath(root);
19
+ return withFileLockSync(file, () => {
20
+ const data = loadEscalationFile(root, path.basename(root));
21
+ const record = data.escalations.find((e) => e?.id === escalationId);
22
+ if (!record)
23
+ return false;
24
+ mutate(record);
25
+ data.lastUpdated = new Date().toISOString();
26
+ writeJsonAtomicSync(file, data);
27
+ return true;
28
+ });
29
+ }
30
+ /** Moves a blocked task back to pending so it can be dispatched again. False when it was not blocked. */
31
+ async function unblockTask(root, taskId, note) {
32
+ const planPath = path.join(root, '.ai', 'master_plan.json');
33
+ if (!fs.existsSync(planPath))
34
+ return false;
35
+ const result = await withPlanLock(planPath, (plan, ctx) => {
36
+ const task = plan.milestones.flatMap((m) => m.tasks ?? []).find((t) => t.id === taskId);
37
+ if (!task || task.status !== 'blocked') {
38
+ ctx.abort();
39
+ return false;
40
+ }
41
+ task.status = 'pending';
42
+ task.notes = note;
43
+ return true;
44
+ });
45
+ return result === true;
46
+ }
47
+ /**
48
+ * Only pending escalations may be triaged: re-running a resolved one could re-apply its proposal.
49
+ * Returns an error message, or null when the escalation can be evaluated.
50
+ */
51
+ export function triageBlocker(root, escalationId) {
52
+ const record = loadEscalationFile(root, path.basename(root)).escalations.find((e) => e?.id === escalationId);
53
+ if (!record)
54
+ return `Escalation "${escalationId}" not found in .ai/escalation.json`;
55
+ if (record.status !== 'pending_review')
56
+ return `Escalation "${escalationId}" is already ${record.status}`;
57
+ return null;
58
+ }
59
+ /**
60
+ * After an evaluation: unblocks the task and closes the escalation when a patch was auto-applied,
61
+ * then stores the verdict on the escalation. Never throws; a bookkeeping failure must not hide an
62
+ * applied patch.
63
+ */
64
+ export async function finalizeTriage(root, evaluation) {
65
+ let taskId = null;
66
+ let unblockedTaskId = null;
67
+ let persisted = false;
68
+ try {
69
+ taskId = loadEscalationFile(root, path.basename(root)).escalations.find((e) => e?.id === evaluation.escalationId)?.taskId ?? null;
70
+ if (evaluation.autoPatchApplied && taskId) {
71
+ const reason = evaluation.resolution?.summary ?? 'contract patch applied';
72
+ if (await unblockTask(root, taskId, `Unblocked by Tier 1 auto-resolution of ${evaluation.escalationId}: ${reason}`)) {
73
+ unblockedTaskId = taskId;
74
+ }
75
+ }
76
+ const snapshot = {
77
+ evaluatedAt: new Date().toISOString(),
78
+ provider: evaluation.provider,
79
+ model: evaluation.model,
80
+ source: evaluation.source,
81
+ latencyMs: evaluation.latencyMs,
82
+ classification: evaluation.classification,
83
+ reasoning: evaluation.reasoning,
84
+ autoPatchApplied: evaluation.autoPatchApplied,
85
+ unblockedTaskId,
86
+ guardrails: evaluation.guardrails,
87
+ ...(evaluation.resolution
88
+ ? {
89
+ resolution: {
90
+ summary: evaluation.resolution.summary,
91
+ patch: evaluation.resolution.patch,
92
+ proofPassed: evaluation.resolution.proof.passed,
93
+ ...(evaluation.resolution.proposalId ? { proposalId: evaluation.resolution.proposalId } : {}),
94
+ },
95
+ }
96
+ : {}),
97
+ ...(evaluation.humanCard ? { humanCard: evaluation.humanCard } : {}),
98
+ };
99
+ persisted = mutateEscalation(root, evaluation.escalationId, (record) => {
100
+ record.triage = snapshot;
101
+ if (evaluation.autoPatchApplied && record.status === 'pending_review') {
102
+ // Same closing fields the Studio approve/reject endpoint writes.
103
+ const closed = record;
104
+ closed.status = 'resolved';
105
+ closed.resolutionNotes = `Auto-resolved by Tier 1 (${evaluation.model}): ${evaluation.resolution?.summary ?? 'contract patch applied'}`;
106
+ closed.resolvedAt = snapshot.evaluatedAt;
107
+ closed.resolution = { decision: 'approve', proposalApplied: true, ruleId: 'TIER1_AUTO_RESOLVED', unblockedTaskId };
108
+ }
109
+ });
110
+ }
111
+ catch {
112
+ // Reported through `persisted: false`.
113
+ }
114
+ return { taskId, unblockedTaskId, persisted };
115
+ }
116
+ export function recordHumanDecision(root, escalationId, decision) {
117
+ return mutateEscalation(root, escalationId, (record) => {
118
+ record.humanDecision = decision;
119
+ });
120
+ }
121
+ /** Queues lines so answers typed (or piped) before the prompt appears are not lost. */
122
+ function createLineReader(input, output) {
123
+ const rl = readline.createInterface({ input, terminal: false });
124
+ const queue = [];
125
+ const waiters = [];
126
+ let closed = false;
127
+ rl.on('line', (line) => {
128
+ const waiter = waiters.shift();
129
+ if (waiter)
130
+ waiter(line);
131
+ else
132
+ queue.push(line);
133
+ });
134
+ rl.on('close', () => {
135
+ closed = true;
136
+ while (waiters.length)
137
+ waiters.shift()(null);
138
+ });
139
+ return {
140
+ ask(prompt) {
141
+ output.write(prompt);
142
+ if (queue.length)
143
+ return Promise.resolve(queue.shift());
144
+ if (closed)
145
+ return Promise.resolve(null);
146
+ return new Promise((resolve) => waiters.push(resolve));
147
+ },
148
+ close: () => rl.close(),
149
+ };
150
+ }
151
+ /** Asks for a number in 1..max; null on Enter, end of input, or repeated invalid answers. */
152
+ async function askChoice(reader, prompt, max, print) {
153
+ for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {
154
+ const answer = await reader.ask(prompt);
155
+ if (answer === null || !answer.trim())
156
+ return null;
157
+ const n = Number(answer.trim());
158
+ if (Number.isInteger(n) && n >= 1 && n <= max)
159
+ return n;
160
+ print(pc.yellow(` Enter a number from 1 to ${max}, or press Enter to skip.`));
161
+ }
162
+ return null;
163
+ }
164
+ function pendingEscalations(root) {
165
+ return loadEscalationFile(root, path.basename(root))
166
+ .escalations.filter((e) => e && typeof e.id === 'string' && e.status === 'pending_review');
167
+ }
168
+ function printEvaluation(print, result, taskId, unblockedTaskId) {
169
+ print('');
170
+ print(pc.bold(`Tier 1 AI Strategist`) + pc.dim(` ${result.escalationId}${taskId ? ` / ${taskId}` : ''} (${result.provider === 'deterministic' ? 'offline rules' : `${result.provider}: ${result.model}`}, ${result.latencyMs}ms)`));
171
+ if (result.classification === 'AUTO_RESOLVE' && !result.resolution) {
172
+ print(pc.green(pc.bold('Safe to proceed')) + pc.green(' No contract change is needed and nothing was written.'));
173
+ }
174
+ else if (result.classification === 'AUTO_RESOLVE' && result.resolution) {
175
+ const { patch, summary } = result.resolution;
176
+ print(pc.green(pc.bold(result.autoPatchApplied ? 'Auto-resolved' : 'Safe fix found')) + pc.green(` ${summary}`));
177
+ print(pc.dim(` ${patch.operation} ${patch.target} ${patch.path}`));
178
+ if (result.autoPatchApplied) {
179
+ print(pc.green(` Patch written to .ai/${patch.target === 'db_schema' ? 'db_schema.json' : 'api_contracts.json'}.`));
180
+ if (unblockedTaskId)
181
+ print(pc.green(` Task ${unblockedTaskId} is unblocked and pending.`));
182
+ }
183
+ else {
184
+ print(pc.white(' Proven in a sandbox copy of the contract. Re-run with --apply to write it.'));
185
+ }
186
+ }
187
+ else if (result.humanCard) {
188
+ const card = result.humanCard;
189
+ print(pc.yellow(pc.bold('Human Decision Required')));
190
+ print('');
191
+ print(pc.bold('1. What is happening?'));
192
+ print(` ${card.symptom}`);
193
+ print(pc.bold('2. Why is this happening?'));
194
+ print(` ${card.rootCause}`);
195
+ print(pc.bold('3. Who and what is affected?'));
196
+ print(` ${card.blastRadius}`);
197
+ print(pc.bold('4. Your options'));
198
+ card.options.forEach((option, i) => {
199
+ const tag = option.recommended && !/recommended/i.test(option.label) ? pc.cyan(' (Recommended)') : '';
200
+ print(` [${i + 1}] ${option.label}${tag}`);
201
+ print(pc.dim(` ${option.outcome}`));
202
+ });
203
+ print(` [${card.options.length + 1}] Give custom instructions`);
204
+ }
205
+ if (result.reasoning)
206
+ print(pc.dim(`\n Reasoning: ${result.reasoning}`));
207
+ for (const note of result.guardrails)
208
+ print(pc.dim(` Guardrail: ${note}`));
209
+ }
210
+ async function askDecision(reader, card, print) {
211
+ const count = card.options.length + 1;
212
+ const keys = Array.from({ length: count }, (_, i) => i + 1).join('/');
213
+ const choice = await askChoice(reader, `\nChoose [${keys}] (Enter to decide later): `, count, print);
214
+ if (choice === null)
215
+ return null;
216
+ if (choice <= card.options.length) {
217
+ const option = card.options[choice - 1];
218
+ return { optionId: option.id, label: option.label, outcome: option.outcome };
219
+ }
220
+ const text = (await reader.ask('Instructions for the team: '))?.trim() ?? '';
221
+ if (!text)
222
+ return null;
223
+ return { optionId: 'custom', label: 'Custom instructions', instructions: text.slice(0, MAX_INSTRUCTIONS) };
224
+ }
225
+ export async function runTriage(escalationIdArg, targetDirArg, options = {}) {
226
+ const output = options.output ?? process.stdout;
227
+ const print = (line = '') => {
228
+ output.write(`${line}\n`);
229
+ };
230
+ const fail = (message) => {
231
+ if (options.json)
232
+ print(JSON.stringify({ ok: false, error: message }, null, 2));
233
+ else
234
+ print(pc.red(`\nTriage failed: ${message}\n`));
235
+ process.exitCode = 1;
236
+ return [];
237
+ };
238
+ // `nativ triage <dir>` reads naturally, so a lone directory argument is the target, not an id.
239
+ let escalationId = escalationIdArg;
240
+ let targetDir = targetDirArg;
241
+ if (escalationId && !targetDir && !ESCALATION_ID.test(escalationId) && fs.existsSync(escalationId)) {
242
+ targetDir = escalationId;
243
+ escalationId = undefined;
244
+ }
245
+ const root = resolveProjectRoot(targetDir);
246
+ const threshold = options.threshold;
247
+ if (threshold !== undefined && threshold !== 'safe_contracts_only' && threshold !== 'all_non_destructive') {
248
+ return fail("--threshold must be 'safe_contracts_only' or 'all_non_destructive'");
249
+ }
250
+ if (escalationId !== undefined && !ESCALATION_ID.test(escalationId)) {
251
+ return fail(`"${escalationId}" is not an escalation id such as esc-01`);
252
+ }
253
+ const liaison = new Tier1Liaison(root, {
254
+ ...options.liaison,
255
+ config: { autoTriageEnabled: Boolean(options.apply), riskThreshold: threshold ?? 'safe_contracts_only' },
256
+ });
257
+ const interactive = !options.json && (options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY));
258
+ const reader = interactive ? createLineReader(options.input ?? process.stdin, output) : null;
259
+ try {
260
+ const pending = pendingEscalations(root);
261
+ let targets;
262
+ if (escalationId) {
263
+ targets = [escalationId];
264
+ }
265
+ else if (options.all) {
266
+ targets = pending.map((e) => e.id);
267
+ }
268
+ else if (!pending.length) {
269
+ targets = [];
270
+ }
271
+ else if (reader) {
272
+ print(pc.bold('\nPending escalations'));
273
+ pending.forEach((e, i) => print(` [${i + 1}] ${pc.cyan(e.id)} ${pc.dim(e.type)} ${e.taskId} ${e.summary}`));
274
+ const choice = await askChoice(reader, `\nChoose an escalation [1-${pending.length}] (Enter to cancel): `, pending.length, print);
275
+ targets = choice === null ? [] : [pending[choice - 1].id];
276
+ if (choice === null)
277
+ print(pc.dim('No escalation selected.'));
278
+ }
279
+ else {
280
+ return fail(`${pending.length} pending escalation(s): pass an escalation id or --all (${pending.map((e) => e.id).join(', ')})`);
281
+ }
282
+ if (!options.json && !liaison.hasApiKey()) {
283
+ print(pc.dim('No AI provider is available (sign in to Claude Code, or set ANTHROPIC_API_KEY or GEMINI_API_KEY): using the offline rules engine.'));
284
+ }
285
+ if (!targets.length) {
286
+ if (options.json)
287
+ print(JSON.stringify({ ok: true, evaluations: [] }, null, 2));
288
+ else if (!pending.length)
289
+ print(pc.green('\nNo pending escalations. Nothing to triage.\n'));
290
+ return [];
291
+ }
292
+ const results = [];
293
+ for (const id of targets) {
294
+ const blocker = triageBlocker(root, id);
295
+ const evaluation = blocker ? { ok: false, escalationId: id, error: blocker } : await liaison.evaluate(id);
296
+ if (!evaluation.ok) {
297
+ results.push({ escalationId: id, evaluation, unblockedTaskId: null });
298
+ process.exitCode = 1;
299
+ if (!options.json)
300
+ print(pc.red(`\n${id}: ${evaluation.error}`));
301
+ continue;
302
+ }
303
+ const { taskId, unblockedTaskId } = await finalizeTriage(root, evaluation);
304
+ const result = { escalationId: id, evaluation, unblockedTaskId };
305
+ results.push(result);
306
+ if (options.json)
307
+ continue;
308
+ printEvaluation(print, evaluation, taskId, unblockedTaskId);
309
+ // Choices are only asked for one escalation at a time; --all is a report.
310
+ if (reader && evaluation.humanCard && targets.length === 1) {
311
+ const picked = await askDecision(reader, evaluation.humanCard, print);
312
+ if (!picked) {
313
+ print(pc.dim(`Decision deferred. ${id} stays pending.`));
314
+ }
315
+ else {
316
+ const decision = { ...picked, decidedAt: new Date().toISOString(), decidedVia: 'cli' };
317
+ if (recordHumanDecision(root, id, decision)) {
318
+ result.decision = decision;
319
+ print(pc.green(`\nRecorded your choice on ${id}: ${decision.label}.`));
320
+ print(pc.white(`Approve or reject it in Studio (Self-Healing tab) or with the architect agent to unblock ${taskId ?? 'the task'}.`));
321
+ }
322
+ else {
323
+ print(pc.red(`\nCould not record the decision: ${id} is no longer in .ai/escalation.json.`));
324
+ process.exitCode = 1;
325
+ }
326
+ }
327
+ }
328
+ }
329
+ if (options.json) {
330
+ print(JSON.stringify({ ok: results.every((r) => r.evaluation.ok), evaluations: results }, null, 2));
331
+ }
332
+ else {
333
+ const evaluated = results.filter((r) => r.evaluation.ok).map((r) => r.evaluation);
334
+ const safe = evaluated.filter((e) => e.classification === 'AUTO_RESOLVE').length;
335
+ const applied = evaluated.filter((e) => e.autoPatchApplied).length;
336
+ print(pc.dim(`\nEvaluated ${evaluated.length}: ${safe} safe (${applied} patched), ${evaluated.length - safe} need a human.\n`));
337
+ }
338
+ return results;
339
+ }
340
+ finally {
341
+ reader?.close();
342
+ }
343
+ }
@@ -0,0 +1,14 @@
1
+ export interface UpdateOptions {
2
+ /** Also replace directive and role-guide files that were edited by hand. */
3
+ force?: boolean;
4
+ }
5
+ /**
6
+ * `nativ update`: brings a project up to the current templates without destroying its own work.
7
+ *
8
+ * - Directives (CLAUDE.md, GEMINI.md) and role guides (.ai/subagents/) are refreshed only when nativ wrote them
9
+ * and nobody edited them; anything edited is kept and reported. `--force` replaces those too.
10
+ * - AGENTS.md, .claude/agents/, settings and the MCP entry follow the same merge rules as `nativ setup`,
11
+ * keeping the command already configured.
12
+ * - Contracts, the plan and the project context are never touched. Missing contracts are backfilled.
13
+ */
14
+ export declare function runUpdate(targetDirArg?: string, options?: UpdateOptions): Promise<void>;
@@ -0,0 +1,112 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { detectProject } from '../scanner/detector.js';
5
+ import { applySetup, getTemplatesDir, planTemplateFile } from '../core/setup-assets.js';
6
+ import { printSetupResult } from './setup.js';
7
+ import { runValidate } from './validate.js';
8
+ const ICON = { created: '+', updated: '~', unchanged: '=', skipped: '!' };
9
+ function printChange(change) {
10
+ const paint = change.action === 'created' ? pc.green : change.action === 'updated' ? pc.cyan : change.action === 'skipped' ? pc.yellow : pc.dim;
11
+ const verb = change.action === 'skipped' ? 'kept' : change.action;
12
+ console.log(paint(` ${ICON[change.action]} ${change.path}`) + pc.dim(` ${verb}${change.detail ? `: ${change.detail}` : ''}`));
13
+ }
14
+ /**
15
+ * `nativ update`: brings a project up to the current templates without destroying its own work.
16
+ *
17
+ * - Directives (CLAUDE.md, GEMINI.md) and role guides (.ai/subagents/) are refreshed only when nativ wrote them
18
+ * and nobody edited them; anything edited is kept and reported. `--force` replaces those too.
19
+ * - AGENTS.md, .claude/agents/, settings and the MCP entry follow the same merge rules as `nativ setup`,
20
+ * keeping the command already configured.
21
+ * - Contracts, the plan and the project context are never touched. Missing contracts are backfilled.
22
+ */
23
+ export async function runUpdate(targetDirArg, options = {}) {
24
+ const targetDir = path.resolve(targetDirArg || process.cwd());
25
+ const templatesDir = getTemplatesDir();
26
+ const aiDir = path.join(targetDir, '.ai');
27
+ console.log(pc.bold(pc.cyan(`\nnativ update: synchronizing directives and agents in ${targetDir}\n`)));
28
+ if (!fs.existsSync(aiDir)) {
29
+ console.error(pc.red(`✖ No .ai/ directory found in: ${targetDir}`));
30
+ console.log(pc.yellow('Run `nativ init` first to scaffold the initial environment.\n'));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ const projectInfo = detectProject(targetDir);
35
+ const timestamp = new Date().toISOString();
36
+ // 1. Directives and role guides: refresh what nativ wrote, keep what a person edited
37
+ const derived = [
38
+ { rel: 'CLAUDE.md', template: path.join(templatesDir, 'CLAUDE.md'), dest: path.join(targetDir, 'CLAUDE.md'), key: 'CLAUDE.md' },
39
+ { rel: 'GEMINI.md', template: path.join(templatesDir, 'GEMINI.md'), dest: path.join(targetDir, 'GEMINI.md'), key: 'GEMINI.md' },
40
+ ];
41
+ const templatesSubagentDir = path.join(templatesDir, 'dot-ai', 'subagents');
42
+ if (fs.existsSync(templatesSubagentDir)) {
43
+ for (const file of fs.readdirSync(templatesSubagentDir).sort()) {
44
+ const src = path.join(templatesSubagentDir, file);
45
+ if (fs.statSync(src).isFile()) {
46
+ derived.push({ rel: `.ai/subagents/${file}`, template: src, dest: path.join(aiDir, 'subagents', file), key: `subagents/${file}` });
47
+ }
48
+ }
49
+ }
50
+ console.log(pc.bold('Directives and role guides'));
51
+ let kept = 0;
52
+ for (const item of derived) {
53
+ if (!fs.existsSync(item.template))
54
+ continue;
55
+ const existing = fs.existsSync(item.dest) ? fs.readFileSync(item.dest, 'utf8') : null;
56
+ const plan = planTemplateFile(existing, fs.readFileSync(item.template, 'utf8'), item.key, options.force === true);
57
+ if (plan.content !== undefined) {
58
+ fs.mkdirSync(path.dirname(item.dest), { recursive: true });
59
+ fs.writeFileSync(item.dest, plan.content, 'utf8');
60
+ }
61
+ if (plan.action === 'skipped')
62
+ kept++;
63
+ printChange({ path: item.rel, action: plan.action, detail: plan.detail });
64
+ }
65
+ // 2. Everything `nativ setup` manages: AGENTS.md, .claude/agents/, settings, MCP server
66
+ console.log(pc.bold('\nClaude Code configuration'));
67
+ const setup = applySetup(targetDir, { force: options.force });
68
+ printSetupResult(setup);
69
+ // 3. Backfill missing core contracts safely (NEVER overwrite existing project state)
70
+ console.log(pc.bold('\nContracts'));
71
+ const apiContractsPath = path.join(aiDir, 'api_contracts.json');
72
+ if (!fs.existsSync(apiContractsPath)) {
73
+ const apiTemplate = fs.readFileSync(path.join(templatesDir, 'dot-ai/api_contracts.json'), 'utf8');
74
+ const apiContent = apiTemplate
75
+ .replace(/\{\{PROJECT_NAME\}\}/g, projectInfo.projectName)
76
+ .replace(/\{\{TIMESTAMP\}\}/g, timestamp);
77
+ fs.writeFileSync(apiContractsPath, apiContent, 'utf8');
78
+ console.log(pc.green('✔ Added missing contract: .ai/api_contracts.json'));
79
+ }
80
+ else {
81
+ console.log(pc.dim(' • Preserved existing .ai/api_contracts.json'));
82
+ }
83
+ const dbPath = path.join(aiDir, 'db_schema.json');
84
+ if (!fs.existsSync(dbPath)) {
85
+ const dbTemplate = fs.readFileSync(path.join(templatesDir, 'dot-ai/db_schema.json'), 'utf8');
86
+ const dbContent = dbTemplate
87
+ .replace(/\{\{PROJECT_NAME\}\}/g, projectInfo.projectName)
88
+ .replace(/\{\{TIMESTAMP\}\}/g, timestamp)
89
+ .replace(/\{\{DETECTED_DATABASE_ENGINE\}\}/g, projectInfo.databaseOrm !== 'None detected' ? projectInfo.databaseOrm : 'PostgreSQL');
90
+ fs.writeFileSync(dbPath, dbContent, 'utf8');
91
+ console.log(pc.green('✔ Added missing contract: .ai/db_schema.json'));
92
+ }
93
+ else {
94
+ console.log(pc.dim(' • Preserved existing .ai/db_schema.json'));
95
+ }
96
+ const uiPath = path.join(aiDir, 'ui_specs.md');
97
+ if (!fs.existsSync(uiPath)) {
98
+ const uiTemplate = fs.readFileSync(path.join(templatesDir, 'dot-ai/ui_specs.md'), 'utf8');
99
+ fs.writeFileSync(uiPath, uiTemplate, 'utf8');
100
+ console.log(pc.green('✔ Added missing spec: .ai/ui_specs.md'));
101
+ }
102
+ else {
103
+ console.log(pc.dim(' • Preserved existing .ai/ui_specs.md'));
104
+ }
105
+ console.log(pc.dim(' • Preserved existing .ai/master_plan.json (active milestones intact)'));
106
+ console.log(pc.dim(' • Preserved existing .ai/context.md (project context intact)'));
107
+ if (kept > 0) {
108
+ console.log(pc.yellow(`\n! ${kept} file(s) were kept because they were edited by hand. Compare them with the templates in the nativ package, or run \`nativ update --force\` to replace them.`));
109
+ }
110
+ console.log(pc.bold(pc.green('\n✨ Framework sync complete! Verifying updated project...\n')));
111
+ await runValidate(targetDir);
112
+ }
@@ -0,0 +1 @@
1
+ export declare function runValidate(targetDirArg?: string): Promise<void>;