@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.
Files changed (76) hide show
  1. package/.telemetry-bundle/dist/index.js +0 -0
  2. package/LICENSE +201 -0
  3. package/dist/api-client.d.ts +75 -0
  4. package/dist/api-client.d.ts.map +1 -1
  5. package/dist/api-client.js +43 -0
  6. package/dist/api-client.js.map +1 -1
  7. package/dist/commands/brain.d.ts.map +1 -1
  8. package/dist/commands/brain.js +151 -0
  9. package/dist/commands/brain.js.map +1 -1
  10. package/dist/commands/cursor.d.ts.map +1 -1
  11. package/dist/commands/cursor.js +72 -0
  12. package/dist/commands/cursor.js.map +1 -1
  13. package/dist/commands/eval.d.ts +19 -0
  14. package/dist/commands/eval.d.ts.map +1 -0
  15. package/dist/commands/eval.js +246 -0
  16. package/dist/commands/eval.js.map +1 -0
  17. package/dist/commands/onboard.d.ts +29 -0
  18. package/dist/commands/onboard.d.ts.map +1 -0
  19. package/dist/commands/onboard.js +247 -0
  20. package/dist/commands/onboard.js.map +1 -0
  21. package/dist/commands/runtime-doctor.d.ts.map +1 -1
  22. package/dist/commands/runtime-doctor.js +80 -9
  23. package/dist/commands/runtime-doctor.js.map +1 -1
  24. package/dist/commands/runtime-sync.d.ts.map +1 -1
  25. package/dist/commands/runtime-sync.js +22 -0
  26. package/dist/commands/runtime-sync.js.map +1 -1
  27. package/dist/commands/runtime.d.ts +18 -0
  28. package/dist/commands/runtime.d.ts.map +1 -1
  29. package/dist/commands/runtime.js +321 -1
  30. package/dist/commands/runtime.js.map +1 -1
  31. package/dist/commands/session-hook.d.ts +10 -2
  32. package/dist/commands/session-hook.d.ts.map +1 -1
  33. package/dist/commands/session-hook.js +533 -122
  34. package/dist/commands/session-hook.js.map +1 -1
  35. package/dist/commands/session.d.ts +34 -0
  36. package/dist/commands/session.d.ts.map +1 -1
  37. package/dist/commands/session.js +243 -2
  38. package/dist/commands/session.js.map +1 -1
  39. package/dist/index.js +84 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/runtime-build.json +5 -5
  42. package/dist/utils/agent-guard-supervisor.d.ts.map +1 -1
  43. package/dist/utils/agent-guard-supervisor.js +0 -1
  44. package/dist/utils/agent-guard-supervisor.js.map +1 -1
  45. package/dist/utils/cursor-gate.d.ts +1 -0
  46. package/dist/utils/cursor-gate.d.ts.map +1 -1
  47. package/dist/utils/cursor-gate.js +34 -7
  48. package/dist/utils/cursor-gate.js.map +1 -1
  49. package/dist/utils/guided-eval.d.ts +251 -0
  50. package/dist/utils/guided-eval.d.ts.map +1 -0
  51. package/dist/utils/guided-eval.js +880 -0
  52. package/dist/utils/guided-eval.js.map +1 -0
  53. package/dist/utils/local-repo-brain.d.ts +158 -0
  54. package/dist/utils/local-repo-brain.d.ts.map +1 -0
  55. package/dist/utils/local-repo-brain.js +854 -0
  56. package/dist/utils/local-repo-brain.js.map +1 -0
  57. package/dist/utils/runtime-live.d.ts +25 -0
  58. package/dist/utils/runtime-live.d.ts.map +1 -1
  59. package/dist/utils/runtime-live.js +103 -4
  60. package/dist/utils/runtime-live.js.map +1 -1
  61. package/dist/utils/runtime-outbox.d.ts +2 -1
  62. package/dist/utils/runtime-outbox.d.ts.map +1 -1
  63. package/dist/utils/runtime-outbox.js +21 -16
  64. package/dist/utils/runtime-outbox.js.map +1 -1
  65. package/dist/utils/session-allowlist-rules.d.ts +12 -0
  66. package/dist/utils/session-allowlist-rules.d.ts.map +1 -1
  67. package/dist/utils/session-allowlist-rules.js +61 -1
  68. package/dist/utils/session-allowlist-rules.js.map +1 -1
  69. package/dist/utils/structural-understanding.d.ts +61 -1
  70. package/dist/utils/structural-understanding.d.ts.map +1 -1
  71. package/dist/utils/structural-understanding.js +534 -1
  72. package/dist/utils/structural-understanding.js.map +1 -1
  73. package/dist/utils/v0-governance.d.ts.map +1 -1
  74. package/dist/utils/v0-governance.js +10 -0
  75. package/dist/utils/v0-governance.js.map +1 -1
  76. 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
- if (!nudge)
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, nudge.nudgeKey))
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.headline,
248
+ message: (nudge ?? reuseNudge)?.headline ?? 'Structural advisory recorded.',
208
249
  detail: {
209
- nudgeVersion: nudge.nudgeVersion,
210
- nudgeKey: nudge.nudgeKey,
211
- severity: nudge.severity,
212
- consequenceClass: nudge.consequenceClass,
213
- operatorAction: nudge.operatorAction,
214
- reviewFocus: nudge.reviewFocus,
215
- artifactHash: nudge.artifactHash,
216
- impact: nudge.impact ? {
217
- rank: nudge.impact.rank,
218
- score: nudge.impact.score,
219
- file: nudge.impact.file,
220
- symbol: nudge.impact.symbol,
221
- summary: nudge.impact.summary,
222
- findingTypes: nudge.impact.findingTypes,
223
- findingRanks: nudge.impact.findingRanks,
224
- findingCount: nudge.impact.findingCount,
225
- productionConsumerCount: nudge.impact.productionConsumerCount,
226
- testConsumerCount: nudge.impact.testConsumerCount,
227
- reachableProductionConsumerCount: nudge.impact.reachableProductionConsumerCount,
228
- externalProductionConsumerCount: nudge.impact.externalProductionConsumerCount,
229
- changedProductionConsumerCount: nudge.impact.changedProductionConsumerCount,
230
- sensitiveConsumerCount: nudge.impact.sensitiveConsumerCount,
231
- approvalRequiredConsumerCount: nudge.impact.approvalRequiredConsumerCount,
232
- runtimeGovernanceConsumerCount: nudge.impact.runtimeGovernanceConsumerCount,
233
- productionFiles: nudge.impact.productionFiles,
234
- changedProductionFiles: nudge.impact.changedProductionFiles,
235
- testFiles: nudge.impact.testFiles,
236
- sensitiveFiles: nudge.impact.sensitiveFiles,
237
- approvalRequiredFiles: nudge.impact.approvalRequiredFiles,
238
- runtimeGovernanceFiles: nudge.impact.runtimeGovernanceFiles,
239
- highFanout: nudge.impact.highFanout,
240
- architectureRelevant: nudge.impact.architectureRelevant,
241
- reasonCodes: nudge.impact.reasonCodes,
242
- provenance: nudge.impact.provenance,
243
- } : null,
244
- finding: {
245
- rank: nudge.finding.rank,
246
- score: nudge.finding.score,
247
- findingType: nudge.finding.findingType,
248
- file: nudge.finding.file,
249
- symbol: nudge.finding.symbol,
250
- summary: nudge.finding.summary,
251
- consumerCount: nudge.finding.consumerCount,
252
- nonTestConsumerCount: nudge.finding.nonTestConsumerCount,
253
- testConsumerCount: nudge.finding.testConsumerCount,
254
- externalConsumerCount: nudge.finding.externalConsumerCount,
255
- externalConsumerFiles: nudge.finding.externalConsumerFiles,
256
- consumerSummary: nudge.finding.consumerSummary,
257
- reasonCodes: nudge.finding.reasonCodes,
258
- },
259
- topImpacts: nudges
260
- .map((item) => item.impact)
261
- .filter((impact) => Boolean(impact))
262
- .map((impact) => ({
263
- rank: impact.rank,
264
- score: impact.score,
265
- file: impact.file,
266
- symbol: impact.symbol,
267
- summary: impact.summary,
268
- findingTypes: impact.findingTypes,
269
- findingCount: impact.findingCount,
270
- reachableProductionConsumerCount: impact.reachableProductionConsumerCount,
271
- externalProductionConsumerCount: impact.externalProductionConsumerCount,
272
- changedProductionConsumerCount: impact.changedProductionConsumerCount,
273
- productionFiles: impact.productionFiles,
274
- changedProductionFiles: impact.changedProductionFiles,
275
- sensitiveConsumerCount: impact.sensitiveConsumerCount,
276
- approvalRequiredConsumerCount: impact.approvalRequiredConsumerCount,
277
- runtimeGovernanceConsumerCount: impact.runtimeGovernanceConsumerCount,
278
- highFanout: impact.highFanout,
279
- architectureRelevant: impact.architectureRelevant,
280
- reasonCodes: impact.reasonCodes,
281
- })),
282
- topFindings: nudges.map((item) => ({
283
- nudgeKey: item.nudgeKey,
284
- severity: item.severity,
285
- consequenceClass: item.consequenceClass,
286
- operatorAction: item.operatorAction,
287
- reviewFocus: item.reviewFocus,
288
- findingType: item.finding.findingType,
289
- file: item.finding.file,
290
- symbol: item.finding.symbol,
291
- externalConsumerCount: item.finding.externalConsumerCount,
292
- externalConsumerFiles: item.finding.externalConsumerFiles,
293
- consumerSummary: item.finding.consumerSummary,
294
- reasonCodes: item.finding.reasonCodes,
295
- })),
296
- provenance: nudge.provenance,
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 latestUnresolvedApprovalBlock(session) {
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 context = event.detail?.approvalContext;
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, blocking.result.approvalContext ? { approvalContext: blocking.result.approvalContext } : undefined);
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
- // No active session — not governed, pass through
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 (ungoverned)`
719
- : `no active session at ${repoRoot} — edit allowed (ungoverned)`);
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: { profileFreshness },
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, { profileFreshness });
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
- if (result.verdict === 'ok' && planCoherencePolicy.action === 'block') {
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} Re-plan or update the plan before editing this path. ` +
972
- `Use neurcode_session_replan or \`neurcode session replan --add-file ${filePath}\`.`,
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(result.message, result.approvalContext ? { approvalContext: result.approvalContext } : undefined);
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 pendingApproval = latestUnresolvedApprovalBlock(session);
1172
- if (shouldKeepSessionActiveForPendingApproval(session, pendingApproval)) {
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 exact approval of ` +
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, pendingApproval
1589
+ const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, pendingActionableBlock
1189
1590
  ? {
1190
- reason: 'finished_with_unresolved_approval_blocks',
1191
- unresolvedApprovalBlocks: [pendingApproval],
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 = pendingApproval
1203
- ? ` Unresolved: 1 approval block left recorded (${pendingApproval.suggestedApprovalPath})`
1613
+ const unresolvedLine = pendingActionableBlock
1614
+ ? ` Unresolved: 1 ${pendingActionableBlock.blockType} left recorded (${pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath})`
1204
1615
  : null;
1205
1616
  const summary = [
1206
- pendingApproval
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}`,