@neurcode-ai/cli 0.16.4 → 0.16.6
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/.telemetry-bundle/dist/index.js +0 -0
- package/LICENSE +201 -0
- package/dist/api-client.d.ts +75 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +43 -0
- package/dist/api-client.js.map +1 -1
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +151 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/cursor.d.ts.map +1 -1
- package/dist/commands/cursor.js +72 -0
- package/dist/commands/cursor.js.map +1 -1
- package/dist/commands/eval.d.ts +19 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/eval.js +246 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/onboard.d.ts +29 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +247 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/runtime-doctor.d.ts.map +1 -1
- package/dist/commands/runtime-doctor.js +80 -9
- package/dist/commands/runtime-doctor.js.map +1 -1
- package/dist/commands/runtime-sync.d.ts.map +1 -1
- package/dist/commands/runtime-sync.js +22 -0
- package/dist/commands/runtime-sync.js.map +1 -1
- package/dist/commands/runtime.d.ts +18 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +321 -1
- package/dist/commands/runtime.js.map +1 -1
- package/dist/commands/session-hook.d.ts +10 -2
- package/dist/commands/session-hook.d.ts.map +1 -1
- package/dist/commands/session-hook.js +533 -122
- package/dist/commands/session-hook.js.map +1 -1
- package/dist/commands/session.d.ts +34 -0
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +243 -2
- package/dist/commands/session.js.map +1 -1
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime-build.json +5 -5
- package/dist/utils/agent-guard-supervisor.d.ts.map +1 -1
- package/dist/utils/agent-guard-supervisor.js +0 -1
- package/dist/utils/agent-guard-supervisor.js.map +1 -1
- package/dist/utils/cursor-gate.d.ts +1 -0
- package/dist/utils/cursor-gate.d.ts.map +1 -1
- package/dist/utils/cursor-gate.js +34 -7
- package/dist/utils/cursor-gate.js.map +1 -1
- package/dist/utils/guided-eval.d.ts +251 -0
- package/dist/utils/guided-eval.d.ts.map +1 -0
- package/dist/utils/guided-eval.js +880 -0
- package/dist/utils/guided-eval.js.map +1 -0
- package/dist/utils/local-repo-brain.d.ts +158 -0
- package/dist/utils/local-repo-brain.d.ts.map +1 -0
- package/dist/utils/local-repo-brain.js +854 -0
- package/dist/utils/local-repo-brain.js.map +1 -0
- package/dist/utils/runtime-live.d.ts +25 -0
- package/dist/utils/runtime-live.d.ts.map +1 -1
- package/dist/utils/runtime-live.js +103 -4
- package/dist/utils/runtime-live.js.map +1 -1
- package/dist/utils/runtime-outbox.d.ts +2 -1
- package/dist/utils/runtime-outbox.d.ts.map +1 -1
- package/dist/utils/runtime-outbox.js +21 -16
- package/dist/utils/runtime-outbox.js.map +1 -1
- package/dist/utils/session-allowlist-rules.d.ts +12 -0
- package/dist/utils/session-allowlist-rules.d.ts.map +1 -1
- package/dist/utils/session-allowlist-rules.js +61 -1
- package/dist/utils/session-allowlist-rules.js.map +1 -1
- package/dist/utils/structural-understanding.d.ts +61 -1
- package/dist/utils/structural-understanding.d.ts.map +1 -1
- package/dist/utils/structural-understanding.js +534 -1
- package/dist/utils/structural-understanding.js.map +1 -1
- package/dist/utils/v0-governance.d.ts.map +1 -1
- package/dist/utils/v0-governance.js +10 -0
- package/dist/utils/v0-governance.js.map +1 -1
- package/package.json +7 -8
|
@@ -22,6 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.resolveSessionForHook = resolveSessionForHook;
|
|
23
23
|
exports.normalizeHookFilePathForRepo = normalizeHookFilePathForRepo;
|
|
24
24
|
exports.hookFilePathCandidates = hookFilePathCandidates;
|
|
25
|
+
exports.evaluateNoActiveSessionWrite = evaluateNoActiveSessionWrite;
|
|
25
26
|
exports.shouldKeepSessionActiveForPendingApproval = shouldKeepSessionActiveForPendingApproval;
|
|
26
27
|
exports.sessionHookCommand = sessionHookCommand;
|
|
27
28
|
const child_process_1 = require("child_process");
|
|
@@ -41,6 +42,7 @@ const diff_parser_1 = require("@neurcode-ai/diff-parser");
|
|
|
41
42
|
const structural_understanding_1 = require("../utils/structural-understanding");
|
|
42
43
|
const consequence_nudges_1 = require("../utils/consequence-nudges");
|
|
43
44
|
const agent_guard_supervisor_1 = require("../utils/agent-guard-supervisor");
|
|
45
|
+
const local_repo_brain_1 = require("../utils/local-repo-brain");
|
|
44
46
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
45
47
|
/** Read the full hook JSON from stdin, or return {} on any error. */
|
|
46
48
|
function readHookInput() {
|
|
@@ -145,6 +147,41 @@ function sessionAlreadyEmittedNudge(session, nudgeKey) {
|
|
|
145
147
|
typeof event.detail === 'object' &&
|
|
146
148
|
event.detail['nudgeKey'] === nudgeKey);
|
|
147
149
|
}
|
|
150
|
+
function reuseNudgeKey(artifactHash, finding) {
|
|
151
|
+
return [
|
|
152
|
+
'reuse',
|
|
153
|
+
artifactHash,
|
|
154
|
+
finding.changed.file,
|
|
155
|
+
finding.changed.name,
|
|
156
|
+
finding.existing.file,
|
|
157
|
+
finding.existing.name,
|
|
158
|
+
finding.matchType,
|
|
159
|
+
].join(':');
|
|
160
|
+
}
|
|
161
|
+
function formatReuseNudge(finding) {
|
|
162
|
+
return finding.message;
|
|
163
|
+
}
|
|
164
|
+
function reuseNudgeFromArtifact(artifactHash, findings) {
|
|
165
|
+
const [finding] = findings;
|
|
166
|
+
if (!finding)
|
|
167
|
+
return null;
|
|
168
|
+
return {
|
|
169
|
+
nudgeVersion: 'reuse-v1',
|
|
170
|
+
nudgeKey: reuseNudgeKey(artifactHash, finding),
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
headline: formatReuseNudge(finding),
|
|
173
|
+
consequenceClass: 'repo-reuse',
|
|
174
|
+
operatorAction: 'Review the existing helper before merging; reuse or intentionally explain the duplicate.',
|
|
175
|
+
reviewFocus: [finding.changed.file, finding.existing.file],
|
|
176
|
+
artifactHash,
|
|
177
|
+
reuseFinding: finding,
|
|
178
|
+
surfacedReuseFindings: findings.slice(0, 3),
|
|
179
|
+
provenance: 'deterministic-static',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function isReuseNudge(nudge) {
|
|
183
|
+
return nudge.nudgeVersion === 'reuse-v1';
|
|
184
|
+
}
|
|
148
185
|
function readWorkingDiff(repoRoot) {
|
|
149
186
|
return (0, child_process_1.execFileSync)('git', ['-C', repoRoot, 'diff', '--no-ext-diff', 'HEAD'], {
|
|
150
187
|
encoding: 'utf8',
|
|
@@ -167,10 +204,12 @@ async function maybeRecordConsequenceNudge(repoRoot, session) {
|
|
|
167
204
|
});
|
|
168
205
|
const nudges = (0, consequence_nudges_1.selectInFlowConsequenceNudges)(artifact, { max: 3 });
|
|
169
206
|
const [nudge] = nudges;
|
|
170
|
-
|
|
207
|
+
const reuseNudge = reuseNudgeFromArtifact(artifact.artifactHash, artifact.reuseFindings);
|
|
208
|
+
const selectedNudge = nudge ?? reuseNudge;
|
|
209
|
+
if (!selectedNudge)
|
|
171
210
|
return null;
|
|
172
211
|
const latest = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId) || session;
|
|
173
|
-
if (sessionAlreadyEmittedNudge(latest,
|
|
212
|
+
if (sessionAlreadyEmittedNudge(latest, selectedNudge.nudgeKey))
|
|
174
213
|
return null;
|
|
175
214
|
const artifactPath = (0, structural_understanding_1.writeStructuralUnderstanding)(repoRoot, session.sessionId, artifact);
|
|
176
215
|
(0, governance_runtime_1.appendEvent)(repoRoot, session.sessionId, {
|
|
@@ -187,6 +226,8 @@ async function maybeRecordConsequenceNudge(repoRoot, session) {
|
|
|
187
226
|
changedFiles: artifact.changedFiles,
|
|
188
227
|
changedSymbols: artifact.changedSymbols,
|
|
189
228
|
digest: artifact.digest,
|
|
229
|
+
repoSymbolIndex: artifact.repoSymbolIndex,
|
|
230
|
+
reuseFindings: artifact.reuseFindings,
|
|
190
231
|
boundaryImpact: artifact.boundaryImpact,
|
|
191
232
|
suppressedArtifacts: artifact.suppressedArtifacts,
|
|
192
233
|
consequenceUnderstanding: {
|
|
@@ -204,103 +245,120 @@ async function maybeRecordConsequenceNudge(repoRoot, session) {
|
|
|
204
245
|
(0, governance_runtime_1.appendEvent)(repoRoot, session.sessionId, {
|
|
205
246
|
type: 'consequence_nudge',
|
|
206
247
|
ts: new Date().toISOString(),
|
|
207
|
-
message: nudge.
|
|
248
|
+
message: (nudge ?? reuseNudge)?.headline ?? 'Structural advisory recorded.',
|
|
208
249
|
detail: {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
250
|
+
...(nudge
|
|
251
|
+
? {
|
|
252
|
+
nudgeVersion: nudge.nudgeVersion,
|
|
253
|
+
nudgeKey: nudge.nudgeKey,
|
|
254
|
+
severity: nudge.severity,
|
|
255
|
+
consequenceClass: nudge.consequenceClass,
|
|
256
|
+
operatorAction: nudge.operatorAction,
|
|
257
|
+
reviewFocus: nudge.reviewFocus,
|
|
258
|
+
artifactHash: nudge.artifactHash,
|
|
259
|
+
impact: nudge.impact ? {
|
|
260
|
+
rank: nudge.impact.rank,
|
|
261
|
+
score: nudge.impact.score,
|
|
262
|
+
file: nudge.impact.file,
|
|
263
|
+
symbol: nudge.impact.symbol,
|
|
264
|
+
summary: nudge.impact.summary,
|
|
265
|
+
findingTypes: nudge.impact.findingTypes,
|
|
266
|
+
findingRanks: nudge.impact.findingRanks,
|
|
267
|
+
findingCount: nudge.impact.findingCount,
|
|
268
|
+
productionConsumerCount: nudge.impact.productionConsumerCount,
|
|
269
|
+
testConsumerCount: nudge.impact.testConsumerCount,
|
|
270
|
+
reachableProductionConsumerCount: nudge.impact.reachableProductionConsumerCount,
|
|
271
|
+
externalProductionConsumerCount: nudge.impact.externalProductionConsumerCount,
|
|
272
|
+
changedProductionConsumerCount: nudge.impact.changedProductionConsumerCount,
|
|
273
|
+
sensitiveConsumerCount: nudge.impact.sensitiveConsumerCount,
|
|
274
|
+
approvalRequiredConsumerCount: nudge.impact.approvalRequiredConsumerCount,
|
|
275
|
+
runtimeGovernanceConsumerCount: nudge.impact.runtimeGovernanceConsumerCount,
|
|
276
|
+
productionFiles: nudge.impact.productionFiles,
|
|
277
|
+
changedProductionFiles: nudge.impact.changedProductionFiles,
|
|
278
|
+
testFiles: nudge.impact.testFiles,
|
|
279
|
+
sensitiveFiles: nudge.impact.sensitiveFiles,
|
|
280
|
+
approvalRequiredFiles: nudge.impact.approvalRequiredFiles,
|
|
281
|
+
runtimeGovernanceFiles: nudge.impact.runtimeGovernanceFiles,
|
|
282
|
+
highFanout: nudge.impact.highFanout,
|
|
283
|
+
architectureRelevant: nudge.impact.architectureRelevant,
|
|
284
|
+
reasonCodes: nudge.impact.reasonCodes,
|
|
285
|
+
provenance: nudge.impact.provenance,
|
|
286
|
+
} : null,
|
|
287
|
+
finding: {
|
|
288
|
+
rank: nudge.finding.rank,
|
|
289
|
+
score: nudge.finding.score,
|
|
290
|
+
findingType: nudge.finding.findingType,
|
|
291
|
+
file: nudge.finding.file,
|
|
292
|
+
symbol: nudge.finding.symbol,
|
|
293
|
+
summary: nudge.finding.summary,
|
|
294
|
+
consumerCount: nudge.finding.consumerCount,
|
|
295
|
+
nonTestConsumerCount: nudge.finding.nonTestConsumerCount,
|
|
296
|
+
testConsumerCount: nudge.finding.testConsumerCount,
|
|
297
|
+
externalConsumerCount: nudge.finding.externalConsumerCount,
|
|
298
|
+
externalConsumerFiles: nudge.finding.externalConsumerFiles,
|
|
299
|
+
consumerSummary: nudge.finding.consumerSummary,
|
|
300
|
+
reasonCodes: nudge.finding.reasonCodes,
|
|
301
|
+
},
|
|
302
|
+
topImpacts: nudges
|
|
303
|
+
.map((item) => item.impact)
|
|
304
|
+
.filter((impact) => Boolean(impact))
|
|
305
|
+
.map((impact) => ({
|
|
306
|
+
rank: impact.rank,
|
|
307
|
+
score: impact.score,
|
|
308
|
+
file: impact.file,
|
|
309
|
+
symbol: impact.symbol,
|
|
310
|
+
summary: impact.summary,
|
|
311
|
+
findingTypes: impact.findingTypes,
|
|
312
|
+
findingCount: impact.findingCount,
|
|
313
|
+
reachableProductionConsumerCount: impact.reachableProductionConsumerCount,
|
|
314
|
+
externalProductionConsumerCount: impact.externalProductionConsumerCount,
|
|
315
|
+
changedProductionConsumerCount: impact.changedProductionConsumerCount,
|
|
316
|
+
productionFiles: impact.productionFiles,
|
|
317
|
+
changedProductionFiles: impact.changedProductionFiles,
|
|
318
|
+
sensitiveConsumerCount: impact.sensitiveConsumerCount,
|
|
319
|
+
approvalRequiredConsumerCount: impact.approvalRequiredConsumerCount,
|
|
320
|
+
runtimeGovernanceConsumerCount: impact.runtimeGovernanceConsumerCount,
|
|
321
|
+
highFanout: impact.highFanout,
|
|
322
|
+
architectureRelevant: impact.architectureRelevant,
|
|
323
|
+
reasonCodes: impact.reasonCodes,
|
|
324
|
+
})),
|
|
325
|
+
topFindings: nudges.map((item) => ({
|
|
326
|
+
nudgeKey: item.nudgeKey,
|
|
327
|
+
severity: item.severity,
|
|
328
|
+
consequenceClass: item.consequenceClass,
|
|
329
|
+
operatorAction: item.operatorAction,
|
|
330
|
+
reviewFocus: item.reviewFocus,
|
|
331
|
+
findingType: item.finding.findingType,
|
|
332
|
+
file: item.finding.file,
|
|
333
|
+
symbol: item.finding.symbol,
|
|
334
|
+
externalConsumerCount: item.finding.externalConsumerCount,
|
|
335
|
+
externalConsumerFiles: item.finding.externalConsumerFiles,
|
|
336
|
+
consumerSummary: item.finding.consumerSummary,
|
|
337
|
+
reasonCodes: item.finding.reasonCodes,
|
|
338
|
+
})),
|
|
339
|
+
provenance: nudge.provenance,
|
|
340
|
+
}
|
|
341
|
+
: reuseNudge
|
|
342
|
+
? {
|
|
343
|
+
nudgeVersion: reuseNudge.nudgeVersion,
|
|
344
|
+
nudgeKey: reuseNudge.nudgeKey,
|
|
345
|
+
severity: reuseNudge.severity,
|
|
346
|
+
consequenceClass: reuseNudge.consequenceClass,
|
|
347
|
+
operatorAction: reuseNudge.operatorAction,
|
|
348
|
+
reviewFocus: reuseNudge.reviewFocus,
|
|
349
|
+
artifactHash: reuseNudge.artifactHash,
|
|
350
|
+
reuseFinding: reuseNudge.reuseFinding,
|
|
351
|
+
topReuseFindings: reuseNudge.surfacedReuseFindings,
|
|
352
|
+
provenance: reuseNudge.provenance,
|
|
353
|
+
}
|
|
354
|
+
: {}),
|
|
297
355
|
killSwitch: 'NEURCODE_DISABLE_CONSEQUENCE_NUDGES=1',
|
|
298
356
|
},
|
|
299
357
|
});
|
|
300
358
|
const refreshed = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
|
|
301
359
|
if (refreshed)
|
|
302
360
|
await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, refreshed);
|
|
303
|
-
return nudge;
|
|
361
|
+
return nudge ?? reuseNudge;
|
|
304
362
|
}
|
|
305
363
|
catch (error) {
|
|
306
364
|
diagnostic(`consequence nudge skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -351,15 +409,117 @@ function hookFilePathCandidates(hookInput) {
|
|
|
351
409
|
}
|
|
352
410
|
return Array.from(new Set(candidates));
|
|
353
411
|
}
|
|
354
|
-
function
|
|
412
|
+
function runtimeMode(session) {
|
|
413
|
+
return session.contract.runtimeMode === 'strict' ||
|
|
414
|
+
session.contract.runtimeMode === 'paused' ||
|
|
415
|
+
session.contract.runtimeMode === 'advisory'
|
|
416
|
+
? session.contract.runtimeMode
|
|
417
|
+
: 'strict';
|
|
418
|
+
}
|
|
419
|
+
function blockContext(input) {
|
|
420
|
+
const isApproval = input.blockType === 'approval_required_boundary';
|
|
421
|
+
const isScope = input.blockType === 'scope_violation_or_task_expansion';
|
|
422
|
+
return {
|
|
423
|
+
schemaVersion: 'neurcode.runtime-block.v1',
|
|
424
|
+
blockType: input.blockType,
|
|
425
|
+
filePath: input.filePath || null,
|
|
426
|
+
message: input.message || null,
|
|
427
|
+
runtimeMode: input.runtimeMode || null,
|
|
428
|
+
operatorActionKind: isApproval
|
|
429
|
+
? 'exact_path_approval'
|
|
430
|
+
: isScope
|
|
431
|
+
? 'scope_amendment'
|
|
432
|
+
: input.blockType === 'profile_or_runtime_health_block'
|
|
433
|
+
? 'runtime_health_recovery'
|
|
434
|
+
: 'split_tool_call',
|
|
435
|
+
operatorActionLabel: isApproval
|
|
436
|
+
? 'Approve exact path / Deny'
|
|
437
|
+
: isScope
|
|
438
|
+
? 'Approve task expansion / Amend scope / Deny'
|
|
439
|
+
: input.blockType === 'profile_or_runtime_health_block'
|
|
440
|
+
? 'Refresh or restart runtime'
|
|
441
|
+
: 'Split into one file per tool call',
|
|
442
|
+
suggestedApprovalPath: isApproval ? input.suggestedApprovalPath || input.filePath || null : null,
|
|
443
|
+
owners: input.owners || [],
|
|
444
|
+
proposalId: input.proposalId || null,
|
|
445
|
+
nextAction: input.nextAction || (isApproval
|
|
446
|
+
? 'Approve only the exact path for this session, or deny the write.'
|
|
447
|
+
: isScope
|
|
448
|
+
? 'Accept the pending scope amendment or re-plan locally, then retry the write.'
|
|
449
|
+
: input.blockType === 'profile_or_runtime_health_block'
|
|
450
|
+
? 'Refresh the governance profile or restart the active governed session.'
|
|
451
|
+
: 'Retry as separate single-file edits so each path can be governed.'),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const NO_ACTIVE_SESSION_SCOPE_SENTINEL = '__neurcode_no_active_session_scope__';
|
|
455
|
+
function evaluateNoActiveSessionWrite(repoRoot, filePath) {
|
|
456
|
+
const profile = (0, v0_governance_1.ensureFreshGovernanceProfile)(repoRoot).profile;
|
|
457
|
+
const result = (0, governance_runtime_1.checkFileBoundary)({
|
|
458
|
+
filePath,
|
|
459
|
+
allowedGlobs: [NO_ACTIVE_SESSION_SCOPE_SENTINEL],
|
|
460
|
+
ownershipRules: profile.ownershipBoundaries,
|
|
461
|
+
sensitiveGlobs: profile.sensitiveBoundaries.map((boundary) => boundary.glob),
|
|
462
|
+
approvalRequiredGlobs: profile.approvalRequiredPaths,
|
|
463
|
+
approvedPaths: [],
|
|
464
|
+
approvalGrants: [],
|
|
465
|
+
scopeMode: 'explicit',
|
|
466
|
+
localMode: 'strict',
|
|
467
|
+
});
|
|
468
|
+
const protectedPath = result.isApprovalRequired || result.isSensitive || result.owners.length > 0;
|
|
469
|
+
const ownerNote = result.owners.length ? ` Owners: ${result.owners.join(', ')}.` : '';
|
|
470
|
+
const message = protectedPath
|
|
471
|
+
? `⏸ Neurcode: no active governed session is running, so protected path ${filePath} cannot be checked or approved safely.${ownerNote} Start a governed session with \`neurcode session-hook start\`/agent activation, or run \`neurcode doctor --runtime\` for recovery before retrying.`
|
|
472
|
+
: `No active governed session at ${repoRoot}; ${filePath} is not a detected protected path and is allowed advisory-only.`;
|
|
473
|
+
return {
|
|
474
|
+
block: protectedPath,
|
|
475
|
+
filePath,
|
|
476
|
+
result,
|
|
477
|
+
message,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function blockTypeFromEvent(event) {
|
|
481
|
+
const detail = event.detail || {};
|
|
482
|
+
const context = detail.blockContext;
|
|
483
|
+
if (context && typeof context === 'object') {
|
|
484
|
+
const value = context.blockType;
|
|
485
|
+
if (value === 'approval_required_boundary' ||
|
|
486
|
+
value === 'scope_violation_or_task_expansion' ||
|
|
487
|
+
value === 'profile_or_runtime_health_block' ||
|
|
488
|
+
value === 'multi_file_or_tool_shape_block') {
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (detail.approvalContext)
|
|
493
|
+
return 'approval_required_boundary';
|
|
494
|
+
if (detail.profileFreshness)
|
|
495
|
+
return 'profile_or_runtime_health_block';
|
|
496
|
+
if (detail.reason === 'multi_file_tool_call_requires_split')
|
|
497
|
+
return 'multi_file_or_tool_shape_block';
|
|
498
|
+
return 'scope_violation_or_task_expansion';
|
|
499
|
+
}
|
|
500
|
+
function latestUnresolvedActionableBlock(session) {
|
|
355
501
|
for (let i = session.events.length - 1; i >= 0; i -= 1) {
|
|
356
502
|
const event = session.events[i];
|
|
503
|
+
if (event.type === 'check_ok' || event.type === 'check_warn' || event.type === 'plan_amended') {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
357
506
|
if (event.type !== 'check_block')
|
|
358
507
|
continue;
|
|
359
|
-
const
|
|
508
|
+
const detail = event.detail;
|
|
509
|
+
const context = detail?.approvalContext;
|
|
360
510
|
const blockedPath = event.filePath || context?.blockedPath || context?.suggestedApprovalPath;
|
|
361
511
|
if (!blockedPath)
|
|
362
512
|
continue;
|
|
513
|
+
const blockType = blockTypeFromEvent(event);
|
|
514
|
+
if (blockType !== 'approval_required_boundary') {
|
|
515
|
+
return {
|
|
516
|
+
filePath: blockedPath,
|
|
517
|
+
blockType,
|
|
518
|
+
suggestedApprovalPath: detail?.blockContext?.suggestedApprovalPath || null,
|
|
519
|
+
proposalId: detail?.blockContext?.proposalId || null,
|
|
520
|
+
message: event.message || null,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
363
523
|
const verdict = (0, governance_runtime_1.checkFileBoundary)({
|
|
364
524
|
filePath: blockedPath,
|
|
365
525
|
allowedGlobs: session.contract.allowedGlobs,
|
|
@@ -369,11 +529,14 @@ function latestUnresolvedApprovalBlock(session) {
|
|
|
369
529
|
approvedPaths: session.contract.approvedPaths,
|
|
370
530
|
approvalGrants: session.contract.approvalGrants,
|
|
371
531
|
scopeMode: session.contract.scopeMode,
|
|
532
|
+
localMode: runtimeMode(session),
|
|
372
533
|
});
|
|
373
534
|
if (verdict.verdict === 'block' && verdict.approvalContext) {
|
|
374
535
|
return {
|
|
375
536
|
filePath: blockedPath,
|
|
537
|
+
blockType: 'approval_required_boundary',
|
|
376
538
|
suggestedApprovalPath: verdict.approvalContext.suggestedApprovalPath || context?.suggestedApprovalPath || blockedPath,
|
|
539
|
+
message: event.message || null,
|
|
377
540
|
};
|
|
378
541
|
}
|
|
379
542
|
return null;
|
|
@@ -383,6 +546,9 @@ function latestUnresolvedApprovalBlock(session) {
|
|
|
383
546
|
function shouldKeepSessionActiveForPendingApproval(session, pendingApproval) {
|
|
384
547
|
if (!pendingApproval)
|
|
385
548
|
return false;
|
|
549
|
+
if (pendingApproval.blockType && pendingApproval.blockType !== 'approval_required_boundary') {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
386
552
|
const hasRecordedApproval = session.contract.approvedPaths.length > 0 ||
|
|
387
553
|
(session.contract.approvalGrants ?? []).some((grant) => !grant.revokedAt) ||
|
|
388
554
|
session.events.some((event) => event.type === 'approval_decision' && event.decision === 'approved');
|
|
@@ -397,6 +563,7 @@ async function recordBashCheck(repoRoot, session, args) {
|
|
|
397
563
|
message: args.message,
|
|
398
564
|
detail: {
|
|
399
565
|
...(args.approvalContext ? { approvalContext: args.approvalContext } : {}),
|
|
566
|
+
...(args.blockContext ? { blockContext: args.blockContext } : {}),
|
|
400
567
|
toolName: 'Bash',
|
|
401
568
|
bash: {
|
|
402
569
|
operation: args.operation,
|
|
@@ -445,6 +612,7 @@ async function handleBashCheck(repoRoot, session, command) {
|
|
|
445
612
|
approvedPaths: session.contract.approvedPaths,
|
|
446
613
|
approvalGrants: session.contract.approvalGrants,
|
|
447
614
|
scopeMode: session.contract.scopeMode,
|
|
615
|
+
localMode: runtimeMode(session),
|
|
448
616
|
}),
|
|
449
617
|
}));
|
|
450
618
|
for (const { filePath, result } of results) {
|
|
@@ -457,13 +625,37 @@ async function handleBashCheck(repoRoot, session, command) {
|
|
|
457
625
|
commandFingerprint: analysis.commandFingerprint,
|
|
458
626
|
boundaryVerdict: result.verdict,
|
|
459
627
|
approvalContext: result.approvalContext,
|
|
628
|
+
blockContext: result.blockType
|
|
629
|
+
? blockContext({
|
|
630
|
+
blockType: result.blockType,
|
|
631
|
+
filePath,
|
|
632
|
+
message: result.message,
|
|
633
|
+
suggestedApprovalPath: result.approvalContext?.suggestedApprovalPath,
|
|
634
|
+
owners: result.owners,
|
|
635
|
+
runtimeMode: runtimeMode(session),
|
|
636
|
+
})
|
|
637
|
+
: undefined,
|
|
460
638
|
});
|
|
461
639
|
}
|
|
462
640
|
const blocking = results.find(({ result }) => result.verdict === 'block');
|
|
463
641
|
if (blocking) {
|
|
464
642
|
const message = `⏸ Neurcode: Bash ${analysis.operation} targets ${blocking.filePath}. ` +
|
|
465
643
|
blocking.result.message.replace(/^⏸ Neurcode:\s*/, '');
|
|
466
|
-
denyPreToolUse(message,
|
|
644
|
+
denyPreToolUse(message, {
|
|
645
|
+
...(blocking.result.approvalContext ? { approvalContext: blocking.result.approvalContext } : {}),
|
|
646
|
+
...(blocking.result.blockType
|
|
647
|
+
? {
|
|
648
|
+
blockContext: blockContext({
|
|
649
|
+
blockType: blocking.result.blockType,
|
|
650
|
+
filePath: blocking.filePath,
|
|
651
|
+
message,
|
|
652
|
+
suggestedApprovalPath: blocking.result.approvalContext?.suggestedApprovalPath,
|
|
653
|
+
owners: blocking.result.owners,
|
|
654
|
+
runtimeMode: runtimeMode(session),
|
|
655
|
+
}),
|
|
656
|
+
}
|
|
657
|
+
: {}),
|
|
658
|
+
});
|
|
467
659
|
}
|
|
468
660
|
const warning = results.find(({ result }) => result.verdict === 'warn');
|
|
469
661
|
if (warning) {
|
|
@@ -710,13 +902,53 @@ async function handleCheck(cmdCwd) {
|
|
|
710
902
|
const repoRoot = (0, v0_governance_1.resolveRepoRoot)(effectiveCwd);
|
|
711
903
|
(0, hook_heartbeat_1.recordHookHeartbeat)({ repoRoot, eventType: 'check' });
|
|
712
904
|
const requestedSessionId = sessionIdFromHookInput(hookInput);
|
|
905
|
+
const toolName = hookInput['tool_name'] ||
|
|
906
|
+
hookInput['toolName'] ||
|
|
907
|
+
'';
|
|
908
|
+
const toolInput = hookInput['tool_input'] ??
|
|
909
|
+
hookInput['toolInput'] ??
|
|
910
|
+
{};
|
|
713
911
|
const resolution = resolveSessionForHook(repoRoot, requestedSessionId);
|
|
714
912
|
const activeSession = resolution.session;
|
|
715
913
|
if (!activeSession) {
|
|
716
|
-
|
|
914
|
+
const rawPaths = hookFilePathCandidates(hookInput);
|
|
915
|
+
const bashLike = /^(bash|shell|runCommand|run_command|runInTerminal|run_in_terminal|terminal)$/i.test(toolName);
|
|
916
|
+
const bashAnalysis = bashLike
|
|
917
|
+
? (0, bash_command_analysis_1.analyzeBashCommand)(toolInput['command'] ||
|
|
918
|
+
toolInput['cmd'] ||
|
|
919
|
+
hookInput['command'] ||
|
|
920
|
+
'')
|
|
921
|
+
: null;
|
|
922
|
+
const candidatePaths = bashAnalysis?.mutates
|
|
923
|
+
? bashAnalysis.targetPaths
|
|
924
|
+
: rawPaths;
|
|
925
|
+
const normalizedPaths = Array.from(new Set(candidatePaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot))));
|
|
926
|
+
for (const filePath of normalizedPaths) {
|
|
927
|
+
try {
|
|
928
|
+
const decision = evaluateNoActiveSessionWrite(repoRoot, filePath);
|
|
929
|
+
if (decision.block) {
|
|
930
|
+
denyPreToolUse(decision.message, {
|
|
931
|
+
...(decision.result.approvalContext ? { approvalContext: decision.result.approvalContext } : {}),
|
|
932
|
+
blockContext: blockContext({
|
|
933
|
+
blockType: 'profile_or_runtime_health_block',
|
|
934
|
+
filePath,
|
|
935
|
+
message: decision.message,
|
|
936
|
+
suggestedApprovalPath: decision.result.approvalContext?.suggestedApprovalPath,
|
|
937
|
+
owners: decision.result.owners,
|
|
938
|
+
runtimeMode: 'strict',
|
|
939
|
+
nextAction: 'Start or resume a governed Neurcode session, then retry this protected path.',
|
|
940
|
+
}),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
diagnostic(`no-active-session protected-path check skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const targetNote = normalizedPaths.length > 0 ? ` for ${normalizedPaths.join(', ')}` : '';
|
|
717
949
|
diagnostic(requestedSessionId
|
|
718
|
-
? `no active session ${requestedSessionId} at ${repoRoot} — edit allowed
|
|
719
|
-
: `no active session at ${repoRoot} — edit allowed
|
|
950
|
+
? `no active session ${requestedSessionId} at ${repoRoot} — edit allowed advisory-only${targetNote}`
|
|
951
|
+
: `no active session at ${repoRoot} — edit allowed advisory-only${targetNote}`);
|
|
720
952
|
process.exit(0);
|
|
721
953
|
return;
|
|
722
954
|
}
|
|
@@ -728,7 +960,7 @@ async function handleCheck(cmdCwd) {
|
|
|
728
960
|
const hasPriorBlock = session.events.some((event) => event.type === 'check_block');
|
|
729
961
|
if (hasPriorBlock) {
|
|
730
962
|
const pending = await (0, runtime_live_1.applyPendingRuntimeLiveApprovals)(repoRoot, session.sessionId);
|
|
731
|
-
if (pending.applied > 0 || pending.revoked > 0) {
|
|
963
|
+
if (pending.applied > 0 || pending.revoked > 0 || pending.scopeAmended > 0 || pending.scopeDenied > 0) {
|
|
732
964
|
const refreshed = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
|
|
733
965
|
if (refreshed)
|
|
734
966
|
session = refreshed;
|
|
@@ -739,6 +971,12 @@ async function handleCheck(cmdCwd) {
|
|
|
739
971
|
if (pending.revoked > 0) {
|
|
740
972
|
diagnostic(`revoked ${pending.revoked} dashboard approval${pending.revoked === 1 ? '' : 's'}`);
|
|
741
973
|
}
|
|
974
|
+
if (pending.scopeAmended > 0) {
|
|
975
|
+
diagnostic(`applied ${pending.scopeAmended} dashboard scope amendment${pending.scopeAmended === 1 ? '' : 's'}`);
|
|
976
|
+
}
|
|
977
|
+
if (pending.scopeDenied > 0) {
|
|
978
|
+
diagnostic(`recorded ${pending.scopeDenied} denied dashboard scope amendment${pending.scopeDenied === 1 ? '' : 's'}`);
|
|
979
|
+
}
|
|
742
980
|
}
|
|
743
981
|
}
|
|
744
982
|
catch {
|
|
@@ -776,12 +1014,6 @@ async function handleCheck(cmdCwd) {
|
|
|
776
1014
|
// ── Extract the target file path ─────────────────────────────────────────
|
|
777
1015
|
// Claude Code PreToolUse payload shape:
|
|
778
1016
|
// { tool_name, tool_input: { path, ... }, cwd, ... }
|
|
779
|
-
const toolName = hookInput['tool_name'] ||
|
|
780
|
-
hookInput['toolName'] ||
|
|
781
|
-
'';
|
|
782
|
-
const toolInput = hookInput['tool_input'] ??
|
|
783
|
-
hookInput['toolInput'] ??
|
|
784
|
-
{};
|
|
785
1017
|
if (/^(bash|shell|runCommand|run_command|runInTerminal|run_in_terminal|terminal)$/i.test(toolName)) {
|
|
786
1018
|
const command = toolInput['command'] ||
|
|
787
1019
|
toolInput['cmd'] ||
|
|
@@ -802,6 +1034,12 @@ async function handleCheck(cmdCwd) {
|
|
|
802
1034
|
verdict: 'block',
|
|
803
1035
|
message,
|
|
804
1036
|
detail: {
|
|
1037
|
+
blockContext: blockContext({
|
|
1038
|
+
blockType: 'multi_file_or_tool_shape_block',
|
|
1039
|
+
filePath: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)).join(','),
|
|
1040
|
+
message,
|
|
1041
|
+
runtimeMode: runtimeMode(session),
|
|
1042
|
+
}),
|
|
805
1043
|
reason: 'multi_file_tool_call_requires_split',
|
|
806
1044
|
paths: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)),
|
|
807
1045
|
toolName,
|
|
@@ -815,6 +1053,12 @@ async function handleCheck(cmdCwd) {
|
|
|
815
1053
|
// Recording failure must not weaken the deny.
|
|
816
1054
|
}
|
|
817
1055
|
denyPreToolUse(message, {
|
|
1056
|
+
blockContext: blockContext({
|
|
1057
|
+
blockType: 'multi_file_or_tool_shape_block',
|
|
1058
|
+
filePath: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)).join(','),
|
|
1059
|
+
message,
|
|
1060
|
+
runtimeMode: runtimeMode(session),
|
|
1061
|
+
}),
|
|
818
1062
|
reason: 'multi_file_tool_call_requires_split',
|
|
819
1063
|
paths: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)),
|
|
820
1064
|
});
|
|
@@ -864,7 +1108,15 @@ async function handleCheck(cmdCwd) {
|
|
|
864
1108
|
filePath,
|
|
865
1109
|
verdict: 'block',
|
|
866
1110
|
message,
|
|
867
|
-
detail: {
|
|
1111
|
+
detail: {
|
|
1112
|
+
profileFreshness,
|
|
1113
|
+
blockContext: blockContext({
|
|
1114
|
+
blockType: 'profile_or_runtime_health_block',
|
|
1115
|
+
filePath,
|
|
1116
|
+
message,
|
|
1117
|
+
runtimeMode: runtimeMode(session),
|
|
1118
|
+
}),
|
|
1119
|
+
},
|
|
868
1120
|
});
|
|
869
1121
|
const refreshedSession = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
|
|
870
1122
|
if (refreshedSession) {
|
|
@@ -874,7 +1126,15 @@ async function handleCheck(cmdCwd) {
|
|
|
874
1126
|
catch {
|
|
875
1127
|
// Recording failure must not weaken the deny.
|
|
876
1128
|
}
|
|
877
|
-
denyPreToolUse(message, {
|
|
1129
|
+
denyPreToolUse(message, {
|
|
1130
|
+
profileFreshness,
|
|
1131
|
+
blockContext: blockContext({
|
|
1132
|
+
blockType: 'profile_or_runtime_health_block',
|
|
1133
|
+
filePath,
|
|
1134
|
+
message,
|
|
1135
|
+
runtimeMode: runtimeMode(session),
|
|
1136
|
+
}),
|
|
1137
|
+
});
|
|
878
1138
|
}
|
|
879
1139
|
if (staleness.status !== 'fresh') {
|
|
880
1140
|
const refreshedProfile = (0, v0_governance_1.ensureFreshGovernanceProfile)(repoRoot);
|
|
@@ -899,6 +1159,12 @@ async function handleCheck(cmdCwd) {
|
|
|
899
1159
|
verdict: 'block',
|
|
900
1160
|
message,
|
|
901
1161
|
detail: {
|
|
1162
|
+
blockContext: blockContext({
|
|
1163
|
+
blockType: 'profile_or_runtime_health_block',
|
|
1164
|
+
filePath,
|
|
1165
|
+
message,
|
|
1166
|
+
runtimeMode: runtimeMode(session),
|
|
1167
|
+
}),
|
|
902
1168
|
profileFreshness: {
|
|
903
1169
|
status: 'unreadable',
|
|
904
1170
|
refreshed: false,
|
|
@@ -919,7 +1185,14 @@ async function handleCheck(cmdCwd) {
|
|
|
919
1185
|
catch {
|
|
920
1186
|
// Recording failure must not weaken the deny.
|
|
921
1187
|
}
|
|
922
|
-
denyPreToolUse(message
|
|
1188
|
+
denyPreToolUse(message, {
|
|
1189
|
+
blockContext: blockContext({
|
|
1190
|
+
blockType: 'profile_or_runtime_health_block',
|
|
1191
|
+
filePath,
|
|
1192
|
+
message,
|
|
1193
|
+
runtimeMode: runtimeMode(session),
|
|
1194
|
+
}),
|
|
1195
|
+
});
|
|
923
1196
|
}
|
|
924
1197
|
// ── Run the boundary + intent-coherence checks ───────────────────────────
|
|
925
1198
|
let result;
|
|
@@ -933,6 +1206,7 @@ async function handleCheck(cmdCwd) {
|
|
|
933
1206
|
approvedPaths: session.contract.approvedPaths,
|
|
934
1207
|
approvalGrants: session.contract.approvalGrants,
|
|
935
1208
|
scopeMode: session.contract.scopeMode,
|
|
1209
|
+
localMode: runtimeMode(session),
|
|
936
1210
|
});
|
|
937
1211
|
}
|
|
938
1212
|
catch (err) {
|
|
@@ -963,19 +1237,50 @@ async function handleCheck(cmdCwd) {
|
|
|
963
1237
|
graph: session.contract.architectureGraph,
|
|
964
1238
|
obligations: session.contract.architectureObligations ?? [],
|
|
965
1239
|
});
|
|
966
|
-
|
|
1240
|
+
let pendingScopeAmendmentProposalId = null;
|
|
1241
|
+
if (result.verdict === 'ok' && planCoherencePolicy.action === 'block' && runtimeMode(session) !== 'strict') {
|
|
1242
|
+
result = {
|
|
1243
|
+
...result,
|
|
1244
|
+
verdict: 'warn',
|
|
1245
|
+
blockType: 'scope_violation_or_task_expansion',
|
|
1246
|
+
message: `⚠️ Neurcode: ${filePath} is not justified by the agent's stated plan. ` +
|
|
1247
|
+
`${planCoherencePolicy.reason} Proceeding in ${runtimeMode(session)} mode — recorded as task expansion evidence. ` +
|
|
1248
|
+
`Re-plan with neurcode_session_replan if this path should become part of the task.`,
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
else if (result.verdict === 'ok' && planCoherencePolicy.action === 'block') {
|
|
1252
|
+
try {
|
|
1253
|
+
const amendment = (0, governance_runtime_1.amendAgentPlan)(repoRoot, {
|
|
1254
|
+
sessionId: session.sessionId,
|
|
1255
|
+
addExpectedFiles: [filePath],
|
|
1256
|
+
addSteps: [`Expand governed task scope to include ${filePath}`],
|
|
1257
|
+
reason: `scope expansion requested for ${filePath}`,
|
|
1258
|
+
source: 'unknown',
|
|
1259
|
+
proposedBy: 'agent',
|
|
1260
|
+
amendedAt: new Date().toISOString(),
|
|
1261
|
+
});
|
|
1262
|
+
pendingScopeAmendmentProposalId = amendment.proposal?.proposalId || amendment.eventId || null;
|
|
1263
|
+
const amendedSession = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
|
|
1264
|
+
if (amendedSession)
|
|
1265
|
+
session = amendedSession;
|
|
1266
|
+
}
|
|
1267
|
+
catch (error) {
|
|
1268
|
+
diagnostic(`scope amendment proposal could not be recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
1269
|
+
}
|
|
967
1270
|
result = {
|
|
968
1271
|
...result,
|
|
969
1272
|
verdict: 'block',
|
|
1273
|
+
blockType: 'scope_violation_or_task_expansion',
|
|
970
1274
|
message: `⏸ Neurcode: ${filePath} is not justified by the agent's stated plan. ` +
|
|
971
|
-
`${planCoherencePolicy.reason}
|
|
972
|
-
`Use
|
|
1275
|
+
`${planCoherencePolicy.reason} Approve task expansion / amend scope, then retry this path. ` +
|
|
1276
|
+
`Use neurcode_session_replan_decide${pendingScopeAmendmentProposalId ? ` for ${pendingScopeAmendmentProposalId}` : ''} or \`neurcode session replan --add-file ${filePath}\`.`,
|
|
973
1277
|
};
|
|
974
1278
|
}
|
|
975
1279
|
else if (result.verdict === 'ok' && architectureObligationFeedback.action === 'block') {
|
|
976
1280
|
result = {
|
|
977
1281
|
...result,
|
|
978
1282
|
verdict: 'block',
|
|
1283
|
+
blockType: 'scope_violation_or_task_expansion',
|
|
979
1284
|
message: `⏸ Neurcode: ${filePath} is blocked by ${architectureObligationFeedback.blocking.length} ` +
|
|
980
1285
|
`architecture obligation${architectureObligationFeedback.blocking.length === 1 ? '' : 's'}. ` +
|
|
981
1286
|
`${architectureObligationFeedback.reasons[0]} Satisfy the obligation, re-plan, or ask the human to waive it with ` +
|
|
@@ -1024,6 +1329,19 @@ async function handleCheck(cmdCwd) {
|
|
|
1024
1329
|
message: result.message,
|
|
1025
1330
|
detail: {
|
|
1026
1331
|
...(result.approvalContext ? { approvalContext: result.approvalContext } : {}),
|
|
1332
|
+
...(result.blockType
|
|
1333
|
+
? {
|
|
1334
|
+
blockContext: blockContext({
|
|
1335
|
+
blockType: result.blockType,
|
|
1336
|
+
filePath,
|
|
1337
|
+
message: result.message,
|
|
1338
|
+
suggestedApprovalPath: result.approvalContext?.suggestedApprovalPath,
|
|
1339
|
+
owners: result.owners,
|
|
1340
|
+
proposalId: pendingScopeAmendmentProposalId,
|
|
1341
|
+
runtimeMode: runtimeMode(session),
|
|
1342
|
+
}),
|
|
1343
|
+
}
|
|
1344
|
+
: {}),
|
|
1027
1345
|
intentCoherence,
|
|
1028
1346
|
planCoherence,
|
|
1029
1347
|
planCoherencePolicy,
|
|
@@ -1052,11 +1370,66 @@ async function handleCheck(cmdCwd) {
|
|
|
1052
1370
|
const consequenceNudge = result.verdict === 'block'
|
|
1053
1371
|
? null
|
|
1054
1372
|
: await maybeRecordConsequenceNudge(repoRoot, session);
|
|
1373
|
+
// ── Repo brain facts (P0/P1) ─────────────────────────────────────────────
|
|
1374
|
+
// Load source-free repo context for the blocked/warned path. Best-effort:
|
|
1375
|
+
// if the artifact is missing the message is unchanged and recovery guidance
|
|
1376
|
+
// is appended instead.
|
|
1377
|
+
let repoBrainFileFacts = null;
|
|
1378
|
+
let repoBrainMeta = {
|
|
1379
|
+
artifactHash: null,
|
|
1380
|
+
generatedAt: null,
|
|
1381
|
+
status: 'missing',
|
|
1382
|
+
};
|
|
1383
|
+
try {
|
|
1384
|
+
const brainCtx = (0, local_repo_brain_1.getRepoBrainContext)(repoRoot, [filePath]);
|
|
1385
|
+
repoBrainMeta = { artifactHash: brainCtx.artifactHash, generatedAt: brainCtx.generatedAt, status: brainCtx.status };
|
|
1386
|
+
repoBrainFileFacts = brainCtx.files[0] ?? null;
|
|
1387
|
+
}
|
|
1388
|
+
catch {
|
|
1389
|
+
// Never let brain context loading break the enforcement path.
|
|
1390
|
+
}
|
|
1055
1391
|
// ── Emit hook response ────────────────────────────────────────────────────
|
|
1056
1392
|
if (result.verdict === 'block') {
|
|
1393
|
+
// Enrich deny message with source-free repo facts (P1).
|
|
1394
|
+
const brainSuffix = repoBrainFileFacts
|
|
1395
|
+
? (0, local_repo_brain_1.formatRepoBrainFactsForMessage)(repoBrainFileFacts)
|
|
1396
|
+
: repoBrainMeta.status === 'missing'
|
|
1397
|
+
? 'Run `neurcode brain index` for local source-free repo context.'
|
|
1398
|
+
: '';
|
|
1399
|
+
const enrichedMessage = brainSuffix
|
|
1400
|
+
? `${result.message} | ${brainSuffix}`
|
|
1401
|
+
: result.message;
|
|
1057
1402
|
// Include machine-readable approvalContext when the block is approval-required,
|
|
1058
1403
|
// so the agent can surface a structured approval request to the human.
|
|
1059
|
-
denyPreToolUse(
|
|
1404
|
+
denyPreToolUse(enrichedMessage, {
|
|
1405
|
+
...(result.approvalContext ? { approvalContext: result.approvalContext } : {}),
|
|
1406
|
+
...(result.blockType
|
|
1407
|
+
? {
|
|
1408
|
+
blockContext: {
|
|
1409
|
+
...blockContext({
|
|
1410
|
+
blockType: result.blockType,
|
|
1411
|
+
filePath,
|
|
1412
|
+
message: enrichedMessage,
|
|
1413
|
+
suggestedApprovalPath: result.approvalContext?.suggestedApprovalPath,
|
|
1414
|
+
owners: result.owners,
|
|
1415
|
+
proposalId: pendingScopeAmendmentProposalId,
|
|
1416
|
+
runtimeMode: runtimeMode(session),
|
|
1417
|
+
}),
|
|
1418
|
+
...(repoBrainFileFacts ? {
|
|
1419
|
+
repoBrainFacts: {
|
|
1420
|
+
sensitiveKinds: repoBrainFileFacts.sensitiveKinds,
|
|
1421
|
+
module: repoBrainFileFacts.module,
|
|
1422
|
+
hotspot: repoBrainFileFacts.hotspot,
|
|
1423
|
+
ownerBoundary: repoBrainFileFacts.ownerBoundary,
|
|
1424
|
+
reuseAdvisories: repoBrainFileFacts.reuseAdvisories,
|
|
1425
|
+
artifactHash: repoBrainMeta.artifactHash,
|
|
1426
|
+
generatedAt: repoBrainMeta.generatedAt,
|
|
1427
|
+
},
|
|
1428
|
+
} : { repoBrainStatus: 'missing', repoBrainRecovery: 'neurcode brain index' }),
|
|
1429
|
+
},
|
|
1430
|
+
}
|
|
1431
|
+
: {}),
|
|
1432
|
+
});
|
|
1060
1433
|
}
|
|
1061
1434
|
if (result.verdict === 'warn') {
|
|
1062
1435
|
const reason = consequenceNudge
|
|
@@ -1071,6 +1444,28 @@ async function handleCheck(cmdCwd) {
|
|
|
1071
1444
|
}) + '\n');
|
|
1072
1445
|
process.exit(0);
|
|
1073
1446
|
}
|
|
1447
|
+
if (consequenceNudge && isReuseNudge(consequenceNudge)) {
|
|
1448
|
+
process.stdout.write(JSON.stringify({
|
|
1449
|
+
hookSpecificOutput: {
|
|
1450
|
+
hookEventName: 'PreToolUse',
|
|
1451
|
+
permissionDecision: 'allow',
|
|
1452
|
+
reason: consequenceNudge.headline,
|
|
1453
|
+
reuseNudge: {
|
|
1454
|
+
nudgeVersion: consequenceNudge.nudgeVersion,
|
|
1455
|
+
nudgeKey: consequenceNudge.nudgeKey,
|
|
1456
|
+
severity: consequenceNudge.severity,
|
|
1457
|
+
consequenceClass: consequenceNudge.consequenceClass,
|
|
1458
|
+
operatorAction: consequenceNudge.operatorAction,
|
|
1459
|
+
reviewFocus: consequenceNudge.reviewFocus,
|
|
1460
|
+
artifactHash: consequenceNudge.artifactHash,
|
|
1461
|
+
finding: consequenceNudge.reuseFinding,
|
|
1462
|
+
surfacedFindingLimit: 3,
|
|
1463
|
+
topFindings: consequenceNudge.surfacedReuseFindings,
|
|
1464
|
+
},
|
|
1465
|
+
},
|
|
1466
|
+
}) + '\n');
|
|
1467
|
+
process.exit(0);
|
|
1468
|
+
}
|
|
1074
1469
|
if (consequenceNudge) {
|
|
1075
1470
|
process.stdout.write(JSON.stringify({
|
|
1076
1471
|
hookSpecificOutput: {
|
|
@@ -1168,11 +1563,17 @@ async function handleFinish(cmdCwd) {
|
|
|
1168
1563
|
diagnostic(`Claude session_id ${requestedSessionId} did not match a Neurcode session; finishing active session ${session.sessionId}`);
|
|
1169
1564
|
}
|
|
1170
1565
|
try {
|
|
1171
|
-
const
|
|
1172
|
-
if (shouldKeepSessionActiveForPendingApproval(session,
|
|
1566
|
+
const pendingActionableBlock = latestUnresolvedActionableBlock(session);
|
|
1567
|
+
if (shouldKeepSessionActiveForPendingApproval(session, pendingActionableBlock)) {
|
|
1568
|
+
const actionLabel = pendingActionableBlock.blockType === 'approval_required_boundary'
|
|
1569
|
+
? `exact approval of ${pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath}`
|
|
1570
|
+
: pendingActionableBlock.blockType === 'scope_violation_or_task_expansion'
|
|
1571
|
+
? `scope amendment for ${pendingActionableBlock.filePath}`
|
|
1572
|
+
: pendingActionableBlock.blockType === 'profile_or_runtime_health_block'
|
|
1573
|
+
? 'runtime/profile recovery'
|
|
1574
|
+
: 'a split single-file retry';
|
|
1173
1575
|
process.stdout.write(JSON.stringify({
|
|
1174
|
-
message: `⏸ Neurcode session ${session.sessionId} remains active; waiting for
|
|
1175
|
-
`${pendingApproval.suggestedApprovalPath}.`,
|
|
1576
|
+
message: `⏸ Neurcode session ${session.sessionId} remains active; waiting for operator action: ${actionLabel}.`,
|
|
1176
1577
|
}) + '\n');
|
|
1177
1578
|
await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, session);
|
|
1178
1579
|
try {
|
|
@@ -1185,10 +1586,20 @@ async function handleFinish(cmdCwd) {
|
|
|
1185
1586
|
}
|
|
1186
1587
|
return;
|
|
1187
1588
|
}
|
|
1188
|
-
const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId,
|
|
1589
|
+
const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, pendingActionableBlock
|
|
1189
1590
|
? {
|
|
1190
|
-
reason: '
|
|
1191
|
-
|
|
1591
|
+
reason: pendingActionableBlock.blockType === 'approval_required_boundary'
|
|
1592
|
+
? 'finished_with_unresolved_approval_blocks'
|
|
1593
|
+
: 'finished_with_unresolved_actionable_blocks',
|
|
1594
|
+
unresolvedActionableBlocks: [pendingActionableBlock],
|
|
1595
|
+
...(pendingActionableBlock.blockType === 'approval_required_boundary'
|
|
1596
|
+
? {
|
|
1597
|
+
unresolvedApprovalBlocks: [{
|
|
1598
|
+
filePath: pendingActionableBlock.filePath,
|
|
1599
|
+
suggestedApprovalPath: pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath,
|
|
1600
|
+
}],
|
|
1601
|
+
}
|
|
1602
|
+
: {}),
|
|
1192
1603
|
}
|
|
1193
1604
|
: undefined);
|
|
1194
1605
|
if (!finished)
|
|
@@ -1199,11 +1610,11 @@ async function handleFinish(cmdCwd) {
|
|
|
1199
1610
|
}
|
|
1200
1611
|
const blockCount = finished.events.filter((e) => e.type === 'check_block').length;
|
|
1201
1612
|
const warnCount = finished.events.filter((e) => e.type === 'check_warn').length;
|
|
1202
|
-
const unresolvedLine =
|
|
1203
|
-
? ` Unresolved: 1
|
|
1613
|
+
const unresolvedLine = pendingActionableBlock
|
|
1614
|
+
? ` Unresolved: 1 ${pendingActionableBlock.blockType} left recorded (${pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath})`
|
|
1204
1615
|
: null;
|
|
1205
1616
|
const summary = [
|
|
1206
|
-
|
|
1617
|
+
pendingActionableBlock
|
|
1207
1618
|
? `✅ Neurcode session ${finished.sessionId} complete with unresolved block evidence`
|
|
1208
1619
|
: `✅ Neurcode session ${finished.sessionId} complete`,
|
|
1209
1620
|
` Scope mode: ${finished.contract.scopeMode}`,
|