@neurcode-ai/cli 0.16.4 → 0.16.5

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 (48) hide show
  1. package/dist/api-client.d.ts +75 -0
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +43 -0
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/commands/cursor.d.ts.map +1 -1
  6. package/dist/commands/cursor.js +72 -0
  7. package/dist/commands/cursor.js.map +1 -1
  8. package/dist/commands/runtime-doctor.d.ts.map +1 -1
  9. package/dist/commands/runtime-doctor.js +80 -9
  10. package/dist/commands/runtime-doctor.js.map +1 -1
  11. package/dist/commands/runtime.d.ts +18 -0
  12. package/dist/commands/runtime.d.ts.map +1 -1
  13. package/dist/commands/runtime.js +321 -1
  14. package/dist/commands/runtime.js.map +1 -1
  15. package/dist/commands/session-hook.d.ts +10 -2
  16. package/dist/commands/session-hook.d.ts.map +1 -1
  17. package/dist/commands/session-hook.js +322 -30
  18. package/dist/commands/session-hook.js.map +1 -1
  19. package/dist/commands/session.d.ts +34 -0
  20. package/dist/commands/session.d.ts.map +1 -1
  21. package/dist/commands/session.js +212 -2
  22. package/dist/commands/session.js.map +1 -1
  23. package/dist/index.js +80 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/runtime-build.json +5 -5
  26. package/dist/utils/agent-guard-supervisor.d.ts.map +1 -1
  27. package/dist/utils/agent-guard-supervisor.js +0 -1
  28. package/dist/utils/agent-guard-supervisor.js.map +1 -1
  29. package/dist/utils/cursor-gate.d.ts +1 -0
  30. package/dist/utils/cursor-gate.d.ts.map +1 -1
  31. package/dist/utils/cursor-gate.js +34 -7
  32. package/dist/utils/cursor-gate.js.map +1 -1
  33. package/dist/utils/runtime-live.d.ts +25 -0
  34. package/dist/utils/runtime-live.d.ts.map +1 -1
  35. package/dist/utils/runtime-live.js +103 -4
  36. package/dist/utils/runtime-live.js.map +1 -1
  37. package/dist/utils/runtime-outbox.d.ts +2 -1
  38. package/dist/utils/runtime-outbox.d.ts.map +1 -1
  39. package/dist/utils/runtime-outbox.js +21 -16
  40. package/dist/utils/runtime-outbox.js.map +1 -1
  41. package/dist/utils/session-allowlist-rules.d.ts +12 -0
  42. package/dist/utils/session-allowlist-rules.d.ts.map +1 -1
  43. package/dist/utils/session-allowlist-rules.js +61 -1
  44. package/dist/utils/session-allowlist-rules.js.map +1 -1
  45. package/dist/utils/v0-governance.d.ts.map +1 -1
  46. package/dist/utils/v0-governance.js +10 -0
  47. package/dist/utils/v0-governance.js.map +1 -1
  48. package/package.json +1 -1
@@ -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");
@@ -351,15 +352,117 @@ function hookFilePathCandidates(hookInput) {
351
352
  }
352
353
  return Array.from(new Set(candidates));
353
354
  }
354
- function latestUnresolvedApprovalBlock(session) {
355
+ function runtimeMode(session) {
356
+ return session.contract.runtimeMode === 'strict' ||
357
+ session.contract.runtimeMode === 'paused' ||
358
+ session.contract.runtimeMode === 'advisory'
359
+ ? session.contract.runtimeMode
360
+ : 'strict';
361
+ }
362
+ function blockContext(input) {
363
+ const isApproval = input.blockType === 'approval_required_boundary';
364
+ const isScope = input.blockType === 'scope_violation_or_task_expansion';
365
+ return {
366
+ schemaVersion: 'neurcode.runtime-block.v1',
367
+ blockType: input.blockType,
368
+ filePath: input.filePath || null,
369
+ message: input.message || null,
370
+ runtimeMode: input.runtimeMode || null,
371
+ operatorActionKind: isApproval
372
+ ? 'exact_path_approval'
373
+ : isScope
374
+ ? 'scope_amendment'
375
+ : input.blockType === 'profile_or_runtime_health_block'
376
+ ? 'runtime_health_recovery'
377
+ : 'split_tool_call',
378
+ operatorActionLabel: isApproval
379
+ ? 'Approve exact path / Deny'
380
+ : isScope
381
+ ? 'Approve task expansion / Amend scope / Deny'
382
+ : input.blockType === 'profile_or_runtime_health_block'
383
+ ? 'Refresh or restart runtime'
384
+ : 'Split into one file per tool call',
385
+ suggestedApprovalPath: isApproval ? input.suggestedApprovalPath || input.filePath || null : null,
386
+ owners: input.owners || [],
387
+ proposalId: input.proposalId || null,
388
+ nextAction: input.nextAction || (isApproval
389
+ ? 'Approve only the exact path for this session, or deny the write.'
390
+ : isScope
391
+ ? 'Accept the pending scope amendment or re-plan locally, then retry the write.'
392
+ : input.blockType === 'profile_or_runtime_health_block'
393
+ ? 'Refresh the governance profile or restart the active governed session.'
394
+ : 'Retry as separate single-file edits so each path can be governed.'),
395
+ };
396
+ }
397
+ const NO_ACTIVE_SESSION_SCOPE_SENTINEL = '__neurcode_no_active_session_scope__';
398
+ function evaluateNoActiveSessionWrite(repoRoot, filePath) {
399
+ const profile = (0, v0_governance_1.ensureFreshGovernanceProfile)(repoRoot).profile;
400
+ const result = (0, governance_runtime_1.checkFileBoundary)({
401
+ filePath,
402
+ allowedGlobs: [NO_ACTIVE_SESSION_SCOPE_SENTINEL],
403
+ ownershipRules: profile.ownershipBoundaries,
404
+ sensitiveGlobs: profile.sensitiveBoundaries.map((boundary) => boundary.glob),
405
+ approvalRequiredGlobs: profile.approvalRequiredPaths,
406
+ approvedPaths: [],
407
+ approvalGrants: [],
408
+ scopeMode: 'explicit',
409
+ localMode: 'strict',
410
+ });
411
+ const protectedPath = result.isApprovalRequired || result.isSensitive || result.owners.length > 0;
412
+ const ownerNote = result.owners.length ? ` Owners: ${result.owners.join(', ')}.` : '';
413
+ const message = protectedPath
414
+ ? `⏸ 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.`
415
+ : `No active governed session at ${repoRoot}; ${filePath} is not a detected protected path and is allowed advisory-only.`;
416
+ return {
417
+ block: protectedPath,
418
+ filePath,
419
+ result,
420
+ message,
421
+ };
422
+ }
423
+ function blockTypeFromEvent(event) {
424
+ const detail = event.detail || {};
425
+ const context = detail.blockContext;
426
+ if (context && typeof context === 'object') {
427
+ const value = context.blockType;
428
+ if (value === 'approval_required_boundary' ||
429
+ value === 'scope_violation_or_task_expansion' ||
430
+ value === 'profile_or_runtime_health_block' ||
431
+ value === 'multi_file_or_tool_shape_block') {
432
+ return value;
433
+ }
434
+ }
435
+ if (detail.approvalContext)
436
+ return 'approval_required_boundary';
437
+ if (detail.profileFreshness)
438
+ return 'profile_or_runtime_health_block';
439
+ if (detail.reason === 'multi_file_tool_call_requires_split')
440
+ return 'multi_file_or_tool_shape_block';
441
+ return 'scope_violation_or_task_expansion';
442
+ }
443
+ function latestUnresolvedActionableBlock(session) {
355
444
  for (let i = session.events.length - 1; i >= 0; i -= 1) {
356
445
  const event = session.events[i];
446
+ if (event.type === 'check_ok' || event.type === 'check_warn' || event.type === 'plan_amended') {
447
+ return null;
448
+ }
357
449
  if (event.type !== 'check_block')
358
450
  continue;
359
- const context = event.detail?.approvalContext;
451
+ const detail = event.detail;
452
+ const context = detail?.approvalContext;
360
453
  const blockedPath = event.filePath || context?.blockedPath || context?.suggestedApprovalPath;
361
454
  if (!blockedPath)
362
455
  continue;
456
+ const blockType = blockTypeFromEvent(event);
457
+ if (blockType !== 'approval_required_boundary') {
458
+ return {
459
+ filePath: blockedPath,
460
+ blockType,
461
+ suggestedApprovalPath: detail?.blockContext?.suggestedApprovalPath || null,
462
+ proposalId: detail?.blockContext?.proposalId || null,
463
+ message: event.message || null,
464
+ };
465
+ }
363
466
  const verdict = (0, governance_runtime_1.checkFileBoundary)({
364
467
  filePath: blockedPath,
365
468
  allowedGlobs: session.contract.allowedGlobs,
@@ -369,11 +472,14 @@ function latestUnresolvedApprovalBlock(session) {
369
472
  approvedPaths: session.contract.approvedPaths,
370
473
  approvalGrants: session.contract.approvalGrants,
371
474
  scopeMode: session.contract.scopeMode,
475
+ localMode: runtimeMode(session),
372
476
  });
373
477
  if (verdict.verdict === 'block' && verdict.approvalContext) {
374
478
  return {
375
479
  filePath: blockedPath,
480
+ blockType: 'approval_required_boundary',
376
481
  suggestedApprovalPath: verdict.approvalContext.suggestedApprovalPath || context?.suggestedApprovalPath || blockedPath,
482
+ message: event.message || null,
377
483
  };
378
484
  }
379
485
  return null;
@@ -383,6 +489,9 @@ function latestUnresolvedApprovalBlock(session) {
383
489
  function shouldKeepSessionActiveForPendingApproval(session, pendingApproval) {
384
490
  if (!pendingApproval)
385
491
  return false;
492
+ if (pendingApproval.blockType && pendingApproval.blockType !== 'approval_required_boundary') {
493
+ return true;
494
+ }
386
495
  const hasRecordedApproval = session.contract.approvedPaths.length > 0 ||
387
496
  (session.contract.approvalGrants ?? []).some((grant) => !grant.revokedAt) ||
388
497
  session.events.some((event) => event.type === 'approval_decision' && event.decision === 'approved');
@@ -397,6 +506,7 @@ async function recordBashCheck(repoRoot, session, args) {
397
506
  message: args.message,
398
507
  detail: {
399
508
  ...(args.approvalContext ? { approvalContext: args.approvalContext } : {}),
509
+ ...(args.blockContext ? { blockContext: args.blockContext } : {}),
400
510
  toolName: 'Bash',
401
511
  bash: {
402
512
  operation: args.operation,
@@ -445,6 +555,7 @@ async function handleBashCheck(repoRoot, session, command) {
445
555
  approvedPaths: session.contract.approvedPaths,
446
556
  approvalGrants: session.contract.approvalGrants,
447
557
  scopeMode: session.contract.scopeMode,
558
+ localMode: runtimeMode(session),
448
559
  }),
449
560
  }));
450
561
  for (const { filePath, result } of results) {
@@ -457,13 +568,37 @@ async function handleBashCheck(repoRoot, session, command) {
457
568
  commandFingerprint: analysis.commandFingerprint,
458
569
  boundaryVerdict: result.verdict,
459
570
  approvalContext: result.approvalContext,
571
+ blockContext: result.blockType
572
+ ? blockContext({
573
+ blockType: result.blockType,
574
+ filePath,
575
+ message: result.message,
576
+ suggestedApprovalPath: result.approvalContext?.suggestedApprovalPath,
577
+ owners: result.owners,
578
+ runtimeMode: runtimeMode(session),
579
+ })
580
+ : undefined,
460
581
  });
461
582
  }
462
583
  const blocking = results.find(({ result }) => result.verdict === 'block');
463
584
  if (blocking) {
464
585
  const message = `⏸ Neurcode: Bash ${analysis.operation} targets ${blocking.filePath}. ` +
465
586
  blocking.result.message.replace(/^⏸ Neurcode:\s*/, '');
466
- denyPreToolUse(message, blocking.result.approvalContext ? { approvalContext: blocking.result.approvalContext } : undefined);
587
+ denyPreToolUse(message, {
588
+ ...(blocking.result.approvalContext ? { approvalContext: blocking.result.approvalContext } : {}),
589
+ ...(blocking.result.blockType
590
+ ? {
591
+ blockContext: blockContext({
592
+ blockType: blocking.result.blockType,
593
+ filePath: blocking.filePath,
594
+ message,
595
+ suggestedApprovalPath: blocking.result.approvalContext?.suggestedApprovalPath,
596
+ owners: blocking.result.owners,
597
+ runtimeMode: runtimeMode(session),
598
+ }),
599
+ }
600
+ : {}),
601
+ });
467
602
  }
468
603
  const warning = results.find(({ result }) => result.verdict === 'warn');
469
604
  if (warning) {
@@ -710,13 +845,53 @@ async function handleCheck(cmdCwd) {
710
845
  const repoRoot = (0, v0_governance_1.resolveRepoRoot)(effectiveCwd);
711
846
  (0, hook_heartbeat_1.recordHookHeartbeat)({ repoRoot, eventType: 'check' });
712
847
  const requestedSessionId = sessionIdFromHookInput(hookInput);
848
+ const toolName = hookInput['tool_name'] ||
849
+ hookInput['toolName'] ||
850
+ '';
851
+ const toolInput = hookInput['tool_input'] ??
852
+ hookInput['toolInput'] ??
853
+ {};
713
854
  const resolution = resolveSessionForHook(repoRoot, requestedSessionId);
714
855
  const activeSession = resolution.session;
715
856
  if (!activeSession) {
716
- // No active session — not governed, pass through
857
+ const rawPaths = hookFilePathCandidates(hookInput);
858
+ const bashLike = /^(bash|shell|runCommand|run_command|runInTerminal|run_in_terminal|terminal)$/i.test(toolName);
859
+ const bashAnalysis = bashLike
860
+ ? (0, bash_command_analysis_1.analyzeBashCommand)(toolInput['command'] ||
861
+ toolInput['cmd'] ||
862
+ hookInput['command'] ||
863
+ '')
864
+ : null;
865
+ const candidatePaths = bashAnalysis?.mutates
866
+ ? bashAnalysis.targetPaths
867
+ : rawPaths;
868
+ const normalizedPaths = Array.from(new Set(candidatePaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot))));
869
+ for (const filePath of normalizedPaths) {
870
+ try {
871
+ const decision = evaluateNoActiveSessionWrite(repoRoot, filePath);
872
+ if (decision.block) {
873
+ denyPreToolUse(decision.message, {
874
+ ...(decision.result.approvalContext ? { approvalContext: decision.result.approvalContext } : {}),
875
+ blockContext: blockContext({
876
+ blockType: 'profile_or_runtime_health_block',
877
+ filePath,
878
+ message: decision.message,
879
+ suggestedApprovalPath: decision.result.approvalContext?.suggestedApprovalPath,
880
+ owners: decision.result.owners,
881
+ runtimeMode: 'strict',
882
+ nextAction: 'Start or resume a governed Neurcode session, then retry this protected path.',
883
+ }),
884
+ });
885
+ }
886
+ }
887
+ catch (error) {
888
+ diagnostic(`no-active-session protected-path check skipped: ${error instanceof Error ? error.message : String(error)}`);
889
+ }
890
+ }
891
+ const targetNote = normalizedPaths.length > 0 ? ` for ${normalizedPaths.join(', ')}` : '';
717
892
  diagnostic(requestedSessionId
718
- ? `no active session ${requestedSessionId} at ${repoRoot} — edit allowed (ungoverned)`
719
- : `no active session at ${repoRoot} — edit allowed (ungoverned)`);
893
+ ? `no active session ${requestedSessionId} at ${repoRoot} — edit allowed advisory-only${targetNote}`
894
+ : `no active session at ${repoRoot} — edit allowed advisory-only${targetNote}`);
720
895
  process.exit(0);
721
896
  return;
722
897
  }
@@ -728,7 +903,7 @@ async function handleCheck(cmdCwd) {
728
903
  const hasPriorBlock = session.events.some((event) => event.type === 'check_block');
729
904
  if (hasPriorBlock) {
730
905
  const pending = await (0, runtime_live_1.applyPendingRuntimeLiveApprovals)(repoRoot, session.sessionId);
731
- if (pending.applied > 0 || pending.revoked > 0) {
906
+ if (pending.applied > 0 || pending.revoked > 0 || pending.scopeAmended > 0 || pending.scopeDenied > 0) {
732
907
  const refreshed = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
733
908
  if (refreshed)
734
909
  session = refreshed;
@@ -739,6 +914,12 @@ async function handleCheck(cmdCwd) {
739
914
  if (pending.revoked > 0) {
740
915
  diagnostic(`revoked ${pending.revoked} dashboard approval${pending.revoked === 1 ? '' : 's'}`);
741
916
  }
917
+ if (pending.scopeAmended > 0) {
918
+ diagnostic(`applied ${pending.scopeAmended} dashboard scope amendment${pending.scopeAmended === 1 ? '' : 's'}`);
919
+ }
920
+ if (pending.scopeDenied > 0) {
921
+ diagnostic(`recorded ${pending.scopeDenied} denied dashboard scope amendment${pending.scopeDenied === 1 ? '' : 's'}`);
922
+ }
742
923
  }
743
924
  }
744
925
  catch {
@@ -776,12 +957,6 @@ async function handleCheck(cmdCwd) {
776
957
  // ── Extract the target file path ─────────────────────────────────────────
777
958
  // Claude Code PreToolUse payload shape:
778
959
  // { 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
960
  if (/^(bash|shell|runCommand|run_command|runInTerminal|run_in_terminal|terminal)$/i.test(toolName)) {
786
961
  const command = toolInput['command'] ||
787
962
  toolInput['cmd'] ||
@@ -802,6 +977,12 @@ async function handleCheck(cmdCwd) {
802
977
  verdict: 'block',
803
978
  message,
804
979
  detail: {
980
+ blockContext: blockContext({
981
+ blockType: 'multi_file_or_tool_shape_block',
982
+ filePath: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)).join(','),
983
+ message,
984
+ runtimeMode: runtimeMode(session),
985
+ }),
805
986
  reason: 'multi_file_tool_call_requires_split',
806
987
  paths: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)),
807
988
  toolName,
@@ -815,6 +996,12 @@ async function handleCheck(cmdCwd) {
815
996
  // Recording failure must not weaken the deny.
816
997
  }
817
998
  denyPreToolUse(message, {
999
+ blockContext: blockContext({
1000
+ blockType: 'multi_file_or_tool_shape_block',
1001
+ filePath: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)).join(','),
1002
+ message,
1003
+ runtimeMode: runtimeMode(session),
1004
+ }),
818
1005
  reason: 'multi_file_tool_call_requires_split',
819
1006
  paths: rawPaths.map((path) => normalizeHookFilePathForRepo(path, repoRoot)),
820
1007
  });
@@ -864,7 +1051,15 @@ async function handleCheck(cmdCwd) {
864
1051
  filePath,
865
1052
  verdict: 'block',
866
1053
  message,
867
- detail: { profileFreshness },
1054
+ detail: {
1055
+ profileFreshness,
1056
+ blockContext: blockContext({
1057
+ blockType: 'profile_or_runtime_health_block',
1058
+ filePath,
1059
+ message,
1060
+ runtimeMode: runtimeMode(session),
1061
+ }),
1062
+ },
868
1063
  });
869
1064
  const refreshedSession = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
870
1065
  if (refreshedSession) {
@@ -874,7 +1069,15 @@ async function handleCheck(cmdCwd) {
874
1069
  catch {
875
1070
  // Recording failure must not weaken the deny.
876
1071
  }
877
- denyPreToolUse(message, { profileFreshness });
1072
+ denyPreToolUse(message, {
1073
+ profileFreshness,
1074
+ blockContext: blockContext({
1075
+ blockType: 'profile_or_runtime_health_block',
1076
+ filePath,
1077
+ message,
1078
+ runtimeMode: runtimeMode(session),
1079
+ }),
1080
+ });
878
1081
  }
879
1082
  if (staleness.status !== 'fresh') {
880
1083
  const refreshedProfile = (0, v0_governance_1.ensureFreshGovernanceProfile)(repoRoot);
@@ -899,6 +1102,12 @@ async function handleCheck(cmdCwd) {
899
1102
  verdict: 'block',
900
1103
  message,
901
1104
  detail: {
1105
+ blockContext: blockContext({
1106
+ blockType: 'profile_or_runtime_health_block',
1107
+ filePath,
1108
+ message,
1109
+ runtimeMode: runtimeMode(session),
1110
+ }),
902
1111
  profileFreshness: {
903
1112
  status: 'unreadable',
904
1113
  refreshed: false,
@@ -919,7 +1128,14 @@ async function handleCheck(cmdCwd) {
919
1128
  catch {
920
1129
  // Recording failure must not weaken the deny.
921
1130
  }
922
- denyPreToolUse(message);
1131
+ denyPreToolUse(message, {
1132
+ blockContext: blockContext({
1133
+ blockType: 'profile_or_runtime_health_block',
1134
+ filePath,
1135
+ message,
1136
+ runtimeMode: runtimeMode(session),
1137
+ }),
1138
+ });
923
1139
  }
924
1140
  // ── Run the boundary + intent-coherence checks ───────────────────────────
925
1141
  let result;
@@ -933,6 +1149,7 @@ async function handleCheck(cmdCwd) {
933
1149
  approvedPaths: session.contract.approvedPaths,
934
1150
  approvalGrants: session.contract.approvalGrants,
935
1151
  scopeMode: session.contract.scopeMode,
1152
+ localMode: runtimeMode(session),
936
1153
  });
937
1154
  }
938
1155
  catch (err) {
@@ -963,19 +1180,50 @@ async function handleCheck(cmdCwd) {
963
1180
  graph: session.contract.architectureGraph,
964
1181
  obligations: session.contract.architectureObligations ?? [],
965
1182
  });
966
- if (result.verdict === 'ok' && planCoherencePolicy.action === 'block') {
1183
+ let pendingScopeAmendmentProposalId = null;
1184
+ if (result.verdict === 'ok' && planCoherencePolicy.action === 'block' && runtimeMode(session) !== 'strict') {
1185
+ result = {
1186
+ ...result,
1187
+ verdict: 'warn',
1188
+ blockType: 'scope_violation_or_task_expansion',
1189
+ message: `⚠️ Neurcode: ${filePath} is not justified by the agent's stated plan. ` +
1190
+ `${planCoherencePolicy.reason} Proceeding in ${runtimeMode(session)} mode — recorded as task expansion evidence. ` +
1191
+ `Re-plan with neurcode_session_replan if this path should become part of the task.`,
1192
+ };
1193
+ }
1194
+ else if (result.verdict === 'ok' && planCoherencePolicy.action === 'block') {
1195
+ try {
1196
+ const amendment = (0, governance_runtime_1.amendAgentPlan)(repoRoot, {
1197
+ sessionId: session.sessionId,
1198
+ addExpectedFiles: [filePath],
1199
+ addSteps: [`Expand governed task scope to include ${filePath}`],
1200
+ reason: `scope expansion requested for ${filePath}`,
1201
+ source: 'unknown',
1202
+ proposedBy: 'agent',
1203
+ amendedAt: new Date().toISOString(),
1204
+ });
1205
+ pendingScopeAmendmentProposalId = amendment.proposal?.proposalId || amendment.eventId || null;
1206
+ const amendedSession = (0, governance_runtime_1.loadSession)(repoRoot, session.sessionId);
1207
+ if (amendedSession)
1208
+ session = amendedSession;
1209
+ }
1210
+ catch (error) {
1211
+ diagnostic(`scope amendment proposal could not be recorded: ${error instanceof Error ? error.message : String(error)}`);
1212
+ }
967
1213
  result = {
968
1214
  ...result,
969
1215
  verdict: 'block',
1216
+ blockType: 'scope_violation_or_task_expansion',
970
1217
  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}\`.`,
1218
+ `${planCoherencePolicy.reason} Approve task expansion / amend scope, then retry this path. ` +
1219
+ `Use neurcode_session_replan_decide${pendingScopeAmendmentProposalId ? ` for ${pendingScopeAmendmentProposalId}` : ''} or \`neurcode session replan --add-file ${filePath}\`.`,
973
1220
  };
974
1221
  }
975
1222
  else if (result.verdict === 'ok' && architectureObligationFeedback.action === 'block') {
976
1223
  result = {
977
1224
  ...result,
978
1225
  verdict: 'block',
1226
+ blockType: 'scope_violation_or_task_expansion',
979
1227
  message: `⏸ Neurcode: ${filePath} is blocked by ${architectureObligationFeedback.blocking.length} ` +
980
1228
  `architecture obligation${architectureObligationFeedback.blocking.length === 1 ? '' : 's'}. ` +
981
1229
  `${architectureObligationFeedback.reasons[0]} Satisfy the obligation, re-plan, or ask the human to waive it with ` +
@@ -1024,6 +1272,19 @@ async function handleCheck(cmdCwd) {
1024
1272
  message: result.message,
1025
1273
  detail: {
1026
1274
  ...(result.approvalContext ? { approvalContext: result.approvalContext } : {}),
1275
+ ...(result.blockType
1276
+ ? {
1277
+ blockContext: blockContext({
1278
+ blockType: result.blockType,
1279
+ filePath,
1280
+ message: result.message,
1281
+ suggestedApprovalPath: result.approvalContext?.suggestedApprovalPath,
1282
+ owners: result.owners,
1283
+ proposalId: pendingScopeAmendmentProposalId,
1284
+ runtimeMode: runtimeMode(session),
1285
+ }),
1286
+ }
1287
+ : {}),
1027
1288
  intentCoherence,
1028
1289
  planCoherence,
1029
1290
  planCoherencePolicy,
@@ -1056,7 +1317,22 @@ async function handleCheck(cmdCwd) {
1056
1317
  if (result.verdict === 'block') {
1057
1318
  // Include machine-readable approvalContext when the block is approval-required,
1058
1319
  // so the agent can surface a structured approval request to the human.
1059
- denyPreToolUse(result.message, result.approvalContext ? { approvalContext: result.approvalContext } : undefined);
1320
+ denyPreToolUse(result.message, {
1321
+ ...(result.approvalContext ? { approvalContext: result.approvalContext } : {}),
1322
+ ...(result.blockType
1323
+ ? {
1324
+ blockContext: blockContext({
1325
+ blockType: result.blockType,
1326
+ filePath,
1327
+ message: result.message,
1328
+ suggestedApprovalPath: result.approvalContext?.suggestedApprovalPath,
1329
+ owners: result.owners,
1330
+ proposalId: pendingScopeAmendmentProposalId,
1331
+ runtimeMode: runtimeMode(session),
1332
+ }),
1333
+ }
1334
+ : {}),
1335
+ });
1060
1336
  }
1061
1337
  if (result.verdict === 'warn') {
1062
1338
  const reason = consequenceNudge
@@ -1168,11 +1444,17 @@ async function handleFinish(cmdCwd) {
1168
1444
  diagnostic(`Claude session_id ${requestedSessionId} did not match a Neurcode session; finishing active session ${session.sessionId}`);
1169
1445
  }
1170
1446
  try {
1171
- const pendingApproval = latestUnresolvedApprovalBlock(session);
1172
- if (shouldKeepSessionActiveForPendingApproval(session, pendingApproval)) {
1447
+ const pendingActionableBlock = latestUnresolvedActionableBlock(session);
1448
+ if (shouldKeepSessionActiveForPendingApproval(session, pendingActionableBlock)) {
1449
+ const actionLabel = pendingActionableBlock.blockType === 'approval_required_boundary'
1450
+ ? `exact approval of ${pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath}`
1451
+ : pendingActionableBlock.blockType === 'scope_violation_or_task_expansion'
1452
+ ? `scope amendment for ${pendingActionableBlock.filePath}`
1453
+ : pendingActionableBlock.blockType === 'profile_or_runtime_health_block'
1454
+ ? 'runtime/profile recovery'
1455
+ : 'a split single-file retry';
1173
1456
  process.stdout.write(JSON.stringify({
1174
- message: `⏸ Neurcode session ${session.sessionId} remains active; waiting for exact approval of ` +
1175
- `${pendingApproval.suggestedApprovalPath}.`,
1457
+ message: `⏸ Neurcode session ${session.sessionId} remains active; waiting for operator action: ${actionLabel}.`,
1176
1458
  }) + '\n');
1177
1459
  await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, session);
1178
1460
  try {
@@ -1185,10 +1467,20 @@ async function handleFinish(cmdCwd) {
1185
1467
  }
1186
1468
  return;
1187
1469
  }
1188
- const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, pendingApproval
1470
+ const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, pendingActionableBlock
1189
1471
  ? {
1190
- reason: 'finished_with_unresolved_approval_blocks',
1191
- unresolvedApprovalBlocks: [pendingApproval],
1472
+ reason: pendingActionableBlock.blockType === 'approval_required_boundary'
1473
+ ? 'finished_with_unresolved_approval_blocks'
1474
+ : 'finished_with_unresolved_actionable_blocks',
1475
+ unresolvedActionableBlocks: [pendingActionableBlock],
1476
+ ...(pendingActionableBlock.blockType === 'approval_required_boundary'
1477
+ ? {
1478
+ unresolvedApprovalBlocks: [{
1479
+ filePath: pendingActionableBlock.filePath,
1480
+ suggestedApprovalPath: pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath,
1481
+ }],
1482
+ }
1483
+ : {}),
1192
1484
  }
1193
1485
  : undefined);
1194
1486
  if (!finished)
@@ -1199,11 +1491,11 @@ async function handleFinish(cmdCwd) {
1199
1491
  }
1200
1492
  const blockCount = finished.events.filter((e) => e.type === 'check_block').length;
1201
1493
  const warnCount = finished.events.filter((e) => e.type === 'check_warn').length;
1202
- const unresolvedLine = pendingApproval
1203
- ? ` Unresolved: 1 approval block left recorded (${pendingApproval.suggestedApprovalPath})`
1494
+ const unresolvedLine = pendingActionableBlock
1495
+ ? ` Unresolved: 1 ${pendingActionableBlock.blockType} left recorded (${pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath})`
1204
1496
  : null;
1205
1497
  const summary = [
1206
- pendingApproval
1498
+ pendingActionableBlock
1207
1499
  ? `✅ Neurcode session ${finished.sessionId} complete with unresolved block evidence`
1208
1500
  : `✅ Neurcode session ${finished.sessionId} complete`,
1209
1501
  ` Scope mode: ${finished.contract.scopeMode}`,