@njeybe/nativ 2.4.1 → 2.5.1
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 +26 -0
- package/README.md +26 -0
- package/dist/commands/db/db-formatter.d.ts +5 -0
- package/dist/commands/db/db-formatter.js +116 -0
- package/dist/commands/db/db-introspect.d.ts +11 -0
- package/dist/commands/db/db-introspect.js +84 -0
- package/dist/commands/db.js +4 -206
- package/dist/commands/profile.d.ts +3 -0
- package/dist/commands/profile.js +39 -0
- package/dist/commands/task/task-add.d.ts +11 -0
- package/dist/commands/task/task-add.js +169 -0
- package/dist/commands/task/task-common.d.ts +57 -0
- package/dist/commands/task/task-common.js +45 -0
- package/dist/commands/task/task-escalate.d.ts +7 -0
- package/dist/commands/task/task-escalate.js +119 -0
- package/dist/commands/task/task-lifecycle.d.ts +5 -0
- package/dist/commands/task/task-lifecycle.js +252 -0
- package/dist/commands/task/task-query.d.ts +10 -0
- package/dist/commands/task/task-query.js +269 -0
- package/dist/commands/task.d.ts +9 -87
- package/dist/commands/task.js +8 -850
- package/dist/commands/triage/triage-store.d.ts +56 -0
- package/dist/commands/triage/triage-store.js +108 -0
- package/dist/commands/triage/triage-ui.d.ts +14 -0
- package/dist/commands/triage/triage-ui.js +104 -0
- package/dist/commands/triage.d.ts +3 -52
- package/dist/commands/triage.js +8 -216
- package/dist/core/enforcement.d.ts +1 -0
- package/dist/core/enforcement.js +19 -0
- package/dist/core/human-gate.d.ts +2 -1
- package/dist/core/human-gate.js +9 -2
- package/dist/core/profiles.d.ts +14 -0
- package/dist/core/profiles.js +82 -0
- package/dist/core/setup/config-merger.d.ts +59 -0
- package/dist/core/setup/config-merger.js +107 -0
- package/dist/core/setup/session-context.d.ts +4 -0
- package/dist/core/setup/session-context.js +65 -0
- package/dist/core/setup/setup-planner.d.ts +14 -0
- package/dist/core/setup/setup-planner.js +180 -0
- package/dist/core/setup/template-planner.d.ts +8 -0
- package/dist/core/setup/template-planner.js +32 -0
- package/dist/core/setup/types.d.ts +65 -0
- package/dist/core/setup/types.js +55 -0
- package/dist/core/setup-assets.d.ts +8 -119
- package/dist/core/setup-assets.js +7 -437
- package/dist/core/tier1-liaison.d.ts +2 -0
- package/dist/core/tier1-liaison.js +64 -3
- package/dist/core/verifier/executor.d.ts +5 -0
- package/dist/core/verifier/executor.js +62 -0
- package/dist/core/verifier/phases.d.ts +5 -0
- package/dist/core/verifier/phases.js +71 -0
- package/dist/core/verifier/task-verifier.d.ts +37 -0
- package/dist/core/verifier/task-verifier.js +233 -0
- package/dist/core/verifier/types.d.ts +51 -0
- package/dist/core/verifier/types.js +1 -0
- package/dist/core/verifier.d.ts +5 -93
- package/dist/core/verifier.js +5 -359
- package/dist/db/introspector.js +59 -59
- package/dist/governor/evaluator.d.ts +4 -64
- package/dist/governor/evaluator.js +21 -686
- package/dist/governor/patch-applier.d.ts +8 -0
- package/dist/governor/patch-applier.js +121 -0
- package/dist/governor/self-healing.d.ts +24 -0
- package/dist/governor/self-healing.js +351 -0
- package/dist/governor/test-integrity.d.ts +41 -0
- package/dist/governor/test-integrity.js +237 -0
- package/dist/index.js +8 -0
- package/dist/mcp/capture.d.ts +4 -0
- package/dist/mcp/capture.js +33 -0
- package/dist/mcp/resources.d.ts +39 -0
- package/dist/mcp/resources.js +44 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +14 -89
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/runner/agent-supervisor.d.ts +8 -302
- package/dist/runner/agent-supervisor.js +41 -1030
- package/dist/runner/cli-config.d.ts +84 -0
- package/dist/runner/cli-config.js +215 -0
- package/dist/runner/native-driver.d.ts +18 -0
- package/dist/runner/native-driver.js +208 -0
- package/dist/runner/native-engine.d.ts +27 -0
- package/dist/runner/native-engine.js +184 -0
- package/dist/runner/native-tools.d.ts +31 -0
- package/dist/runner/native-tools.js +241 -0
- package/dist/runner/process-utils.d.ts +12 -0
- package/dist/runner/process-utils.js +96 -0
- package/dist/runner/run-lifecycle.d.ts +6 -0
- package/dist/runner/run-lifecycle.js +42 -0
- package/dist/runner/types.d.ts +215 -0
- package/dist/runner/types.js +41 -0
- package/dist/scanner/detector.js +42 -663
- package/dist/scanner/detectors/ecosystem-detector.d.ts +16 -0
- package/dist/scanner/detectors/ecosystem-detector.js +354 -0
- package/dist/scanner/detectors/orm-detector.d.ts +5 -0
- package/dist/scanner/detectors/orm-detector.js +59 -0
- package/dist/scanner/detectors/workspace-detector.d.ts +19 -0
- package/dist/scanner/detectors/workspace-detector.js +266 -0
- package/dist/server/cli-runner.d.ts +15 -0
- package/dist/server/cli-runner.js +43 -0
- package/dist/server/event-hub.d.ts +42 -0
- package/dist/server/event-hub.js +169 -0
- package/dist/server/http-utils.d.ts +15 -0
- package/dist/server/http-utils.js +56 -0
- package/dist/server/plan-utils.d.ts +9 -0
- package/dist/server/plan-utils.js +31 -0
- package/dist/server/routes/db-routes.d.ts +57 -0
- package/dist/server/routes/db-routes.js +484 -0
- package/dist/server/routes/task-routes.d.ts +266 -0
- package/dist/server/routes/task-routes.js +285 -0
- package/dist/server/routes/telemetry-routes.d.ts +128 -0
- package/dist/server/routes/telemetry-routes.js +179 -0
- package/dist/server/routes/triage-routes.d.ts +81 -0
- package/dist/server/routes/triage-routes.js +244 -0
- package/dist/server/routes/worktree-routes.d.ts +31 -0
- package/dist/server/routes/worktree-routes.js +144 -0
- package/dist/server/studio-server.d.ts +4 -11
- package/dist/server/studio-server.js +34 -1578
- package/dist/server/studio-ui/shell/pipeline-state.js +2 -0
- package/dist/server/studio-ui/shell/triage.js +10 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +4 -2
- package/plugin/agents/verifier.md +1 -1
- package/plugin/agents/worker.md +7 -4
- package/plugin/skills/nativ/SKILL.md +1 -0
- package/templates/.claudeignore +62 -45
- package/templates/AGENTS.md +1 -0
- package/templates/CLAUDE.md +13 -0
- package/templates/claude-agents/architect.md +4 -2
- package/templates/claude-agents/verifier.md +1 -1
- package/templates/claude-agents/worker.md +7 -4
- package/templates/dot-ai/api_contracts.json +66 -66
- package/templates/dot-ai/context.md +38 -38
- package/templates/dot-ai/db_schema.json +39 -39
- package/templates/dot-ai/subagents/architect.md +54 -0
- package/dist/commands/session.d.ts +0 -22
- package/dist/commands/session.js +0 -106
- package/dist/core/session-memory.d.ts +0 -42
- package/dist/core/session-memory.js +0 -121
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const MCP_SERVER_NAME = 'nativ';
|
|
7
|
+
export const AGENT_FILES = ['architect.md', 'worker.md', 'verifier.md', 'explorer.md'];
|
|
8
|
+
export const MANAGED_PREFIX = '<!-- nativ:managed sha256=';
|
|
9
|
+
export const MANAGED_PATTERN = /\n?<!-- nativ:managed sha256=([0-9a-f]{12}) -->\s*$/;
|
|
10
|
+
export const PRE_TOOL_USE_MATCHER = 'Write|Edit|MultiEdit|NotebookEdit';
|
|
11
|
+
export const SESSION_START_MATCHER = 'startup|resume|clear|compact';
|
|
12
|
+
export const HOOK_TIMEOUT_SECONDS = 10;
|
|
13
|
+
export function defaultHasBinary(name) {
|
|
14
|
+
try {
|
|
15
|
+
return spawnSync(process.platform === 'win32' ? 'where' : 'which', [name], { stdio: 'ignore' }).status === 0;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function quoteIfNeeded(part) {
|
|
22
|
+
return /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part;
|
|
23
|
+
}
|
|
24
|
+
export function resolveCliInvocation(options = {}) {
|
|
25
|
+
const explicit = options.command?.trim();
|
|
26
|
+
if (explicit) {
|
|
27
|
+
const [command, ...prefixArgs] = explicit.split(/\s+/);
|
|
28
|
+
return { command, prefixArgs, portable: ![command, ...prefixArgs].some((p) => path.isAbsolute(p)) };
|
|
29
|
+
}
|
|
30
|
+
if ((options.hasBinary ?? defaultHasBinary)('nativ'))
|
|
31
|
+
return { command: 'nativ', prefixArgs: [], portable: true };
|
|
32
|
+
// Not installed globally: fall back to this very build, which only works on this machine.
|
|
33
|
+
const self = path.resolve(__dirname, '..', '..', '..', 'bin', 'cli.js').replace(/\\/g, '/');
|
|
34
|
+
return { command: 'node', prefixArgs: [self], portable: false };
|
|
35
|
+
}
|
|
36
|
+
/** The invocation as a shell string, e.g. `npx -y @njeybe/nativ`, quoting parts that need it. */
|
|
37
|
+
export function cliString(invocation) {
|
|
38
|
+
return [invocation.command, ...invocation.prefixArgs].map(quoteIfNeeded).join(' ');
|
|
39
|
+
}
|
|
40
|
+
export const shortHash = (text) => crypto.createHash('sha256').update(text).digest('hex').slice(0, 12);
|
|
41
|
+
/** CRLF and a leading BOM to plain LF. */
|
|
42
|
+
export const toLf = (text) => text.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
43
|
+
/** Appends the marker that lets a later run tell "nativ wrote this, untouched" from "a person edited this". */
|
|
44
|
+
export function stampManaged(content) {
|
|
45
|
+
const body = toLf(content).replace(/\s+$/, '');
|
|
46
|
+
return `${body}\n\n${MANAGED_PREFIX}${shortHash(body)} -->\n`;
|
|
47
|
+
}
|
|
48
|
+
export function managedState(content) {
|
|
49
|
+
const lf = toLf(content);
|
|
50
|
+
const match = MANAGED_PATTERN.exec(lf);
|
|
51
|
+
if (!match)
|
|
52
|
+
return 'unmanaged';
|
|
53
|
+
const body = lf.slice(0, match.index).replace(/\s+$/, '');
|
|
54
|
+
return shortHash(body) === match[1] ? 'pristine' : 'modified';
|
|
55
|
+
}
|
|
@@ -1,123 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nativ · Single source for every file `nativ setup` generates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* existing files: user-authored settings, servers and agent files are never overwritten.
|
|
4
|
+
* Barrel module re-exporting all setup asset planning, config merging,
|
|
5
|
+
* marker validation, and session context creation.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
path: string;
|
|
15
|
-
action: AssetAction;
|
|
16
|
-
detail?: string;
|
|
17
|
-
}
|
|
18
|
-
export interface CliInvocation {
|
|
19
|
-
command: string;
|
|
20
|
-
prefixArgs: string[];
|
|
21
|
-
/** False when the command embeds an absolute path, which will not work on another machine. */
|
|
22
|
-
portable: boolean;
|
|
23
|
-
}
|
|
24
|
-
export interface SetupOptions {
|
|
25
|
-
/** How Claude Code should invoke nativ, e.g. `npx -y @njeybe/nativ`. Defaults to `nativ` when it is on PATH. */
|
|
26
|
-
command?: string;
|
|
27
|
-
enforcement?: EnforcementMode;
|
|
28
|
-
/** Also overwrite agent files that were edited by hand or are not nativ's. */
|
|
29
|
-
force?: boolean;
|
|
30
|
-
hasBinary?: (name: string) => boolean;
|
|
31
|
-
templatesDir?: string;
|
|
32
|
-
}
|
|
33
|
-
export interface SetupPlan {
|
|
34
|
-
root: string;
|
|
35
|
-
invocation: CliInvocation;
|
|
36
|
-
changes: AssetChange[];
|
|
37
|
-
warnings: string[];
|
|
38
|
-
/** Full new content per changed path, keyed by the same relative path. */
|
|
39
|
-
writes: Map<string, string>;
|
|
40
|
-
}
|
|
41
|
-
export declare function defaultHasBinary(name: string): boolean;
|
|
42
|
-
export declare function resolveCliInvocation(options?: {
|
|
43
|
-
command?: string;
|
|
44
|
-
hasBinary?: (name: string) => boolean;
|
|
45
|
-
}): CliInvocation;
|
|
46
|
-
/** The invocation as a shell string, e.g. `npx -y @njeybe/nativ`, quoting parts that need it. */
|
|
47
|
-
export declare function cliString(invocation: CliInvocation): string;
|
|
48
|
-
export interface DesiredClaudeConfig {
|
|
49
|
-
cli: string;
|
|
50
|
-
mcpServer: {
|
|
51
|
-
command: string;
|
|
52
|
-
args: string[];
|
|
53
|
-
};
|
|
54
|
-
allow: string[];
|
|
55
|
-
deny: string[];
|
|
56
|
-
ask: string[];
|
|
57
|
-
preToolUse: {
|
|
58
|
-
matcher: string;
|
|
59
|
-
command: string;
|
|
60
|
-
timeout: number;
|
|
61
|
-
};
|
|
62
|
-
sessionStart: {
|
|
63
|
-
matcher: string;
|
|
64
|
-
command: string;
|
|
65
|
-
timeout: number;
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
export declare function buildDesiredConfig(invocation: CliInvocation): DesiredClaudeConfig;
|
|
69
|
-
type Json = Record<string, unknown>;
|
|
70
|
-
export interface MergeResult {
|
|
71
|
-
merged: Json;
|
|
72
|
-
changes: string[];
|
|
73
|
-
warnings: string[];
|
|
74
|
-
}
|
|
75
|
-
export declare function mergeClaudeSettings(existing: Json, desired: DesiredClaudeConfig): MergeResult;
|
|
76
|
-
export declare function mergeMcpConfig(existing: Json, desired: DesiredClaudeConfig): {
|
|
77
|
-
merged: Json;
|
|
78
|
-
changed: boolean;
|
|
79
|
-
};
|
|
80
|
-
/** CRLF and a leading BOM to plain LF. */
|
|
81
|
-
export declare const toLf: (text: string) => string;
|
|
82
|
-
/** Appends the marker that lets a later run tell "nativ wrote this, untouched" from "a person edited this". */
|
|
83
|
-
export declare function stampManaged(content: string): string;
|
|
84
|
-
export type ManagedState = 'pristine' | 'modified' | 'unmanaged';
|
|
85
|
-
export declare function managedState(content: string): ManagedState;
|
|
86
|
-
export interface TemplateFilePlan {
|
|
87
|
-
action: AssetAction;
|
|
88
|
-
detail?: string;
|
|
89
|
-
/** The stamped content to write; absent when nothing should change. */
|
|
90
|
-
content?: string;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Decides what to do with a file derived from a template (a directive or a role guide).
|
|
94
|
-
* Missing files are created; files nativ wrote and nobody edited (marker intact, or matching a template nativ
|
|
95
|
-
* shipped before markers existed) follow the template; anything edited by hand is kept unless `force` is set.
|
|
96
|
-
* `legacyKey` names the file in LEGACY_TEMPLATE_HASHES.
|
|
97
|
-
*/
|
|
98
|
-
export declare function planTemplateFile(existing: string | null, template: string, legacyKey: string, force?: boolean): TemplateFilePlan;
|
|
99
|
-
export declare function getTemplatesDir(): string;
|
|
100
|
-
/**
|
|
101
|
-
* The command already registered for the nativ MCP server in `.mcp.json`, when it has the shape setup writes
|
|
102
|
-
* (`<command> <args...> mcp`). Reusing it keeps hooks and permissions in step with what the project chose, so a
|
|
103
|
-
* plain `nativ setup` or `nativ doctor --fix` never silently swaps a deliberate `--command` for auto-detection.
|
|
104
|
-
*/
|
|
105
|
-
export declare function configuredInvocation(root: string): CliInvocation | null;
|
|
106
|
-
/** Computes what `nativ setup` would do, without touching disk. */
|
|
107
|
-
export declare function planSetup(rootArg: string, options?: SetupOptions): SetupPlan;
|
|
108
|
-
export interface SetupResult {
|
|
109
|
-
root: string;
|
|
110
|
-
dryRun: boolean;
|
|
111
|
-
invocation: CliInvocation;
|
|
112
|
-
changes: AssetChange[];
|
|
113
|
-
warnings: string[];
|
|
114
|
-
}
|
|
115
|
-
/** Writes the planned differences. Idempotent: applying twice changes nothing the second time. */
|
|
116
|
-
export declare function applySetup(rootArg: string, options?: SetupOptions & {
|
|
117
|
-
dryRun?: boolean;
|
|
118
|
-
}): SetupResult;
|
|
119
|
-
/** A few lines of orientation for a new Claude Code session, or null when this is not a nativ project. */
|
|
120
|
-
export declare function buildSessionContext(cwd: string, env?: NodeJS.ProcessEnv): string | null;
|
|
121
|
-
/** `nativ hook context`: prints the SessionStart hook output. Never throws and always prints valid JSON or nothing. */
|
|
122
|
-
export declare function runSessionContext(targetDirArg?: string, write?: (text: string) => void): void;
|
|
123
|
-
export {};
|
|
7
|
+
export * from './setup/types.js';
|
|
8
|
+
export type { DesiredClaudeConfig, Json, HookGroup, MergeResult, } from './setup/config-merger.js';
|
|
9
|
+
export { SECRET_READ_DENY, buildDesiredConfig, isObject, unionStrings, mergeHook, mergeClaudeSettings, mergeMcpConfig, } from './setup/config-merger.js';
|
|
10
|
+
export { planTemplateFile } from './setup/template-planner.js';
|
|
11
|
+
export { getTemplatesDir, configuredInvocation, planSetup, applySetup, } from './setup/setup-planner.js';
|
|
12
|
+
export { buildSessionContext, runSessionContext, } from './setup/session-context.js';
|
|
@@ -1,441 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nativ · Single source for every file `nativ setup` generates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* existing files: user-authored settings, servers and agent files are never overwritten.
|
|
4
|
+
* Barrel module re-exporting all setup asset planning, config merging,
|
|
5
|
+
* marker validation, and session context creation.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { ROLE_ENV, loadEnforcementMode, parseJsonLoose } from './enforcement.js';
|
|
14
|
-
import { resolveProjectRoot } from './root-resolver.js';
|
|
15
|
-
import { isLegacyTemplate, normalizeForHash } from './legacy-templates.js';
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
export const MCP_SERVER_NAME = 'nativ';
|
|
18
|
-
export const AGENT_FILES = ['architect.md', 'worker.md', 'verifier.md', 'explorer.md'];
|
|
19
|
-
const MANAGED_PREFIX = '<!-- nativ:managed sha256=';
|
|
20
|
-
const MANAGED_PATTERN = /\n?<!-- nativ:managed sha256=([0-9a-f]{12}) -->\s*$/;
|
|
21
|
-
const PRE_TOOL_USE_MATCHER = 'Write|Edit|MultiEdit|NotebookEdit';
|
|
22
|
-
const SESSION_START_MATCHER = 'startup|resume|clear|compact';
|
|
23
|
-
const HOOK_TIMEOUT_SECONDS = 10;
|
|
24
|
-
// ─── CLI invocation ────────────────────────────────────────────────────────────────────────────────
|
|
25
|
-
export function defaultHasBinary(name) {
|
|
26
|
-
try {
|
|
27
|
-
return spawnSync(process.platform === 'win32' ? 'where' : 'which', [name], { stdio: 'ignore' }).status === 0;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function quoteIfNeeded(part) {
|
|
34
|
-
return /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part;
|
|
35
|
-
}
|
|
36
|
-
export function resolveCliInvocation(options = {}) {
|
|
37
|
-
const explicit = options.command?.trim();
|
|
38
|
-
if (explicit) {
|
|
39
|
-
const [command, ...prefixArgs] = explicit.split(/\s+/);
|
|
40
|
-
return { command, prefixArgs, portable: ![command, ...prefixArgs].some((p) => path.isAbsolute(p)) };
|
|
41
|
-
}
|
|
42
|
-
if ((options.hasBinary ?? defaultHasBinary)('nativ'))
|
|
43
|
-
return { command: 'nativ', prefixArgs: [], portable: true };
|
|
44
|
-
// Not installed globally: fall back to this very build, which only works on this machine.
|
|
45
|
-
const self = path.resolve(__dirname, '..', '..', 'bin', 'cli.js').replace(/\\/g, '/');
|
|
46
|
-
return { command: 'node', prefixArgs: [self], portable: false };
|
|
47
|
-
}
|
|
48
|
-
/** The invocation as a shell string, e.g. `npx -y @njeybe/nativ`, quoting parts that need it. */
|
|
49
|
-
export function cliString(invocation) {
|
|
50
|
-
return [invocation.command, ...invocation.prefixArgs].map(quoteIfNeeded).join(' ');
|
|
51
|
-
}
|
|
52
|
-
// ─── Desired settings ──────────────────────────────────────────────────────────────────────────────
|
|
53
|
-
/** Secret files an agent must not read. Mirrors the env files the air-gap protects. */
|
|
54
|
-
const SECRET_READ_DENY = [
|
|
55
|
-
'Read(./.env)',
|
|
56
|
-
'Read(./.env.local)',
|
|
57
|
-
'Read(./.env.development)',
|
|
58
|
-
'Read(./.env.development.local)',
|
|
59
|
-
'Read(./.env.production)',
|
|
60
|
-
'Read(./.env.production.local)',
|
|
61
|
-
'Read(./**/*.pem)',
|
|
62
|
-
'Read(./**/*.key)',
|
|
63
|
-
'Read(./.nativ/*.local.json)',
|
|
64
|
-
];
|
|
65
|
-
export function buildDesiredConfig(invocation) {
|
|
66
|
-
const cli = cliString(invocation);
|
|
67
|
-
const prefixes = [...new Set([cli, 'nativ'])];
|
|
68
|
-
return {
|
|
69
|
-
cli,
|
|
70
|
-
mcpServer: { command: invocation.command, args: [...invocation.prefixArgs, 'mcp'] },
|
|
71
|
-
allow: [`Bash(${cli} *)`, `mcp__${MCP_SERVER_NAME}__*`],
|
|
72
|
-
// Deny beats allow, so the broad `nativ *` allow cannot be used to lift a guardrail or write a contract.
|
|
73
|
-
deny: [
|
|
74
|
-
...prefixes.flatMap((p) => [
|
|
75
|
-
`Bash(${p} task unlock *)`,
|
|
76
|
-
`Bash(${p} learn approve *)`,
|
|
77
|
-
`Bash(${p} learn reject *)`,
|
|
78
|
-
`Bash(${p} db sync *)`,
|
|
79
|
-
`Bash(${p} task complete * --no-verify)`,
|
|
80
|
-
`Bash(${p} task complete * --no-verify *)`,
|
|
81
|
-
]),
|
|
82
|
-
...SECRET_READ_DENY,
|
|
83
|
-
],
|
|
84
|
-
// Contract edits always need a human's yes, whichever agent asks.
|
|
85
|
-
ask: ['Edit(./.ai/**)', 'Write(./.ai/**)', 'Edit(./.nativ/**)', 'Write(./.nativ/**)'],
|
|
86
|
-
preToolUse: { matcher: PRE_TOOL_USE_MATCHER, command: `${cli} hook check`, timeout: HOOK_TIMEOUT_SECONDS },
|
|
87
|
-
sessionStart: { matcher: SESSION_START_MATCHER, command: `${cli} hook context`, timeout: HOOK_TIMEOUT_SECONDS },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function isObject(value) {
|
|
91
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
92
|
-
}
|
|
93
|
-
function unionStrings(existing, wanted) {
|
|
94
|
-
const list = Array.isArray(existing) ? existing.filter((v) => typeof v === 'string') : [];
|
|
95
|
-
const added = wanted.filter((w) => !list.includes(w));
|
|
96
|
-
return { list: [...list, ...added], added };
|
|
97
|
-
}
|
|
98
|
-
const isNativHookCommand = (command, kind) => typeof command === 'string' && new RegExp(`\\bhook ${kind}\\b`).test(command) && /nativ|cli\.js/.test(command);
|
|
99
|
-
/** Updates our hook in place when there is one, appends a group when there is not, leaves every other group alone. */
|
|
100
|
-
function mergeHook(groups, kind, want) {
|
|
101
|
-
const list = Array.isArray(groups) ? groups.filter(isObject) : [];
|
|
102
|
-
const desiredHook = { type: 'command', command: want.command, timeout: want.timeout };
|
|
103
|
-
for (const group of list) {
|
|
104
|
-
const hooks = Array.isArray(group.hooks) ? group.hooks : [];
|
|
105
|
-
const index = hooks.findIndex((h) => isNativHookCommand(h?.command, kind));
|
|
106
|
-
if (index === -1)
|
|
107
|
-
continue;
|
|
108
|
-
const before = JSON.stringify([group.matcher, hooks[index]]);
|
|
109
|
-
group.matcher = want.matcher;
|
|
110
|
-
hooks[index] = { ...hooks[index], ...desiredHook };
|
|
111
|
-
group.hooks = hooks;
|
|
112
|
-
return { groups: list, changed: JSON.stringify([group.matcher, hooks[index]]) !== before };
|
|
113
|
-
}
|
|
114
|
-
list.push({ matcher: want.matcher, hooks: [desiredHook] });
|
|
115
|
-
return { groups: list, changed: true };
|
|
116
|
-
}
|
|
117
|
-
export function mergeClaudeSettings(existing, desired) {
|
|
118
|
-
const merged = { ...existing };
|
|
119
|
-
const changes = [];
|
|
120
|
-
const warnings = [];
|
|
121
|
-
const permissions = isObject(merged.permissions) ? { ...merged.permissions } : {};
|
|
122
|
-
for (const [key, wanted] of [['allow', desired.allow], ['deny', desired.deny], ['ask', desired.ask]]) {
|
|
123
|
-
const { list, added } = unionStrings(permissions[key], wanted);
|
|
124
|
-
if (added.length) {
|
|
125
|
-
permissions[key] = list;
|
|
126
|
-
changes.push(`permissions.${key}: +${added.length}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
merged.permissions = permissions;
|
|
130
|
-
const hooks = isObject(merged.hooks) ? { ...merged.hooks } : {};
|
|
131
|
-
const pre = mergeHook(hooks.PreToolUse, 'check', desired.preToolUse);
|
|
132
|
-
hooks.PreToolUse = pre.groups;
|
|
133
|
-
if (pre.changed)
|
|
134
|
-
changes.push('hooks.PreToolUse (nativ hook check)');
|
|
135
|
-
const start = mergeHook(hooks.SessionStart, 'context', desired.sessionStart);
|
|
136
|
-
hooks.SessionStart = start.groups;
|
|
137
|
-
if (start.changed)
|
|
138
|
-
changes.push('hooks.SessionStart (nativ hook context)');
|
|
139
|
-
merged.hooks = hooks;
|
|
140
|
-
const enabled = unionStrings(merged.enabledMcpjsonServers, [MCP_SERVER_NAME]);
|
|
141
|
-
if (enabled.added.length) {
|
|
142
|
-
merged.enabledMcpjsonServers = enabled.list;
|
|
143
|
-
changes.push('enabledMcpjsonServers: +nativ');
|
|
144
|
-
}
|
|
145
|
-
if (Array.isArray(merged.disabledMcpjsonServers) && merged.disabledMcpjsonServers.includes(MCP_SERVER_NAME)) {
|
|
146
|
-
warnings.push(`.claude/settings.json disables the "${MCP_SERVER_NAME}" MCP server; leaving that choice alone.`);
|
|
147
|
-
}
|
|
148
|
-
return { merged, changes, warnings };
|
|
149
|
-
}
|
|
150
|
-
export function mergeMcpConfig(existing, desired) {
|
|
151
|
-
const servers = isObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
|
|
152
|
-
const current = servers[MCP_SERVER_NAME];
|
|
153
|
-
const want = { command: desired.mcpServer.command, args: desired.mcpServer.args };
|
|
154
|
-
const same = isObject(current) && current.command === want.command && JSON.stringify(current.args) === JSON.stringify(want.args);
|
|
155
|
-
if (!same)
|
|
156
|
-
servers[MCP_SERVER_NAME] = isObject(current) ? { ...current, ...want } : want;
|
|
157
|
-
return { merged: { ...existing, mcpServers: servers }, changed: !same };
|
|
158
|
-
}
|
|
159
|
-
// ─── Managed markdown files ────────────────────────────────────────────────────────────────────────
|
|
160
|
-
const shortHash = (text) => crypto.createHash('sha256').update(text).digest('hex').slice(0, 12);
|
|
161
|
-
/** CRLF and a leading BOM to plain LF. */
|
|
162
|
-
export const toLf = (text) => text.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
163
|
-
/** Appends the marker that lets a later run tell "nativ wrote this, untouched" from "a person edited this". */
|
|
164
|
-
export function stampManaged(content) {
|
|
165
|
-
// Always LF: a Windows checkout (core.autocrlf) or an editor may hand us CRLF, and neither is an edit.
|
|
166
|
-
const body = toLf(content).replace(/\s+$/, '');
|
|
167
|
-
return `${body}\n\n${MANAGED_PREFIX}${shortHash(body)} -->\n`;
|
|
168
|
-
}
|
|
169
|
-
export function managedState(content) {
|
|
170
|
-
const lf = toLf(content);
|
|
171
|
-
const match = MANAGED_PATTERN.exec(lf);
|
|
172
|
-
if (!match)
|
|
173
|
-
return 'unmanaged';
|
|
174
|
-
const body = lf.slice(0, match.index).replace(/\s+$/, '');
|
|
175
|
-
return shortHash(body) === match[1] ? 'pristine' : 'modified';
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Decides what to do with a file derived from a template (a directive or a role guide).
|
|
179
|
-
* Missing files are created; files nativ wrote and nobody edited (marker intact, or matching a template nativ
|
|
180
|
-
* shipped before markers existed) follow the template; anything edited by hand is kept unless `force` is set.
|
|
181
|
-
* `legacyKey` names the file in LEGACY_TEMPLATE_HASHES.
|
|
182
|
-
*/
|
|
183
|
-
export function planTemplateFile(existing, template, legacyKey, force = false) {
|
|
184
|
-
const next = stampManaged(template);
|
|
185
|
-
if (existing === null)
|
|
186
|
-
return { action: 'created', content: next };
|
|
187
|
-
if (existing === next)
|
|
188
|
-
return { action: 'unchanged' };
|
|
189
|
-
const state = managedState(existing);
|
|
190
|
-
if (state === 'pristine')
|
|
191
|
-
return { action: 'updated', detail: 'template updated', content: next };
|
|
192
|
-
if (state === 'unmanaged') {
|
|
193
|
-
if (normalizeForHash(existing) === normalizeForHash(template))
|
|
194
|
-
return { action: 'updated', detail: 'now tracked by nativ', content: next };
|
|
195
|
-
if (isLegacyTemplate(legacyKey, existing))
|
|
196
|
-
return { action: 'updated', detail: 'refreshed from an earlier nativ template', content: next };
|
|
197
|
-
}
|
|
198
|
-
if (force)
|
|
199
|
-
return { action: 'updated', detail: 'forced', content: next };
|
|
200
|
-
return {
|
|
201
|
-
action: 'skipped',
|
|
202
|
-
detail: state === 'modified'
|
|
203
|
-
? 'edited by hand (use --force to replace)'
|
|
204
|
-
: 'not an untouched nativ template, so treated as edited by hand (use --force to replace)',
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
// ─── Planning ──────────────────────────────────────────────────────────────────────────────────────
|
|
208
|
-
export function getTemplatesDir() {
|
|
209
|
-
const candidate = path.resolve(__dirname, '..', '..', 'templates');
|
|
210
|
-
return fs.existsSync(candidate) ? candidate : path.resolve(__dirname, '..', 'templates');
|
|
211
|
-
}
|
|
212
|
-
function readText(file) {
|
|
213
|
-
try {
|
|
214
|
-
return fs.readFileSync(file, 'utf8');
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function serializeJson(value) {
|
|
221
|
-
return JSON.stringify(value, null, 2) + '\n';
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* The command already registered for the nativ MCP server in `.mcp.json`, when it has the shape setup writes
|
|
225
|
-
* (`<command> <args...> mcp`). Reusing it keeps hooks and permissions in step with what the project chose, so a
|
|
226
|
-
* plain `nativ setup` or `nativ doctor --fix` never silently swaps a deliberate `--command` for auto-detection.
|
|
227
|
-
*/
|
|
228
|
-
export function configuredInvocation(root) {
|
|
229
|
-
const text = readText(path.join(root, '.mcp.json'));
|
|
230
|
-
if (text === null)
|
|
231
|
-
return null;
|
|
232
|
-
try {
|
|
233
|
-
const parsed = parseJsonLoose(text);
|
|
234
|
-
const server = isObject(parsed) && isObject(parsed.mcpServers) ? parsed.mcpServers[MCP_SERVER_NAME] : undefined;
|
|
235
|
-
if (!isObject(server) || typeof server.command !== 'string' || !server.command.trim())
|
|
236
|
-
return null;
|
|
237
|
-
const args = Array.isArray(server.args) ? server.args : [];
|
|
238
|
-
if (args.length === 0 || args[args.length - 1] !== 'mcp' || !args.every((a) => typeof a === 'string'))
|
|
239
|
-
return null;
|
|
240
|
-
const prefixArgs = args.slice(0, -1);
|
|
241
|
-
return { command: server.command, prefixArgs, portable: ![server.command, ...prefixArgs].some((p) => path.isAbsolute(p)) };
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
/** Computes what `nativ setup` would do, without touching disk. */
|
|
248
|
-
export function planSetup(rootArg, options = {}) {
|
|
249
|
-
const root = path.resolve(rootArg);
|
|
250
|
-
const invocation = options.command?.trim()
|
|
251
|
-
? resolveCliInvocation({ command: options.command, hasBinary: options.hasBinary })
|
|
252
|
-
: configuredInvocation(root) ?? resolveCliInvocation({ hasBinary: options.hasBinary });
|
|
253
|
-
const desired = buildDesiredConfig(invocation);
|
|
254
|
-
const templatesDir = options.templatesDir ?? getTemplatesDir();
|
|
255
|
-
const changes = [];
|
|
256
|
-
const warnings = [];
|
|
257
|
-
const writes = new Map();
|
|
258
|
-
if (!invocation.portable) {
|
|
259
|
-
warnings.push('nativ is not on PATH, so the generated config points at this machine\'s install. ' +
|
|
260
|
-
'Install it globally (npm i -g @njeybe/nativ) or pass --command "npx -y @njeybe/nativ" so the config works for your team.');
|
|
261
|
-
}
|
|
262
|
-
const record = (rel, existing, next, detail) => {
|
|
263
|
-
if (existing === null) {
|
|
264
|
-
changes.push({ path: rel, action: 'created', detail });
|
|
265
|
-
writes.set(rel, next);
|
|
266
|
-
}
|
|
267
|
-
else if (existing === next) {
|
|
268
|
-
changes.push({ path: rel, action: 'unchanged' });
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
changes.push({ path: rel, action: 'updated', detail });
|
|
272
|
-
writes.set(rel, next);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
const skip = (rel, detail) => {
|
|
276
|
-
changes.push({ path: rel, action: 'skipped', detail });
|
|
277
|
-
warnings.push(`${rel}: ${detail}`);
|
|
278
|
-
};
|
|
279
|
-
/** Reads a JSON file for merging. Returns null (after recording a skip) when it exists but is not valid JSON. */
|
|
280
|
-
const readJsonObject = (rel) => {
|
|
281
|
-
const text = readText(path.join(root, rel));
|
|
282
|
-
if (text === null || !text.trim())
|
|
283
|
-
return { text, value: {} };
|
|
284
|
-
try {
|
|
285
|
-
const parsed = parseJsonLoose(text);
|
|
286
|
-
if (isObject(parsed))
|
|
287
|
-
return { text, value: parsed };
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// Fall through.
|
|
291
|
-
}
|
|
292
|
-
skip(rel, 'exists but is not a valid JSON object; not modified. Fix or delete it and re-run.');
|
|
293
|
-
return null;
|
|
294
|
-
};
|
|
295
|
-
// .mcp.json
|
|
296
|
-
const mcp = readJsonObject('.mcp.json');
|
|
297
|
-
if (mcp) {
|
|
298
|
-
const { merged, changed } = mergeMcpConfig(mcp.value, desired);
|
|
299
|
-
record('.mcp.json', mcp.text, changed || mcp.text === null ? serializeJson(merged) : mcp.text, changed ? `${MCP_SERVER_NAME} server -> ${desired.cli} mcp` : undefined);
|
|
300
|
-
}
|
|
301
|
-
// .claude/settings.json
|
|
302
|
-
const settings = readJsonObject('.claude/settings.json');
|
|
303
|
-
if (settings) {
|
|
304
|
-
const { merged, changes: settingChanges, warnings: settingWarnings } = mergeClaudeSettings(settings.value, desired);
|
|
305
|
-
warnings.push(...settingWarnings);
|
|
306
|
-
record('.claude/settings.json', settings.text, settingChanges.length || settings.text === null ? serializeJson(merged) : (settings.text ?? ''), settingChanges.join('; ') || undefined);
|
|
307
|
-
}
|
|
308
|
-
// .claude/agents/*.md: written only when missing, pristine, or forced
|
|
309
|
-
for (const file of AGENT_FILES) {
|
|
310
|
-
const rel = `.claude/agents/${file}`;
|
|
311
|
-
const template = readText(path.join(templatesDir, 'claude-agents', file));
|
|
312
|
-
if (template === null) {
|
|
313
|
-
skip(rel, `template templates/claude-agents/${file} is missing from this nativ install.`);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
const next = stampManaged(template);
|
|
317
|
-
const existing = readText(path.join(root, rel));
|
|
318
|
-
if (existing === null || existing === next) {
|
|
319
|
-
record(rel, existing, next);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const state = managedState(existing);
|
|
323
|
-
if (state === 'pristine' || options.force)
|
|
324
|
-
record(rel, existing, next, state === 'pristine' ? 'template updated' : 'forced');
|
|
325
|
-
else
|
|
326
|
-
skip(rel, state === 'modified' ? 'was edited by hand; not overwritten (use --force to replace).' : 'exists and is not managed by nativ; not overwritten (use --force to replace).');
|
|
327
|
-
}
|
|
328
|
-
// AGENTS.md is the canonical directive; CLAUDE.md and GEMINI.md only ever get a pointer, and only when absent
|
|
329
|
-
const agents = readText(path.join(templatesDir, 'AGENTS.md'));
|
|
330
|
-
if (agents === null) {
|
|
331
|
-
skip('AGENTS.md', 'template templates/AGENTS.md is missing from this nativ install.');
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
const next = stampManaged(agents);
|
|
335
|
-
const existing = readText(path.join(root, 'AGENTS.md'));
|
|
336
|
-
if (existing === null || existing === next)
|
|
337
|
-
record('AGENTS.md', existing, next);
|
|
338
|
-
else if (managedState(existing) === 'pristine' || options.force)
|
|
339
|
-
record('AGENTS.md', existing, next, 'template updated');
|
|
340
|
-
else
|
|
341
|
-
skip('AGENTS.md', 'exists and was not written by nativ (or was edited); not overwritten (use --force to replace).');
|
|
342
|
-
}
|
|
343
|
-
for (const [file, title] of [['CLAUDE.md', 'Claude Code'], ['GEMINI.md', 'Gemini / Antigravity']]) {
|
|
344
|
-
const existing = readText(path.join(root, file));
|
|
345
|
-
if (existing === null)
|
|
346
|
-
record(file, null, `# ${title} directive\n\n@AGENTS.md\n`);
|
|
347
|
-
else
|
|
348
|
-
changes.push({ path: file, action: 'unchanged', detail: /AGENTS\.md/.test(existing) ? undefined : 'exists; does not reference AGENTS.md' });
|
|
349
|
-
}
|
|
350
|
-
// .nativ/config.json
|
|
351
|
-
const config = readJsonObject('.nativ/config.json');
|
|
352
|
-
if (config) {
|
|
353
|
-
const next = { ...config.value };
|
|
354
|
-
let detail;
|
|
355
|
-
if (options.enforcement && next.enforcement !== options.enforcement) {
|
|
356
|
-
next.enforcement = options.enforcement;
|
|
357
|
-
detail = `enforcement=${options.enforcement}`;
|
|
358
|
-
}
|
|
359
|
-
else if (next.enforcement === undefined) {
|
|
360
|
-
next.enforcement = loadEnforcementMode(root);
|
|
361
|
-
detail = `enforcement=${String(next.enforcement)}`;
|
|
362
|
-
}
|
|
363
|
-
record('.nativ/config.json', config.text, detail || config.text === null ? serializeJson(next) : (config.text ?? ''), detail);
|
|
364
|
-
}
|
|
365
|
-
return { root, invocation, changes, warnings, writes };
|
|
366
|
-
}
|
|
367
|
-
/** Writes the planned differences. Idempotent: applying twice changes nothing the second time. */
|
|
368
|
-
export function applySetup(rootArg, options = {}) {
|
|
369
|
-
const plan = planSetup(rootArg, options);
|
|
370
|
-
if (!options.dryRun) {
|
|
371
|
-
for (const [rel, content] of plan.writes) {
|
|
372
|
-
const file = path.join(plan.root, rel);
|
|
373
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
374
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
375
|
-
fs.writeFileSync(tmp, content, 'utf8');
|
|
376
|
-
fs.renameSync(tmp, file);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return { root: plan.root, dryRun: options.dryRun === true, invocation: plan.invocation, changes: plan.changes, warnings: plan.warnings };
|
|
380
|
-
}
|
|
381
|
-
function readPlanTasks(root) {
|
|
382
|
-
try {
|
|
383
|
-
const plan = parseJsonLoose(fs.readFileSync(path.join(root, '.ai', 'master_plan.json'), 'utf8'));
|
|
384
|
-
const tasks = [];
|
|
385
|
-
for (const milestone of plan.milestones ?? []) {
|
|
386
|
-
for (const t of milestone.tasks ?? []) {
|
|
387
|
-
if (typeof t.id === 'string') {
|
|
388
|
-
tasks.push({
|
|
389
|
-
id: t.id,
|
|
390
|
-
title: typeof t.title === 'string' ? t.title : '',
|
|
391
|
-
status: typeof t.status === 'string' ? t.status : 'pending',
|
|
392
|
-
dependencies: Array.isArray(t.dependencies) ? t.dependencies : [],
|
|
393
|
-
targetFiles: Array.isArray(t.targetFiles) ? t.targetFiles : [],
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return tasks;
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
/** A few lines of orientation for a new Claude Code session, or null when this is not a nativ project. */
|
|
405
|
-
export function buildSessionContext(cwd, env = process.env) {
|
|
406
|
-
const root = resolveProjectRoot(cwd);
|
|
407
|
-
if (!fs.existsSync(path.join(root, '.ai', 'master_plan.json')))
|
|
408
|
-
return null;
|
|
409
|
-
const tasks = readPlanTasks(root);
|
|
410
|
-
const active = tasks.filter((t) => t.status === 'in_progress');
|
|
411
|
-
const done = new Set(tasks.filter((t) => t.status === 'completed').map((t) => t.id));
|
|
412
|
-
const next = tasks.find((t) => t.status === 'pending' && t.dependencies.every((d) => done.has(d)));
|
|
413
|
-
const role = (env[ROLE_ENV] ?? '').trim().toLowerCase() || 'project manager';
|
|
414
|
-
const lines = [
|
|
415
|
-
'This is a nativ project (role-based multi-agent workflow).',
|
|
416
|
-
`Session role: ${role}. Enforcement: ${loadEnforcementMode(root)}. Edits outside the active task's targetFiles are flagged, and .ai/ contracts are architect-only.`,
|
|
417
|
-
];
|
|
418
|
-
if (active.length) {
|
|
419
|
-
for (const t of active)
|
|
420
|
-
lines.push(`In progress: ${t.id} "${t.title}" (targetFiles: ${t.targetFiles.join(', ') || 'none'})`);
|
|
421
|
-
}
|
|
422
|
-
else if (next) {
|
|
423
|
-
lines.push(`Next available task: ${next.id} "${next.title}". Run \`nativ task next\` to load its context.`);
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
lines.push('No task is in progress or ready.');
|
|
427
|
-
}
|
|
428
|
-
lines.push('Prefer the nativ_* MCP tools (or the `nativ` CLI). Do not edit .ai/ directly: escalate contract gaps with `nativ task escalate`.');
|
|
429
|
-
return lines.join('\n');
|
|
430
|
-
}
|
|
431
|
-
/** `nativ hook context`: prints the SessionStart hook output. Never throws and always prints valid JSON or nothing. */
|
|
432
|
-
export function runSessionContext(targetDirArg, write = (t) => process.stdout.write(t)) {
|
|
433
|
-
try {
|
|
434
|
-
const context = buildSessionContext(targetDirArg ? path.resolve(targetDirArg) : process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
435
|
-
if (context)
|
|
436
|
-
write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context } }));
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
439
|
-
// Orientation is a convenience; a failure must never disturb session start.
|
|
440
|
-
}
|
|
441
|
-
}
|
|
7
|
+
export * from './setup/types.js';
|
|
8
|
+
export { SECRET_READ_DENY, buildDesiredConfig, isObject, unionStrings, mergeHook, mergeClaudeSettings, mergeMcpConfig, } from './setup/config-merger.js';
|
|
9
|
+
export { planTemplateFile } from './setup/template-planner.js';
|
|
10
|
+
export { getTemplatesDir, configuredInvocation, planSetup, applySetup, } from './setup/setup-planner.js';
|
|
11
|
+
export { buildSessionContext, runSessionContext, } from './setup/session-context.js';
|
|
@@ -108,6 +108,8 @@ export declare function hasGeminiApiKey(root?: string): boolean;
|
|
|
108
108
|
* text leaves the machine. `extraSecrets` (e.g. the Gemini key itself) are masked verbatim.
|
|
109
109
|
*/
|
|
110
110
|
export declare function redactSecrets(text: string, extraSecrets?: string[]): string;
|
|
111
|
+
export declare function loadTriageConfig(root: string): Partial<TriageConfig>;
|
|
112
|
+
export declare function saveTriageConfig(root: string, update: Partial<TriageConfig>): void;
|
|
111
113
|
/**
|
|
112
114
|
* Tier 1 AI Strategist Liaison instance managing escalation evaluation,
|
|
113
115
|
* auto-patch application, and configuration.
|