@neurcode-ai/cli 0.20.21 → 0.20.23
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/README.md +13 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +69 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pilot.d.ts.map +1 -1
- package/dist/commands/pilot.js +39 -27
- package/dist/commands/pilot.js.map +1 -1
- package/dist/commands/repo.d.ts.map +1 -1
- package/dist/commands/repo.js +67 -0
- package/dist/commands/repo.js.map +1 -1
- package/dist/commands/runtime-sync.d.ts +1 -0
- package/dist/commands/runtime-sync.d.ts.map +1 -1
- package/dist/commands/runtime-sync.js +69 -2
- package/dist/commands/runtime-sync.js.map +1 -1
- package/dist/commands/session-hook-recovery.d.ts +25 -0
- package/dist/commands/session-hook-recovery.d.ts.map +1 -0
- package/dist/commands/session-hook-recovery.js +94 -0
- package/dist/commands/session-hook-recovery.js.map +1 -0
- package/dist/commands/session-hook.d.ts.map +1 -1
- package/dist/commands/session-hook.js +33 -3
- package/dist/commands/session-hook.js.map +1 -1
- package/dist/commands/telemetry.d.ts.map +1 -1
- package/dist/commands/telemetry.js +10 -2
- package/dist/commands/telemetry.js.map +1 -1
- package/dist/runtime-build.json +4 -4
- package/dist/utils/activation-proof.d.ts +62 -0
- package/dist/utils/activation-proof.d.ts.map +1 -0
- package/dist/utils/activation-proof.js +303 -0
- package/dist/utils/activation-proof.js.map +1 -0
- package/dist/utils/activation-telemetry.d.ts +1 -0
- package/dist/utils/activation-telemetry.d.ts.map +1 -1
- package/dist/utils/activation-telemetry.js +8 -0
- package/dist/utils/activation-telemetry.js.map +1 -1
- package/dist/utils/first-value-proof.d.ts.map +1 -1
- package/dist/utils/first-value-proof.js +40 -1
- package/dist/utils/first-value-proof.js.map +1 -1
- package/dist/utils/local-first-value.d.ts +77 -0
- package/dist/utils/local-first-value.d.ts.map +1 -0
- package/dist/utils/local-first-value.js +808 -0
- package/dist/utils/local-first-value.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local-First Aha V1 — the default `neurcode pilot start` engine.
|
|
4
|
+
*
|
|
5
|
+
* Runs a complete first-value proof in the user's own repository BEFORE any
|
|
6
|
+
* login: detect boundaries from the existing governance profile, run a real
|
|
7
|
+
* (non-activated) governed session through the same decision kernel the hooks
|
|
8
|
+
* use, demonstrate block → exact-path approval → neighbor containment, and
|
|
9
|
+
* write a source-free local proof artifact. Login/sync is offered only after
|
|
10
|
+
* the proof exists.
|
|
11
|
+
*
|
|
12
|
+
* Safety properties:
|
|
13
|
+
* - never modifies user source (write attempts are decisions, not writes)
|
|
14
|
+
* - never requires cloud auth, never opens a browser
|
|
15
|
+
* - never touches the active session pointer (activate: false throughout)
|
|
16
|
+
* - never requires a healthy runtime manifest; stale identity only downgrades
|
|
17
|
+
* the reported host tier and prints the recovery command (no wedge)
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.LOCAL_FIRST_VALUE_LOGIN_PROMPT = exports.LOCAL_FIRST_VALUE_MARKDOWN_PATH = exports.LOCAL_FIRST_VALUE_JSON_PATH = void 0;
|
|
21
|
+
exports.pickProtectedPair = pickProtectedPair;
|
|
22
|
+
exports.probeHookBinary = probeHookBinary;
|
|
23
|
+
exports.renderLocalFirstValueMarkdown = renderLocalFirstValueMarkdown;
|
|
24
|
+
exports.renderLocalFirstValueText = renderLocalFirstValueText;
|
|
25
|
+
exports.runLocalFirstValue = runLocalFirstValue;
|
|
26
|
+
const node_child_process_1 = require("node:child_process");
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
const node_path_1 = require("node:path");
|
|
29
|
+
const promises_1 = require("node:readline/promises");
|
|
30
|
+
const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
|
|
31
|
+
const cli_runtime_1 = require("@neurcode-ai/cli-runtime");
|
|
32
|
+
const contracts_1 = require("@neurcode-ai/contracts");
|
|
33
|
+
const v0_governance_1 = require("./v0-governance");
|
|
34
|
+
const runtime_authority_1 = require("./runtime-authority");
|
|
35
|
+
const cli_entry_1 = require("./cli-entry");
|
|
36
|
+
const agent_adapter_setup_1 = require("./agent-adapter-setup");
|
|
37
|
+
const onboard_1 = require("../commands/onboard");
|
|
38
|
+
const activation_telemetry_1 = require("./activation-telemetry");
|
|
39
|
+
exports.LOCAL_FIRST_VALUE_JSON_PATH = '.neurcode/eval/local-first-value.json';
|
|
40
|
+
exports.LOCAL_FIRST_VALUE_MARKDOWN_PATH = '.neurcode/eval/local-first-value.md';
|
|
41
|
+
exports.LOCAL_FIRST_VALUE_LOGIN_PROMPT = 'Want this in the dashboard or to share with your team? Run `neurcode login`.';
|
|
42
|
+
const PROOF_GOAL = 'Local first-value proof: attempt one protected write, approve one exact path, verify the neighbor stays blocked';
|
|
43
|
+
const SYNTHETIC_WRITE_CONTENT = '// neurcode local first-value probe — synthetic content, no user source\n';
|
|
44
|
+
const MAX_CANDIDATE_FILES = 20_000;
|
|
45
|
+
const MAX_BLOCKED_CANDIDATES = 400;
|
|
46
|
+
// ── Repo + file detection ─────────────────────────────────────────────────────
|
|
47
|
+
function isGitRepo(repoRoot) {
|
|
48
|
+
try {
|
|
49
|
+
return (0, node_child_process_1.execFileSync)('git', ['-C', repoRoot, 'rev-parse', '--is-inside-work-tree'], {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
|
+
}).trim() === 'true';
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function listTrackedFiles(repoRoot) {
|
|
59
|
+
try {
|
|
60
|
+
return (0, node_child_process_1.execFileSync)('git', ['-C', repoRoot, 'ls-files'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
63
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
64
|
+
})
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.slice(0, MAX_CANDIDATE_FILES);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const WALK_SKIP = new Set(['.git', '.neurcode', 'node_modules', 'dist', 'build', 'coverage', '.next', 'vendor']);
|
|
75
|
+
function walkFiles(repoRoot, dir = '', depth = 0, out = []) {
|
|
76
|
+
if (depth > 6 || out.length >= 5_000)
|
|
77
|
+
return out;
|
|
78
|
+
let entries = [];
|
|
79
|
+
try {
|
|
80
|
+
entries = (0, node_fs_1.readdirSync)((0, node_path_1.join)(repoRoot, dir));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries.sort()) {
|
|
86
|
+
if (out.length >= 5_000)
|
|
87
|
+
break;
|
|
88
|
+
if (WALK_SKIP.has(entry))
|
|
89
|
+
continue;
|
|
90
|
+
const rel = dir ? `${dir}/${entry}` : entry;
|
|
91
|
+
try {
|
|
92
|
+
const stats = (0, node_fs_1.statSync)((0, node_path_1.join)(repoRoot, rel));
|
|
93
|
+
if (stats.isDirectory())
|
|
94
|
+
walkFiles(repoRoot, rel, depth + 1, out);
|
|
95
|
+
else if (stats.isFile())
|
|
96
|
+
out.push(rel);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Unreadable entries are skipped; detection stays best-effort.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function candidateFiles(repoRoot, gitDetected) {
|
|
105
|
+
const files = gitDetected ? listTrackedFiles(repoRoot) : [];
|
|
106
|
+
const chosen = files.length > 0 ? files : walkFiles(repoRoot);
|
|
107
|
+
return chosen.filter((file) => !file.startsWith('.neurcode/') && !file.startsWith('.git/'));
|
|
108
|
+
}
|
|
109
|
+
// ── Host posture ──────────────────────────────────────────────────────────────
|
|
110
|
+
function normalizeAgentOption(agent, detected) {
|
|
111
|
+
const value = (agent || '').trim().toLowerCase();
|
|
112
|
+
if (['claude', 'cursor', 'codex', 'copilot', 'vscode'].includes(value))
|
|
113
|
+
return value;
|
|
114
|
+
return detected;
|
|
115
|
+
}
|
|
116
|
+
function detectHostPosture(repoRoot, agentOption) {
|
|
117
|
+
const environment = (0, onboard_1.detectOnboardEnvironment)(repoRoot);
|
|
118
|
+
const agent = normalizeAgentOption(agentOption, environment.target);
|
|
119
|
+
let hooksInstalled = false;
|
|
120
|
+
try {
|
|
121
|
+
const claude = (0, v0_governance_1.inspectClaudeActivation)(repoRoot);
|
|
122
|
+
hooksInstalled = claude.hooks.installed;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
hooksInstalled = false;
|
|
126
|
+
}
|
|
127
|
+
if (hooksInstalled) {
|
|
128
|
+
let identityHealthy = null;
|
|
129
|
+
let recoveryCommand = null;
|
|
130
|
+
let hookEntrypoint = null;
|
|
131
|
+
try {
|
|
132
|
+
const manifest = (0, cli_runtime_1.readActivatedRuntimeManifest)(repoRoot);
|
|
133
|
+
const wired = manifest?.integrations.find((integration) => integration.adapter === 'claude-code-hooks');
|
|
134
|
+
if (wired?.absoluteEntrypoint && (0, node_fs_1.existsSync)(wired.absoluteEntrypoint)) {
|
|
135
|
+
hookEntrypoint = wired.absoluteEntrypoint;
|
|
136
|
+
}
|
|
137
|
+
const authority = (0, runtime_authority_1.inspectRuntimeAuthority)(repoRoot, 'claude-code-hooks', true);
|
|
138
|
+
identityHealthy = authority.ok;
|
|
139
|
+
if (!authority.ok)
|
|
140
|
+
recoveryCommand = authority.repairCommand;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
identityHealthy = false;
|
|
144
|
+
recoveryCommand = 'neurcode runtime repair';
|
|
145
|
+
}
|
|
146
|
+
if (identityHealthy) {
|
|
147
|
+
return {
|
|
148
|
+
agent: agent === 'terminal' ? 'claude' : agent,
|
|
149
|
+
tier: 'hard_hook',
|
|
150
|
+
note: 'Claude Code hooks are wired here: a protected write is blocked before it lands.',
|
|
151
|
+
identityHealthy,
|
|
152
|
+
recoveryCommand: null,
|
|
153
|
+
hookEntrypoint: hookEntrypoint || (0, cli_entry_1.getActiveCliEntry)(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
agent: agent === 'terminal' ? 'claude' : agent,
|
|
158
|
+
tier: 'hard_hook_degraded',
|
|
159
|
+
note: 'Claude Code hooks are wired but the runtime identity is stale; hard blocking resumes after repair.',
|
|
160
|
+
identityHealthy: false,
|
|
161
|
+
recoveryCommand: recoveryCommand || 'neurcode runtime repair',
|
|
162
|
+
hookEntrypoint: null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
for (const target of ['cursor', 'codex']) {
|
|
166
|
+
try {
|
|
167
|
+
if ((0, agent_adapter_setup_1.inspectAgentSetup)({ target, repoRoot, global: target === 'codex' }).configured) {
|
|
168
|
+
return {
|
|
169
|
+
agent,
|
|
170
|
+
tier: 'cooperative',
|
|
171
|
+
note: `A ${target} integration is configured: the agent is asked to check before writing, but a non-cooperating agent can skip it.`,
|
|
172
|
+
identityHealthy: null,
|
|
173
|
+
recoveryCommand: null,
|
|
174
|
+
hookEntrypoint: null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Detection is best-effort.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
if ((0, agent_adapter_setup_1.inspectAgentSetup)({ target: 'vscode', repoRoot }).configured) {
|
|
184
|
+
return {
|
|
185
|
+
agent,
|
|
186
|
+
tier: 'observe_only',
|
|
187
|
+
note: 'The VS Code integration observes and records; it cannot block a write by itself.',
|
|
188
|
+
identityHealthy: null,
|
|
189
|
+
recoveryCommand: null,
|
|
190
|
+
hookEntrypoint: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Detection is best-effort.
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
agent,
|
|
199
|
+
tier: 'not_wired',
|
|
200
|
+
note: 'No agent integration is wired in this repo yet, so this proof simulated the decisions with the same engine the hooks use.',
|
|
201
|
+
identityHealthy: null,
|
|
202
|
+
recoveryCommand: null,
|
|
203
|
+
hookEntrypoint: null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function boundaryFor(contract, filePath, approvedPaths) {
|
|
207
|
+
return (0, governance_runtime_1.checkFileBoundary)({
|
|
208
|
+
filePath,
|
|
209
|
+
allowedGlobs: contract.allowedGlobs,
|
|
210
|
+
ownershipRules: contract.ownershipRules,
|
|
211
|
+
sensitiveGlobs: contract.sensitiveGlobs,
|
|
212
|
+
approvalRequiredGlobs: contract.approvalRequiredGlobs,
|
|
213
|
+
approvedPaths,
|
|
214
|
+
approvalGrants: [],
|
|
215
|
+
scopeMode: contract.scopeMode,
|
|
216
|
+
localMode: 'strict',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function rankCandidate(file, result) {
|
|
220
|
+
let score = 0;
|
|
221
|
+
if (result.isSensitive)
|
|
222
|
+
score += 4;
|
|
223
|
+
if (result.owners.length > 0)
|
|
224
|
+
score += 2;
|
|
225
|
+
if (file.includes('/'))
|
|
226
|
+
score += 3; // exact approval of a nested path can never basename-match a neighbor
|
|
227
|
+
const base = file.split('/').pop() || file;
|
|
228
|
+
if (base === 'package.json' || base.endsWith('.lock') || base === 'pnpm-lock.yaml')
|
|
229
|
+
score -= 3;
|
|
230
|
+
return score;
|
|
231
|
+
}
|
|
232
|
+
function pickProtectedPair(files, contract) {
|
|
233
|
+
const blocked = [];
|
|
234
|
+
const detection = { ...contract, allowedGlobs: [], scopeMode: 'explicit' };
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
const result = boundaryFor(detection, file, []);
|
|
237
|
+
if (result.verdict !== 'block')
|
|
238
|
+
continue;
|
|
239
|
+
blocked.push({ file, result, score: rankCandidate(file, result) });
|
|
240
|
+
if (blocked.length >= MAX_BLOCKED_CANDIDATES)
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (blocked.length === 0)
|
|
244
|
+
return null;
|
|
245
|
+
blocked.sort((left, right) => right.score - left.score || left.file.length - right.file.length || left.file.localeCompare(right.file));
|
|
246
|
+
for (const primary of blocked.slice(0, 25)) {
|
|
247
|
+
const dir = primary.file.split('/').slice(0, -1).join('/');
|
|
248
|
+
const neighbors = blocked
|
|
249
|
+
.filter((candidate) => candidate.file !== primary.file)
|
|
250
|
+
.sort((left, right) => {
|
|
251
|
+
const leftSame = left.file.split('/').slice(0, -1).join('/') === dir ? 1 : 0;
|
|
252
|
+
const rightSame = right.file.split('/').slice(0, -1).join('/') === dir ? 1 : 0;
|
|
253
|
+
return rightSame - leftSame || right.score - left.score || left.file.localeCompare(right.file);
|
|
254
|
+
});
|
|
255
|
+
for (const neighbor of neighbors.slice(0, 12)) {
|
|
256
|
+
// Self-verify with the same engine: approving exactly `primary` must
|
|
257
|
+
// unblock only `primary` and keep `neighbor` blocked.
|
|
258
|
+
const approvedPrimary = boundaryFor(detection, primary.file, [primary.file]);
|
|
259
|
+
const containedNeighbor = boundaryFor(detection, neighbor.file, [primary.file]);
|
|
260
|
+
if (approvedPrimary.verdict !== 'block' && containedNeighbor.verdict === 'block') {
|
|
261
|
+
return {
|
|
262
|
+
target: primary.file,
|
|
263
|
+
neighbor: neighbor.file,
|
|
264
|
+
targetResult: primary.result,
|
|
265
|
+
neighborResult: neighbor.result,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// No pair with provable containment; still demonstrate the block honestly.
|
|
271
|
+
const primary = blocked[0];
|
|
272
|
+
const approvedPrimary = boundaryFor(detection, primary.file, [primary.file]);
|
|
273
|
+
if (approvedPrimary.verdict === 'block')
|
|
274
|
+
return null;
|
|
275
|
+
return { target: primary.file, neighbor: null, targetResult: primary.result, neighborResult: null };
|
|
276
|
+
}
|
|
277
|
+
function probeHookBinary(input) {
|
|
278
|
+
try {
|
|
279
|
+
const stdout = (0, node_child_process_1.execFileSync)(process.execPath, [input.entrypoint, 'session-hook', 'check', '--trusted-adapter', 'claude-code-hooks', '--trusted-timing', 'before_write'], {
|
|
280
|
+
cwd: input.repoRoot,
|
|
281
|
+
input: JSON.stringify({
|
|
282
|
+
tool_name: 'Write',
|
|
283
|
+
tool_input: { file_path: (0, node_path_1.join)(input.repoRoot, input.filePath), content: SYNTHETIC_WRITE_CONTENT },
|
|
284
|
+
cwd: input.repoRoot,
|
|
285
|
+
session_id: input.sessionId,
|
|
286
|
+
}),
|
|
287
|
+
encoding: 'utf8',
|
|
288
|
+
timeout: 30_000,
|
|
289
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
290
|
+
env: { ...process.env, NEURCODE_DISABLE_CONSEQUENCE_NUDGES: '1' },
|
|
291
|
+
});
|
|
292
|
+
for (const line of stdout.split('\n')) {
|
|
293
|
+
const trimmed = line.trim();
|
|
294
|
+
if (!trimmed.startsWith('{'))
|
|
295
|
+
continue;
|
|
296
|
+
try {
|
|
297
|
+
const parsed = JSON.parse(trimmed);
|
|
298
|
+
if (parsed.hookSpecificOutput?.permissionDecision === 'deny') {
|
|
299
|
+
return { decision: 'block', reason: parsed.hookSpecificOutput.permissionDecisionReason || null };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Non-JSON diagnostics are expected on stdout-adjacent lines.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { decision: 'allow', reason: null };
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// ── Interactive approval ──────────────────────────────────────────────────────
|
|
313
|
+
async function confirmApproval(path, interactive) {
|
|
314
|
+
if (!interactive || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
315
|
+
return { approved: true, approvedBy: 'pilot_start_auto' };
|
|
316
|
+
}
|
|
317
|
+
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
318
|
+
try {
|
|
319
|
+
const answer = (await rl.question(`Approve this exact path for the proof session — ${path}? [Y/n] `)).trim().toLowerCase();
|
|
320
|
+
return { approved: answer === '' || answer === 'y' || answer === 'yes', approvedBy: 'local_operator' };
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
rl.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── Artifact + rendering ──────────────────────────────────────────────────────
|
|
327
|
+
function baseLimitations(host) {
|
|
328
|
+
const limitations = [];
|
|
329
|
+
if (host.tier === 'hard_hook') {
|
|
330
|
+
limitations.push('Hard pre-write blocking is proven for hook-based agents; cooperative agents are checked only when they ask.');
|
|
331
|
+
}
|
|
332
|
+
else if (host.tier === 'hard_hook_degraded') {
|
|
333
|
+
limitations.push('Hard hooks are wired but currently degraded; decisions here came from the same kernel as a local simulation.');
|
|
334
|
+
}
|
|
335
|
+
else if (host.tier === 'cooperative') {
|
|
336
|
+
limitations.push('This host is cooperative: a non-cooperating agent could write without asking. Hard blocking needs the hook integration.');
|
|
337
|
+
}
|
|
338
|
+
else if (host.tier === 'observe_only') {
|
|
339
|
+
limitations.push('This host can observe and record but cannot block a write by itself.');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
limitations.push('No agent is wired yet, so no live agent write was intercepted; decisions were simulated with the production decision engine.');
|
|
343
|
+
}
|
|
344
|
+
limitations.push('This proof ran locally and is self-attested until synced; no source was uploaded.');
|
|
345
|
+
return limitations;
|
|
346
|
+
}
|
|
347
|
+
function proofIdFor(repoHash, generatedAt) {
|
|
348
|
+
return `lfv_${(0, contracts_1.localFirstValueStableHash)(`${repoHash ?? 'no-repo'}:${generatedAt}`)}`;
|
|
349
|
+
}
|
|
350
|
+
function renderLocalFirstValueMarkdown(artifact) {
|
|
351
|
+
const lines = [];
|
|
352
|
+
const blocked = artifact.decisions.find((decision) => decision.step === 'protected_write_blocked');
|
|
353
|
+
const allowed = artifact.decisions.find((decision) => decision.step === 'approved_write_allowed');
|
|
354
|
+
const neighbor = artifact.decisions.find((decision) => decision.step === 'neighbor_write_blocked');
|
|
355
|
+
lines.push('# Neurcode local first-value proof');
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push(`Generated ${artifact.generatedAt} · proof \`${artifact.proofId}\` · schema \`${artifact.schemaVersion}\``);
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push('## What happened');
|
|
360
|
+
lines.push('');
|
|
361
|
+
if (blocked) {
|
|
362
|
+
lines.push(`1. **An agent-style write to a protected file was blocked before it landed.** Path: \`${blocked.path}\` (${blocked.reasonCodes.join(', ')}).`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
lines.push('1. **No protected write was demonstrated.** See limitations below.');
|
|
366
|
+
}
|
|
367
|
+
if (artifact.approvedExactPath) {
|
|
368
|
+
lines.push(`2. **A human approved exactly one path.** Only \`${artifact.approvedExactPath}\` was approved, for this session only.`);
|
|
369
|
+
lines.push(`3. **The approved write became allowed.** ${allowed ? `Verdict: ${allowed.verdict}.` : 'Not re-checked.'}`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
lines.push('2. **No path was approved.**');
|
|
373
|
+
}
|
|
374
|
+
if (neighbor) {
|
|
375
|
+
lines.push(`4. **The file next door stayed blocked.** \`${neighbor.path}\` was still denied after the approval (${neighbor.reasonCodes.join(', ')}).`);
|
|
376
|
+
}
|
|
377
|
+
else if (artifact.neighborContainment === 'not_evaluated') {
|
|
378
|
+
lines.push('4. **Neighbor containment was not evaluated** — no second protected file with provable containment was found.');
|
|
379
|
+
}
|
|
380
|
+
lines.push('');
|
|
381
|
+
lines.push('## What Neurcode detected in this repo');
|
|
382
|
+
lines.push('');
|
|
383
|
+
lines.push(`- Files scanned: ${artifact.detection.trackedFileCount ?? 'unknown'}`);
|
|
384
|
+
lines.push(`- Sensitive boundaries: ${artifact.detection.sensitiveBoundaryCount}`);
|
|
385
|
+
lines.push(`- Approval-required rules: ${artifact.detection.approvalRequiredGlobCount}`);
|
|
386
|
+
lines.push(`- CODEOWNERS detected: ${artifact.detection.codeownersDetected ? 'yes' : 'no'}`);
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push('## Host enforcement');
|
|
389
|
+
lines.push('');
|
|
390
|
+
lines.push(`- Tier: \`${artifact.host.enforcementTier}\` (agent: ${artifact.host.agent})`);
|
|
391
|
+
lines.push(`- ${artifact.host.enforcementNote}`);
|
|
392
|
+
if (artifact.host.recoveryCommand) {
|
|
393
|
+
lines.push(`- Recovery: \`${artifact.host.recoveryCommand}\``);
|
|
394
|
+
}
|
|
395
|
+
lines.push('');
|
|
396
|
+
lines.push('## What stayed local');
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push('- Source code, prompts, diffs, and absolute paths never left this machine.');
|
|
399
|
+
lines.push('- This file records repo-relative paths, verdicts, reason codes, counts, and hashes only.');
|
|
400
|
+
lines.push('');
|
|
401
|
+
lines.push('## Evidence');
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push(`- Session: \`${artifact.sessionId ?? 'none'}\` · replay hash: \`${artifact.replayHash ?? 'none'}\``);
|
|
404
|
+
lines.push(`- Decisions: ${artifact.decisions.length} · blocked: ${artifact.blockedPathCount} · containment: ${artifact.neighborContainment}`);
|
|
405
|
+
lines.push(`- Content hash: \`${artifact.contentHash}\``);
|
|
406
|
+
lines.push('');
|
|
407
|
+
lines.push('## Limitations');
|
|
408
|
+
lines.push('');
|
|
409
|
+
for (const limitation of artifact.limitations)
|
|
410
|
+
lines.push(`- ${limitation}`);
|
|
411
|
+
lines.push('');
|
|
412
|
+
lines.push(`Next: ${artifact.nextActions.local}`);
|
|
413
|
+
lines.push('');
|
|
414
|
+
lines.push(exports.LOCAL_FIRST_VALUE_LOGIN_PROMPT);
|
|
415
|
+
lines.push('');
|
|
416
|
+
return lines.join('\n');
|
|
417
|
+
}
|
|
418
|
+
function renderLocalFirstValueText(result) {
|
|
419
|
+
const { artifact } = result;
|
|
420
|
+
const lines = [];
|
|
421
|
+
const blocked = artifact.decisions.find((decision) => decision.step === 'protected_write_blocked');
|
|
422
|
+
const allowed = artifact.decisions.find((decision) => decision.step === 'approved_write_allowed');
|
|
423
|
+
const neighbor = artifact.decisions.find((decision) => decision.step === 'neighbor_write_blocked');
|
|
424
|
+
lines.push('');
|
|
425
|
+
lines.push('Neurcode — first value, before any login');
|
|
426
|
+
lines.push('');
|
|
427
|
+
lines.push(`Repo: ${artifact.repo.label ?? 'unknown'} · git: ${artifact.repo.gitDetected ? 'yes' : 'no'} · files scanned: ${artifact.detection.trackedFileCount ?? 'unknown'}`);
|
|
428
|
+
lines.push(`Protected boundaries: ${artifact.detection.sensitiveBoundaryCount} sensitive · ${artifact.detection.approvalRequiredGlobCount} approval-required · CODEOWNERS: ${artifact.detection.codeownersDetected ? 'yes' : 'no'}`);
|
|
429
|
+
lines.push('');
|
|
430
|
+
if (blocked) {
|
|
431
|
+
lines.push('1. Neurcode blocked a protected write before it landed');
|
|
432
|
+
lines.push(` ✗ ${blocked.path}${blocked.reasonCodes.includes('codeowners_owned') ? ' (CODEOWNERS-owned)' : ''}`);
|
|
433
|
+
if (artifact.approvalCommand) {
|
|
434
|
+
lines.push(` Approve only this path with: ${artifact.approvalCommand}`);
|
|
435
|
+
}
|
|
436
|
+
if (artifact.approvedExactPath) {
|
|
437
|
+
lines.push('2. One exact path was approved — nothing broader');
|
|
438
|
+
lines.push(` ✓ ${artifact.approvedExactPath} (this session only)`);
|
|
439
|
+
lines.push('3. The approved write is now allowed');
|
|
440
|
+
lines.push(` ✓ ${allowed ? `${allowed.path} (${allowed.verdict})` : 'not re-checked'}`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
lines.push('2. No path was approved (declined)');
|
|
444
|
+
}
|
|
445
|
+
if (neighbor) {
|
|
446
|
+
lines.push('4. The neighbor stayed blocked');
|
|
447
|
+
lines.push(` ✗ ${neighbor.path}`);
|
|
448
|
+
}
|
|
449
|
+
else if (artifact.neighborContainment === 'not_evaluated') {
|
|
450
|
+
lines.push('4. Neighbor containment: not evaluated (no provable second protected file found)');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
lines.push('No protected write could be demonstrated in this repo yet.');
|
|
455
|
+
for (const limitation of artifact.limitations.slice(0, 1))
|
|
456
|
+
lines.push(` ${limitation}`);
|
|
457
|
+
}
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(`Host enforcement: ${artifact.host.enforcementTier} — ${artifact.host.enforcementNote}`);
|
|
460
|
+
if (artifact.host.recoveryCommand)
|
|
461
|
+
lines.push(`Recover hard enforcement with: ${artifact.host.recoveryCommand}`);
|
|
462
|
+
lines.push('No source left your machine. Proof written to:');
|
|
463
|
+
lines.push(` ${result.artifactFiles.json}`);
|
|
464
|
+
lines.push(` ${result.artifactFiles.markdown}`);
|
|
465
|
+
if (artifact.replayHash)
|
|
466
|
+
lines.push(`Replay hash: ${artifact.replayHash}`);
|
|
467
|
+
lines.push('');
|
|
468
|
+
lines.push(`Next: ${artifact.nextActions.local}`);
|
|
469
|
+
lines.push(exports.LOCAL_FIRST_VALUE_LOGIN_PROMPT);
|
|
470
|
+
lines.push('');
|
|
471
|
+
return lines.join('\n');
|
|
472
|
+
}
|
|
473
|
+
// ── Main flow ─────────────────────────────────────────────────────────────────
|
|
474
|
+
function decisionReasonCodes(result, extra = []) {
|
|
475
|
+
const codes = new Set(extra);
|
|
476
|
+
if (result.blockType)
|
|
477
|
+
codes.add(result.blockType);
|
|
478
|
+
else if (result.isApprovalRequired)
|
|
479
|
+
codes.add('approval_required_boundary');
|
|
480
|
+
if (result.isSensitive)
|
|
481
|
+
codes.add('sensitive_boundary');
|
|
482
|
+
if (result.owners.length > 0)
|
|
483
|
+
codes.add('codeowners_owned');
|
|
484
|
+
if (codes.size === 0)
|
|
485
|
+
codes.add(result.verdict === 'block' ? 'blocked' : 'allowed');
|
|
486
|
+
return [...codes];
|
|
487
|
+
}
|
|
488
|
+
function writeArtifacts(repoRoot, artifact) {
|
|
489
|
+
const evalDir = (0, node_path_1.join)(repoRoot, '.neurcode', 'eval');
|
|
490
|
+
(0, node_fs_1.mkdirSync)(evalDir, { recursive: true });
|
|
491
|
+
const markdown = renderLocalFirstValueMarkdown(artifact);
|
|
492
|
+
const markdownScan = (0, contracts_1.validateLocalFirstValueSourceFree)({ markdown });
|
|
493
|
+
if (!markdownScan.ok) {
|
|
494
|
+
throw new Error(`local first-value markdown failed the source-free scan: ${markdownScan.errors.join('; ')}`);
|
|
495
|
+
}
|
|
496
|
+
const jsonPath = (0, node_path_1.join)(repoRoot, exports.LOCAL_FIRST_VALUE_JSON_PATH);
|
|
497
|
+
const markdownPath = (0, node_path_1.join)(repoRoot, exports.LOCAL_FIRST_VALUE_MARKDOWN_PATH);
|
|
498
|
+
(0, node_fs_1.writeFileSync)(jsonPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
|
|
499
|
+
(0, node_fs_1.writeFileSync)(markdownPath, markdown, 'utf8');
|
|
500
|
+
return { json: (0, node_path_1.relative)(repoRoot, jsonPath), markdown: (0, node_path_1.relative)(repoRoot, markdownPath) };
|
|
501
|
+
}
|
|
502
|
+
function finalizeArtifact(unsigned) {
|
|
503
|
+
const artifact = {
|
|
504
|
+
...unsigned,
|
|
505
|
+
contentHash: (0, contracts_1.localFirstValueContentHash)(unsigned),
|
|
506
|
+
};
|
|
507
|
+
return (0, contracts_1.assertLocalFirstValueArtifact)(artifact);
|
|
508
|
+
}
|
|
509
|
+
async function runLocalFirstValue(options = {}) {
|
|
510
|
+
const repoRoot = (0, v0_governance_1.resolveRepoRoot)(options.dir || process.cwd());
|
|
511
|
+
const generatedAt = new Date().toISOString();
|
|
512
|
+
const gitDetected = isGitRepo(repoRoot);
|
|
513
|
+
const repoLabel = repoRoot.split('/').filter(Boolean).pop() ?? null;
|
|
514
|
+
const repoHash = (0, contracts_1.localFirstValueStableHash)(repoRoot);
|
|
515
|
+
const interactive = options.nonInteractive !== true && options.assumeYes !== true;
|
|
516
|
+
(0, activation_telemetry_1.trackActivationEvent)({
|
|
517
|
+
eventType: 'onboarding_step_completed',
|
|
518
|
+
commandFamily: 'pilot_start',
|
|
519
|
+
reasonCode: 'local_first_value.started',
|
|
520
|
+
flush: false,
|
|
521
|
+
});
|
|
522
|
+
const host = detectHostPosture(repoRoot, options.agent);
|
|
523
|
+
const emptyDetection = {
|
|
524
|
+
trackedFileCount: null,
|
|
525
|
+
sensitiveBoundaryCount: 0,
|
|
526
|
+
approvalRequiredGlobCount: 0,
|
|
527
|
+
ownershipRuleCount: 0,
|
|
528
|
+
codeownersDetected: false,
|
|
529
|
+
};
|
|
530
|
+
const degraded = (outcome, detection, limitations, localNext) => {
|
|
531
|
+
const artifact = finalizeArtifact({
|
|
532
|
+
schemaVersion: contracts_1.LOCAL_FIRST_VALUE_SCHEMA_VERSION,
|
|
533
|
+
proofId: proofIdFor(repoHash, generatedAt),
|
|
534
|
+
generatedAt,
|
|
535
|
+
repo: { label: repoLabel, hash: repoHash, gitDetected },
|
|
536
|
+
detection,
|
|
537
|
+
host: {
|
|
538
|
+
agent: host.agent,
|
|
539
|
+
enforcementTier: host.tier,
|
|
540
|
+
enforcementNote: host.note,
|
|
541
|
+
identityHealthy: host.identityHealthy,
|
|
542
|
+
recoveryCommand: host.recoveryCommand,
|
|
543
|
+
},
|
|
544
|
+
sessionId: null,
|
|
545
|
+
replayHash: null,
|
|
546
|
+
decisions: [],
|
|
547
|
+
blockedPathCount: 0,
|
|
548
|
+
approvedExactPath: null,
|
|
549
|
+
neighborPath: null,
|
|
550
|
+
neighborContainment: 'not_evaluated',
|
|
551
|
+
approvalCommand: null,
|
|
552
|
+
nextActions: { local: localNext, login: 'neurcode login' },
|
|
553
|
+
limitations: [...limitations, ...baseLimitations(host)],
|
|
554
|
+
privacy: {
|
|
555
|
+
sourceUploaded: false,
|
|
556
|
+
promptsUploaded: false,
|
|
557
|
+
diffsUploaded: false,
|
|
558
|
+
absolutePathsStored: false,
|
|
559
|
+
sourceFree: true,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
const artifactFiles = writeArtifacts(repoRoot, artifact);
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
outcome,
|
|
566
|
+
artifact,
|
|
567
|
+
artifactFiles,
|
|
568
|
+
text: renderLocalFirstValueText({ artifact, outcome, artifactFiles }),
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
// 1. Detect boundaries from the existing governance profile machinery.
|
|
572
|
+
let profile;
|
|
573
|
+
try {
|
|
574
|
+
profile = (0, v0_governance_1.ensureFreshGovernanceProfile)(repoRoot).profile;
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return degraded('setup_required', emptyDetection, [
|
|
578
|
+
gitDetected
|
|
579
|
+
? 'The repository governance profile could not be built, so boundaries were not detected.'
|
|
580
|
+
: 'This directory is not a git repository, so repository boundaries were not detected.',
|
|
581
|
+
], gitDetected ? 'neurcode doctor' : 'git init && neurcode pilot start (or try the sandbox: neurcode pilot start --fixture)');
|
|
582
|
+
}
|
|
583
|
+
const detection = {
|
|
584
|
+
trackedFileCount: profile.topology.trackedFileCount ?? null,
|
|
585
|
+
sensitiveBoundaryCount: profile.sensitiveBoundaries.length,
|
|
586
|
+
approvalRequiredGlobCount: profile.approvalRequiredPaths.length,
|
|
587
|
+
ownershipRuleCount: profile.ownershipBoundaries.length,
|
|
588
|
+
codeownersDetected: profile.ownershipBoundaries.length > 0 || Boolean(profile.topology.codeownersHash),
|
|
589
|
+
};
|
|
590
|
+
// 2. Pick a protected pair, self-verified with the production decision engine.
|
|
591
|
+
const files = candidateFiles(repoRoot, gitDetected);
|
|
592
|
+
const pair = pickProtectedPair(files, {
|
|
593
|
+
allowedGlobs: [],
|
|
594
|
+
ownershipRules: profile.ownershipBoundaries,
|
|
595
|
+
sensitiveGlobs: profile.sensitiveBoundaries.map((boundary) => boundary.glob),
|
|
596
|
+
approvalRequiredGlobs: profile.approvalRequiredPaths,
|
|
597
|
+
scopeMode: 'explicit',
|
|
598
|
+
});
|
|
599
|
+
if (!pair) {
|
|
600
|
+
return degraded('setup_required', detection, ['No approval-required boundary matched a repository file, so a real protected write could not be demonstrated.'], 'neurcode pilot start --fixture (safe sandbox demo) — or add approvalRequiredGlobs to .neurcode/governance.json');
|
|
601
|
+
}
|
|
602
|
+
// 3. Run the proof as a real (non-activated) governed session.
|
|
603
|
+
const session = (0, governance_runtime_1.createSession)(repoRoot, profile, PROOF_GOAL, { activate: false });
|
|
604
|
+
const sessionId = session.sessionId;
|
|
605
|
+
const decisions = [];
|
|
606
|
+
const limitations = [];
|
|
607
|
+
const useHookBinary = host.tier === 'hard_hook' && host.hookEntrypoint !== null;
|
|
608
|
+
let hookProbeDegraded = false;
|
|
609
|
+
const decide = (filePath) => {
|
|
610
|
+
const contract = ((0, governance_runtime_1.loadSession)(repoRoot, sessionId) ?? session).contract;
|
|
611
|
+
const kernel = (0, governance_runtime_1.checkFileBoundary)({
|
|
612
|
+
filePath,
|
|
613
|
+
allowedGlobs: [],
|
|
614
|
+
ownershipRules: contract.ownershipRules,
|
|
615
|
+
sensitiveGlobs: contract.sensitiveGlobs,
|
|
616
|
+
approvalRequiredGlobs: contract.approvalRequiredGlobs,
|
|
617
|
+
approvedPaths: contract.approvedPaths,
|
|
618
|
+
approvalGrants: contract.approvalGrants ?? [],
|
|
619
|
+
scopeMode: 'explicit',
|
|
620
|
+
localMode: 'strict',
|
|
621
|
+
});
|
|
622
|
+
if (useHookBinary && !hookProbeDegraded && host.hookEntrypoint) {
|
|
623
|
+
const probe = probeHookBinary({ repoRoot, entrypoint: host.hookEntrypoint, sessionId, filePath });
|
|
624
|
+
if (probe && ((probe.decision === 'block') === (kernel.verdict === 'block'))) {
|
|
625
|
+
return { verdict: kernel, source: 'host_hook_binary', hookReason: probe.reason };
|
|
626
|
+
}
|
|
627
|
+
hookProbeDegraded = true;
|
|
628
|
+
limitations.push('The installed hook binary probe did not complete, so decisions were recorded from the decision kernel directly.');
|
|
629
|
+
}
|
|
630
|
+
return { verdict: kernel, source: 'kernel_library', hookReason: null };
|
|
631
|
+
};
|
|
632
|
+
const recordKernelEvent = (filePath, result, source, step) => {
|
|
633
|
+
if (source === 'host_hook_binary')
|
|
634
|
+
return; // the hook binary already recorded real events
|
|
635
|
+
try {
|
|
636
|
+
(0, governance_runtime_1.appendEvent)(repoRoot, sessionId, {
|
|
637
|
+
type: result.verdict === 'block' ? 'check_block' : result.verdict === 'warn' ? 'check_warn' : 'check_ok',
|
|
638
|
+
ts: new Date().toISOString(),
|
|
639
|
+
filePath,
|
|
640
|
+
verdict: result.verdict,
|
|
641
|
+
message: result.message,
|
|
642
|
+
detail: {
|
|
643
|
+
localFirstValueStep: step,
|
|
644
|
+
...(result.approvalContext ? { approvalContext: result.approvalContext } : {}),
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
// Evidence recording is best-effort; the decision itself already happened.
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
// Step 1 — the protected write is blocked before it lands.
|
|
653
|
+
const blockDecision = decide(pair.target);
|
|
654
|
+
const approvalCommand = `neurcode session approve --path ${pair.target} --session-id ${sessionId}`;
|
|
655
|
+
recordKernelEvent(pair.target, blockDecision.verdict, blockDecision.source, 'protected_write_blocked');
|
|
656
|
+
decisions.push({
|
|
657
|
+
step: 'protected_write_blocked',
|
|
658
|
+
path: pair.target,
|
|
659
|
+
verdict: blockDecision.verdict.verdict,
|
|
660
|
+
reasonCodes: decisionReasonCodes(blockDecision.verdict),
|
|
661
|
+
decisionSource: blockDecision.source,
|
|
662
|
+
});
|
|
663
|
+
(0, activation_telemetry_1.trackActivationEvent)({
|
|
664
|
+
eventType: 'first_block_observed',
|
|
665
|
+
commandFamily: 'pilot_start',
|
|
666
|
+
reasonCode: 'local_first_value.block_demonstrated',
|
|
667
|
+
flush: false,
|
|
668
|
+
});
|
|
669
|
+
// Step 2 — the human approves exactly one path.
|
|
670
|
+
const approval = await confirmApproval(pair.target, interactive);
|
|
671
|
+
let approvedExactPath = null;
|
|
672
|
+
let neighborContainment = 'not_evaluated';
|
|
673
|
+
const neighborPath = pair.neighbor;
|
|
674
|
+
let approvedWriteAllowed = false;
|
|
675
|
+
if (approval.approved) {
|
|
676
|
+
// sessionId must travel inside the options object: approveSession ignores
|
|
677
|
+
// its positional sessionId parameter when options are passed as an object,
|
|
678
|
+
// and would otherwise grant against the repo's active session.
|
|
679
|
+
(0, governance_runtime_1.approveSession)(repoRoot, pair.target, {
|
|
680
|
+
reason: 'local first-value proof exact-path approval',
|
|
681
|
+
approvedBy: approval.approvedBy,
|
|
682
|
+
sessionId,
|
|
683
|
+
});
|
|
684
|
+
approvedExactPath = pair.target;
|
|
685
|
+
decisions.push({
|
|
686
|
+
step: 'exact_path_approved',
|
|
687
|
+
path: pair.target,
|
|
688
|
+
verdict: 'approved',
|
|
689
|
+
reasonCodes: ['exact_path_approval_granted'],
|
|
690
|
+
decisionSource: 'kernel_library',
|
|
691
|
+
});
|
|
692
|
+
(0, activation_telemetry_1.trackActivationEvent)({
|
|
693
|
+
eventType: 'first_approval_observed',
|
|
694
|
+
commandFamily: 'pilot_start',
|
|
695
|
+
reasonCode: 'local_first_value.exact_approval',
|
|
696
|
+
flush: false,
|
|
697
|
+
});
|
|
698
|
+
// Step 3 — the approved write is allowed now.
|
|
699
|
+
const allowedDecision = decide(pair.target);
|
|
700
|
+
approvedWriteAllowed = allowedDecision.verdict.verdict !== 'block';
|
|
701
|
+
recordKernelEvent(pair.target, allowedDecision.verdict, allowedDecision.source, 'approved_write_allowed');
|
|
702
|
+
decisions.push({
|
|
703
|
+
step: 'approved_write_allowed',
|
|
704
|
+
path: pair.target,
|
|
705
|
+
verdict: allowedDecision.verdict.verdict,
|
|
706
|
+
reasonCodes: approvedWriteAllowed
|
|
707
|
+
? decisionReasonCodes(allowedDecision.verdict, ['approved_exact_path'])
|
|
708
|
+
: decisionReasonCodes(allowedDecision.verdict),
|
|
709
|
+
decisionSource: allowedDecision.source,
|
|
710
|
+
});
|
|
711
|
+
if (!approvedWriteAllowed) {
|
|
712
|
+
limitations.push('The approved path was still blocked on re-check; treat this proof as degraded and report it.');
|
|
713
|
+
}
|
|
714
|
+
// Step 4 — the neighbor stays blocked.
|
|
715
|
+
if (pair.neighbor) {
|
|
716
|
+
const neighborDecision = decide(pair.neighbor);
|
|
717
|
+
recordKernelEvent(pair.neighbor, neighborDecision.verdict, neighborDecision.source, 'neighbor_write_blocked');
|
|
718
|
+
const contained = neighborDecision.verdict.verdict === 'block';
|
|
719
|
+
decisions.push({
|
|
720
|
+
step: 'neighbor_write_blocked',
|
|
721
|
+
path: pair.neighbor,
|
|
722
|
+
verdict: neighborDecision.verdict.verdict,
|
|
723
|
+
reasonCodes: contained
|
|
724
|
+
? decisionReasonCodes(neighborDecision.verdict, ['neighbor_not_approved'])
|
|
725
|
+
: decisionReasonCodes(neighborDecision.verdict),
|
|
726
|
+
decisionSource: neighborDecision.source,
|
|
727
|
+
});
|
|
728
|
+
neighborContainment = contained ? 'contained' : 'not_contained';
|
|
729
|
+
if (!contained) {
|
|
730
|
+
limitations.push('Neighbor containment did not hold for the selected pair; treat this proof as degraded and report it.');
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
limitations.push('Only one provable protected file was found, so neighbor containment was not evaluated.');
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
limitations.push('The operator declined the exact-path approval, so the approval and containment steps were not demonstrated.');
|
|
739
|
+
}
|
|
740
|
+
// Finish the proof session so the evidence carries a replay hash.
|
|
741
|
+
let replayHash = null;
|
|
742
|
+
try {
|
|
743
|
+
const finished = (0, governance_runtime_1.finishSession)(repoRoot, sessionId, {
|
|
744
|
+
reason: 'local first-value proof finished',
|
|
745
|
+
completionStatus: approval.approved ? 'completed' : 'attention_required',
|
|
746
|
+
});
|
|
747
|
+
replayHash = finished?.replayHash ?? null;
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
limitations.push('The proof session could not be finalized; decisions above are still recorded in the session log.');
|
|
751
|
+
}
|
|
752
|
+
const nextLocal = host.tier === 'hard_hook'
|
|
753
|
+
? `Ask your agent to edit ${pair.target} — the write is blocked before it lands.`
|
|
754
|
+
: host.tier === 'hard_hook_degraded'
|
|
755
|
+
? `${host.recoveryCommand ?? 'neurcode runtime repair'} # restore hard enforcement, then re-run neurcode pilot start`
|
|
756
|
+
: (0, onboard_1.agentSetupCommandFor)((['claude', 'cursor', 'codex', 'copilot', 'vscode'].includes(host.agent) ? host.agent : 'terminal'));
|
|
757
|
+
const contained = neighborContainment === 'contained';
|
|
758
|
+
const blockDemonstrated = decisions.some((decision) => decision.step === 'protected_write_blocked' && decision.verdict === 'block');
|
|
759
|
+
const outcome = blockDemonstrated && approval.approved && approvedWriteAllowed && contained
|
|
760
|
+
? 'proof_complete'
|
|
761
|
+
: 'proof_degraded';
|
|
762
|
+
const artifact = finalizeArtifact({
|
|
763
|
+
schemaVersion: contracts_1.LOCAL_FIRST_VALUE_SCHEMA_VERSION,
|
|
764
|
+
proofId: proofIdFor(repoHash, generatedAt),
|
|
765
|
+
generatedAt,
|
|
766
|
+
repo: { label: repoLabel, hash: repoHash, gitDetected },
|
|
767
|
+
detection,
|
|
768
|
+
host: {
|
|
769
|
+
agent: host.agent,
|
|
770
|
+
enforcementTier: host.tier,
|
|
771
|
+
enforcementNote: host.note,
|
|
772
|
+
identityHealthy: host.identityHealthy,
|
|
773
|
+
recoveryCommand: host.recoveryCommand,
|
|
774
|
+
},
|
|
775
|
+
sessionId,
|
|
776
|
+
replayHash,
|
|
777
|
+
decisions,
|
|
778
|
+
blockedPathCount: decisions.filter((decision) => decision.verdict === 'block').length,
|
|
779
|
+
approvedExactPath,
|
|
780
|
+
neighborPath,
|
|
781
|
+
neighborContainment,
|
|
782
|
+
approvalCommand,
|
|
783
|
+
nextActions: { local: nextLocal, login: 'neurcode login' },
|
|
784
|
+
limitations: [...limitations, ...baseLimitations(host)],
|
|
785
|
+
privacy: {
|
|
786
|
+
sourceUploaded: false,
|
|
787
|
+
promptsUploaded: false,
|
|
788
|
+
diffsUploaded: false,
|
|
789
|
+
absolutePathsStored: false,
|
|
790
|
+
sourceFree: true,
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
const artifactFiles = writeArtifacts(repoRoot, artifact);
|
|
794
|
+
(0, activation_telemetry_1.trackActivationEvent)({
|
|
795
|
+
eventType: 'onboarding_step_completed',
|
|
796
|
+
commandFamily: 'pilot_start',
|
|
797
|
+
reasonCode: outcome === 'proof_complete' ? 'local_first_value.completed' : 'local_first_value.degraded',
|
|
798
|
+
flush: true,
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
ok: outcome === 'proof_complete',
|
|
802
|
+
outcome,
|
|
803
|
+
artifact,
|
|
804
|
+
artifactFiles,
|
|
805
|
+
text: renderLocalFirstValueText({ artifact, outcome, artifactFiles }),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
//# sourceMappingURL=local-first-value.js.map
|