@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.
- 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/cursor.d.ts.map +1 -1
- package/dist/commands/cursor.js +72 -0
- package/dist/commands/cursor.js.map +1 -1
- 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.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 +322 -30
- 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 +212 -2
- package/dist/commands/session.js.map +1 -1
- package/dist/index.js +80 -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/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/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 +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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
719
|
-
: `no active session at ${repoRoot} — edit allowed
|
|
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: {
|
|
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, {
|
|
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
|
-
|
|
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}
|
|
972
|
-
`Use
|
|
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,
|
|
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
|
|
1172
|
-
if (shouldKeepSessionActiveForPendingApproval(session,
|
|
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
|
|
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,
|
|
1470
|
+
const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, pendingActionableBlock
|
|
1189
1471
|
? {
|
|
1190
|
-
reason: '
|
|
1191
|
-
|
|
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 =
|
|
1203
|
-
? ` Unresolved: 1
|
|
1494
|
+
const unresolvedLine = pendingActionableBlock
|
|
1495
|
+
? ` Unresolved: 1 ${pendingActionableBlock.blockType} left recorded (${pendingActionableBlock.suggestedApprovalPath || pendingActionableBlock.filePath})`
|
|
1204
1496
|
: null;
|
|
1205
1497
|
const summary = [
|
|
1206
|
-
|
|
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}`,
|