@neurcode-ai/cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +451 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +296 -0
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/runtime-adapter.d.ts +2 -1
- package/dist/commands/runtime-adapter.d.ts.map +1 -1
- package/dist/commands/runtime-adapter.js +51 -2
- package/dist/commands/runtime-adapter.js.map +1 -1
- package/dist/commands/session-hook.d.ts +17 -0
- package/dist/commands/session-hook.d.ts.map +1 -1
- package/dist/commands/session-hook.js +279 -14
- package/dist/commands/session-hook.js.map +1 -1
- package/dist/runtime-build.json +4 -4
- package/dist/utils/agent-adapter-setup.js +1 -1
- package/dist/utils/agent-adapter-setup.js.map +1 -1
- package/dist/utils/agent-guard.d.ts +1 -0
- package/dist/utils/agent-guard.d.ts.map +1 -1
- package/dist/utils/agent-guard.js +18 -6
- package/dist/utils/agent-guard.js.map +1 -1
- package/dist/utils/git-coverage.d.ts.map +1 -1
- package/dist/utils/git-coverage.js +1 -0
- package/dist/utils/git-coverage.js.map +1 -1
- package/dist/utils/local-repo-brain.d.ts +85 -0
- package/dist/utils/local-repo-brain.d.ts.map +1 -1
- package/dist/utils/local-repo-brain.js +259 -4
- package/dist/utils/local-repo-brain.js.map +1 -1
- package/dist/utils/proposed-change-analysis.d.ts +20 -0
- package/dist/utils/proposed-change-analysis.d.ts.map +1 -0
- package/dist/utils/proposed-change-analysis.js +448 -0
- package/dist/utils/proposed-change-analysis.js.map +1 -0
- package/dist/utils/repo-intelligence-v2.d.ts +28 -0
- package/dist/utils/repo-intelligence-v2.d.ts.map +1 -0
- package/dist/utils/repo-intelligence-v2.js +174 -0
- package/dist/utils/repo-intelligence-v2.js.map +1 -0
- package/dist/utils/v0-governance.d.ts +1 -1
- package/dist/utils/v0-governance.d.ts.map +1 -1
- package/dist/utils/v0-governance.js +86 -15
- package/dist/utils/v0-governance.js.map +1 -1
- package/package.json +12 -11
- package/LICENSE +0 -201
|
@@ -22,10 +22,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.resolveSessionForHook = resolveSessionForHook;
|
|
23
23
|
exports.normalizeHookFilePathForRepo = normalizeHookFilePathForRepo;
|
|
24
24
|
exports.hookFilePathCandidates = hookFilePathCandidates;
|
|
25
|
+
exports.proposedSourceFromHookInput = proposedSourceFromHookInput;
|
|
25
26
|
exports.evaluateNoActiveSessionWrite = evaluateNoActiveSessionWrite;
|
|
26
27
|
exports.shouldKeepSessionActiveForPendingApproval = shouldKeepSessionActiveForPendingApproval;
|
|
28
|
+
exports.reconcileTrustedAdapterPosture = reconcileTrustedAdapterPosture;
|
|
27
29
|
exports.sessionHookCommand = sessionHookCommand;
|
|
28
30
|
const child_process_1 = require("child_process");
|
|
31
|
+
const crypto_1 = require("crypto");
|
|
29
32
|
const fs_1 = require("fs");
|
|
30
33
|
const path_1 = require("path");
|
|
31
34
|
const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
|
|
@@ -43,6 +46,8 @@ const structural_understanding_1 = require("../utils/structural-understanding");
|
|
|
43
46
|
const consequence_nudges_1 = require("../utils/consequence-nudges");
|
|
44
47
|
const agent_guard_supervisor_1 = require("../utils/agent-guard-supervisor");
|
|
45
48
|
const local_repo_brain_1 = require("../utils/local-repo-brain");
|
|
49
|
+
const proposed_change_analysis_1 = require("../utils/proposed-change-analysis");
|
|
50
|
+
const repo_intelligence_v2_1 = require("../utils/repo-intelligence-v2");
|
|
46
51
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
47
52
|
/** Read the full hook JSON from stdin, or return {} on any error. */
|
|
48
53
|
function readHookInput() {
|
|
@@ -409,6 +414,36 @@ function hookFilePathCandidates(hookInput) {
|
|
|
409
414
|
}
|
|
410
415
|
return Array.from(new Set(candidates));
|
|
411
416
|
}
|
|
417
|
+
function stringField(record, keys) {
|
|
418
|
+
for (const key of keys) {
|
|
419
|
+
const value = record[key];
|
|
420
|
+
if (typeof value === 'string' && value.trim())
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function proposedSourceFromHookInput(hookInput) {
|
|
426
|
+
const toolInput = hookInput['tool_input'] ??
|
|
427
|
+
hookInput['toolInput'] ??
|
|
428
|
+
{};
|
|
429
|
+
const direct = stringField(toolInput, ['content', 'file_content', 'fileContent', 'text']);
|
|
430
|
+
if (direct)
|
|
431
|
+
return { source: direct, sourceKind: 'write_content' };
|
|
432
|
+
const replacement = stringField(toolInput, ['new_string', 'newString', 'replacement', 'newText']);
|
|
433
|
+
if (replacement)
|
|
434
|
+
return { source: replacement, sourceKind: 'edit_new_string' };
|
|
435
|
+
const edits = toolInput['edits'] ?? toolInput['changes'];
|
|
436
|
+
if (Array.isArray(edits)) {
|
|
437
|
+
const parts = edits
|
|
438
|
+
.map((edit) => edit && typeof edit === 'object'
|
|
439
|
+
? stringField(edit, ['new_string', 'newString', 'replacement', 'newText'])
|
|
440
|
+
: null)
|
|
441
|
+
.filter((value) => Boolean(value));
|
|
442
|
+
if (parts.length > 0)
|
|
443
|
+
return { source: parts.join('\n'), sourceKind: 'multi_edit_new_strings' };
|
|
444
|
+
}
|
|
445
|
+
return { source: null, sourceKind: 'not_available' };
|
|
446
|
+
}
|
|
412
447
|
function runtimeMode(session) {
|
|
413
448
|
return session.contract.runtimeMode === 'strict' ||
|
|
414
449
|
session.contract.runtimeMode === 'paused' ||
|
|
@@ -416,9 +451,92 @@ function runtimeMode(session) {
|
|
|
416
451
|
? session.contract.runtimeMode
|
|
417
452
|
: 'strict';
|
|
418
453
|
}
|
|
454
|
+
function repoSymbolDuplicateMode(session) {
|
|
455
|
+
const mode = session.contract.repoSymbolDuplicateMode;
|
|
456
|
+
return mode === 'off' || mode === 'warn' || mode === 'block' ? mode : 'warn';
|
|
457
|
+
}
|
|
458
|
+
function repoSymbolPolicyMessage(policy) {
|
|
459
|
+
const first = policy.findings[0];
|
|
460
|
+
if (!first)
|
|
461
|
+
return policy.reason;
|
|
462
|
+
const modeSuffix = policy.verdict === 'block'
|
|
463
|
+
? 'Policy mode: block. Reuse or rename the symbol, or intentionally relax repoSymbolDuplicateMode.'
|
|
464
|
+
: 'Policy mode: warn. Review the existing symbol before continuing.';
|
|
465
|
+
return `${first.message} ${modeSuffix}`;
|
|
466
|
+
}
|
|
467
|
+
function classifyClarification(prompt) {
|
|
468
|
+
const compact = prompt
|
|
469
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
470
|
+
.split(/\r?\n/)
|
|
471
|
+
.filter((line) => {
|
|
472
|
+
const trimmed = line.trim();
|
|
473
|
+
if (/^(?:diff --git |index [0-9a-f]{6,}|@@ |--- |\+\+\+ |Binary files )/.test(trimmed))
|
|
474
|
+
return false;
|
|
475
|
+
return !/^[+-](?!\s)/.test(trimmed);
|
|
476
|
+
})
|
|
477
|
+
.join(' ')
|
|
478
|
+
.replace(/\s+/g, ' ')
|
|
479
|
+
.trim();
|
|
480
|
+
if (!compact)
|
|
481
|
+
return 'Human follow-up recorded without textual content.';
|
|
482
|
+
if (/^(?:yes|no|ok|okay|sure|continue|proceed|approved?|denied?|option\s+[a-z0-9]+|[a-z0-9]+)$/i.test(compact)) {
|
|
483
|
+
return 'Short human clarification referencing the active agent context.';
|
|
484
|
+
}
|
|
485
|
+
if (compact.length <= 80) {
|
|
486
|
+
return 'Brief human clarification recorded against the active agent proposal.';
|
|
487
|
+
}
|
|
488
|
+
return 'Detailed human clarification recorded against the active governed session.';
|
|
489
|
+
}
|
|
490
|
+
function acceptedProposalContext(session, rawPrompt) {
|
|
491
|
+
const pendingProposal = [...(session.contract.planAmendmentProposals ?? [])]
|
|
492
|
+
.reverse()
|
|
493
|
+
.find((proposal) => proposal.status === 'pending');
|
|
494
|
+
const activePlan = session.contract.agentPlan;
|
|
495
|
+
return {
|
|
496
|
+
schemaVersion: 'neurcode.intent-continuity-context.v1',
|
|
497
|
+
latestUserClarification: {
|
|
498
|
+
summary: classifyClarification(rawPrompt),
|
|
499
|
+
promptLength: rawPrompt.length,
|
|
500
|
+
promptHash: (0, crypto_1.createHash)('sha256').update(rawPrompt).digest('hex'),
|
|
501
|
+
recordedAt: new Date().toISOString(),
|
|
502
|
+
source: 'user_prompt_submit',
|
|
503
|
+
},
|
|
504
|
+
acceptedAgentProposal: activePlan
|
|
505
|
+
? {
|
|
506
|
+
activePlanRevision: (0, governance_runtime_1.activeAgentPlanRevision)(session.contract),
|
|
507
|
+
summary: activePlan.summary || null,
|
|
508
|
+
expectedFiles: activePlan.expectedFiles,
|
|
509
|
+
expectedGlobs: activePlan.expectedGlobs,
|
|
510
|
+
constraints: activePlan.constraints,
|
|
511
|
+
risks: activePlan.risks,
|
|
512
|
+
source: activePlan.source,
|
|
513
|
+
}
|
|
514
|
+
: null,
|
|
515
|
+
pendingPlanAmendment: pendingProposal
|
|
516
|
+
? {
|
|
517
|
+
proposalId: pendingProposal.proposalId,
|
|
518
|
+
previousRevision: pendingProposal.previousRevision,
|
|
519
|
+
proposedBy: pendingProposal.proposedBy,
|
|
520
|
+
reason: pendingProposal.reason,
|
|
521
|
+
riskLevel: pendingProposal.risk.level,
|
|
522
|
+
addedFiles: pendingProposal.risk.addedFiles,
|
|
523
|
+
addedGlobs: pendingProposal.risk.addedGlobs,
|
|
524
|
+
status: pendingProposal.status,
|
|
525
|
+
}
|
|
526
|
+
: null,
|
|
527
|
+
privacy: {
|
|
528
|
+
sourceUploaded: false,
|
|
529
|
+
sourceIncluded: false,
|
|
530
|
+
rawPromptStored: false,
|
|
531
|
+
summaryOnly: true,
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
419
535
|
function blockContext(input) {
|
|
420
536
|
const isApproval = input.blockType === 'approval_required_boundary';
|
|
421
537
|
const isScope = input.blockType === 'scope_violation_or_task_expansion';
|
|
538
|
+
const isRepoSymbolDuplicate = input.blockType === 'repo_symbol_duplicate_policy';
|
|
539
|
+
const isStructuralPolicy = input.blockType === 'structural_policy_violation';
|
|
422
540
|
return {
|
|
423
541
|
schemaVersion: 'neurcode.runtime-block.v1',
|
|
424
542
|
blockType: input.blockType,
|
|
@@ -429,16 +547,24 @@ function blockContext(input) {
|
|
|
429
547
|
? 'exact_path_approval'
|
|
430
548
|
: isScope
|
|
431
549
|
? 'scope_amendment'
|
|
432
|
-
:
|
|
433
|
-
? '
|
|
434
|
-
:
|
|
550
|
+
: isRepoSymbolDuplicate
|
|
551
|
+
? 'symbol_reuse_or_policy_decision'
|
|
552
|
+
: isStructuralPolicy
|
|
553
|
+
? 'structural_policy_remediation'
|
|
554
|
+
: input.blockType === 'profile_or_runtime_health_block'
|
|
555
|
+
? 'runtime_health_recovery'
|
|
556
|
+
: 'split_tool_call',
|
|
435
557
|
operatorActionLabel: isApproval
|
|
436
558
|
? 'Approve exact path / Deny'
|
|
437
559
|
: isScope
|
|
438
560
|
? 'Approve task expansion / Amend scope / Deny'
|
|
439
|
-
:
|
|
440
|
-
? '
|
|
441
|
-
:
|
|
561
|
+
: isRepoSymbolDuplicate
|
|
562
|
+
? 'Reuse existing symbol / Rename / Relax policy'
|
|
563
|
+
: isStructuralPolicy
|
|
564
|
+
? 'Remediate structural policy / Obtain required approval'
|
|
565
|
+
: input.blockType === 'profile_or_runtime_health_block'
|
|
566
|
+
? 'Refresh or restart runtime'
|
|
567
|
+
: 'Split into one file per tool call',
|
|
442
568
|
suggestedApprovalPath: isApproval ? input.suggestedApprovalPath || input.filePath || null : null,
|
|
443
569
|
owners: input.owners || [],
|
|
444
570
|
proposalId: input.proposalId || null,
|
|
@@ -446,9 +572,13 @@ function blockContext(input) {
|
|
|
446
572
|
? 'Approve only the exact path for this session, or deny the write.'
|
|
447
573
|
: isScope
|
|
448
574
|
? 'Accept the pending scope amendment or re-plan locally, then retry the write.'
|
|
449
|
-
:
|
|
450
|
-
? '
|
|
451
|
-
:
|
|
575
|
+
: isRepoSymbolDuplicate
|
|
576
|
+
? 'Reuse or rename the duplicate symbol, or change repoSymbolDuplicateMode with an explicit rationale.'
|
|
577
|
+
: isStructuralPolicy
|
|
578
|
+
? 'Review the matched deterministic facts and remediation, then retry after fixing the change or recording an authorized approval.'
|
|
579
|
+
: input.blockType === 'profile_or_runtime_health_block'
|
|
580
|
+
? 'Refresh the governance profile or restart the active governed session.'
|
|
581
|
+
: 'Retry as separate single-file edits so each path can be governed.'),
|
|
452
582
|
};
|
|
453
583
|
}
|
|
454
584
|
const NO_ACTIVE_SESSION_SCOPE_SENTINEL = '__neurcode_no_active_session_scope__';
|
|
@@ -485,7 +615,9 @@ function blockTypeFromEvent(event) {
|
|
|
485
615
|
if (value === 'approval_required_boundary' ||
|
|
486
616
|
value === 'scope_violation_or_task_expansion' ||
|
|
487
617
|
value === 'profile_or_runtime_health_block' ||
|
|
488
|
-
value === 'multi_file_or_tool_shape_block'
|
|
618
|
+
value === 'multi_file_or_tool_shape_block' ||
|
|
619
|
+
value === 'repo_symbol_duplicate_policy' ||
|
|
620
|
+
value === 'structural_policy_violation') {
|
|
489
621
|
return value;
|
|
490
622
|
}
|
|
491
623
|
}
|
|
@@ -546,6 +678,9 @@ function latestUnresolvedActionableBlock(session) {
|
|
|
546
678
|
function shouldKeepSessionActiveForPendingApproval(session, pendingApproval) {
|
|
547
679
|
if (!pendingApproval)
|
|
548
680
|
return false;
|
|
681
|
+
if (pendingApproval.blockType === 'profile_or_runtime_health_block') {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
549
684
|
if (pendingApproval.blockType && pendingApproval.blockType !== 'approval_required_boundary') {
|
|
550
685
|
return true;
|
|
551
686
|
}
|
|
@@ -747,6 +882,7 @@ async function maybeContinueActiveClaudeSession(repoRoot, rawPrompt, intentSelec
|
|
|
747
882
|
if (decision.action === 'start_new_session')
|
|
748
883
|
return null;
|
|
749
884
|
if (decision.action === 'record_operator_note') {
|
|
885
|
+
const continuityContext = acceptedProposalContext(activeSession, rawPrompt);
|
|
750
886
|
(0, governance_runtime_1.appendEvent)(repoRoot, activeSession.sessionId, {
|
|
751
887
|
type: 'user_decision',
|
|
752
888
|
ts: new Date().toISOString(),
|
|
@@ -754,6 +890,7 @@ async function maybeContinueActiveClaudeSession(repoRoot, rawPrompt, intentSelec
|
|
|
754
890
|
message: 'Human follow-up prompt recorded without changing the active governed plan.',
|
|
755
891
|
detail: {
|
|
756
892
|
intentContinuity: decision.detail,
|
|
893
|
+
continuityContext,
|
|
757
894
|
reason: decision.reason,
|
|
758
895
|
confidence: decision.confidence,
|
|
759
896
|
},
|
|
@@ -778,6 +915,7 @@ async function maybeContinueActiveClaudeSession(repoRoot, rawPrompt, intentSelec
|
|
|
778
915
|
: `Human follow-up prompt updated active plan revision ${amended.previousRevision} -> ${amended.revision}.`,
|
|
779
916
|
detail: {
|
|
780
917
|
intentContinuity: decision.detail,
|
|
918
|
+
continuityContext: acceptedProposalContext(session, rawPrompt),
|
|
781
919
|
reason: decision.reason,
|
|
782
920
|
confidence: decision.confidence,
|
|
783
921
|
planAmendment: {
|
|
@@ -895,8 +1033,25 @@ async function handleStart(cmdCwd) {
|
|
|
895
1033
|
// Fail open — don't break the agent turn
|
|
896
1034
|
}
|
|
897
1035
|
}
|
|
1036
|
+
const HARD_PREWRITE_ADAPTERS = new Set(['claude-code-hooks', 'copilot-hooks']);
|
|
1037
|
+
/**
|
|
1038
|
+
* Bind the attested host posture to the session's established launcher posture.
|
|
1039
|
+
* A governed session launched by a cooperative or observe-only agent can never
|
|
1040
|
+
* be re-labelled as host-enforced hard pre-write by a later check, even if the
|
|
1041
|
+
* check declares a hard adapter string. Changing adapters requires an explicit
|
|
1042
|
+
* re-handshake that re-launches the session. This is posture binding, not a
|
|
1043
|
+
* cryptographic host-attestation claim.
|
|
1044
|
+
*/
|
|
1045
|
+
function reconcileTrustedAdapterPosture(declared, launched) {
|
|
1046
|
+
if (!launched || declared === launched)
|
|
1047
|
+
return { adapterId: declared, downgraded: false };
|
|
1048
|
+
if (HARD_PREWRITE_ADAPTERS.has(declared) && !HARD_PREWRITE_ADAPTERS.has(launched)) {
|
|
1049
|
+
return { adapterId: launched, downgraded: true };
|
|
1050
|
+
}
|
|
1051
|
+
return { adapterId: declared, downgraded: false };
|
|
1052
|
+
}
|
|
898
1053
|
/** PreToolUse — check a pending Edit/Write/MultiEdit before it lands. */
|
|
899
|
-
async function handleCheck(cmdCwd) {
|
|
1054
|
+
async function handleCheck(cmdCwd, trustedAdapterId, trustedTiming) {
|
|
900
1055
|
const hookInput = readHookInput();
|
|
901
1056
|
const effectiveCwd = cwdFromHookInput(hookInput, cmdCwd);
|
|
902
1057
|
const repoRoot = (0, v0_governance_1.resolveRepoRoot)(effectiveCwd);
|
|
@@ -1312,6 +1467,88 @@ async function handleCheck(cmdCwd) {
|
|
|
1312
1467
|
`${planCoherencePolicy.reason} Proceeding — recorded in session.`,
|
|
1313
1468
|
};
|
|
1314
1469
|
}
|
|
1470
|
+
const launchedAdapter = (0, agent_session_launcher_1.latestAgentLauncherState)(session)?.agent.adapter;
|
|
1471
|
+
const adapterPosture = reconcileTrustedAdapterPosture(trustedAdapterId, launchedAdapter);
|
|
1472
|
+
if (adapterPosture.downgraded) {
|
|
1473
|
+
diagnostic(`trusted adapter ${trustedAdapterId} cannot host-enforce a session launched by ${launchedAdapter}; ` +
|
|
1474
|
+
`attesting ${adapterPosture.adapterId} posture. Re-handshake to change adapters.`);
|
|
1475
|
+
}
|
|
1476
|
+
const proposedSource = proposedSourceFromHookInput(hookInput);
|
|
1477
|
+
const proposedChange = (0, proposed_change_analysis_1.analyzeProposedChange)({
|
|
1478
|
+
repoRoot,
|
|
1479
|
+
filePath,
|
|
1480
|
+
proposedSource: proposedSource.source,
|
|
1481
|
+
sourceKind: proposedSource.sourceKind,
|
|
1482
|
+
adapterId: adapterPosture.adapterId,
|
|
1483
|
+
timing: trustedTiming,
|
|
1484
|
+
sessionId: session.sessionId,
|
|
1485
|
+
planRevision: (0, governance_runtime_1.activeAgentPlanRevision)(session.contract),
|
|
1486
|
+
proposedChange: hookInput['proposed_change'] ?? hookInput['proposedChange'],
|
|
1487
|
+
});
|
|
1488
|
+
let repoSymbolPolicy = (0, local_repo_brain_1.evaluateRepoSymbolDuplicatePolicy)({
|
|
1489
|
+
projectRoot: repoRoot,
|
|
1490
|
+
filePath,
|
|
1491
|
+
proposedSource: proposedSource.source,
|
|
1492
|
+
proposedSymbols: proposedChange.localSymbols,
|
|
1493
|
+
policyMode: repoSymbolDuplicateMode(session),
|
|
1494
|
+
});
|
|
1495
|
+
repoSymbolPolicy = {
|
|
1496
|
+
...repoSymbolPolicy,
|
|
1497
|
+
reason: repoSymbolPolicy.verdict === 'not_evaluated'
|
|
1498
|
+
? `${repoSymbolPolicy.reason} Source extraction: ${proposedSource.sourceKind}; ` +
|
|
1499
|
+
`content availability: ${proposedChange.envelope.content.availabilityReason}.`
|
|
1500
|
+
: repoSymbolPolicy.reason,
|
|
1501
|
+
};
|
|
1502
|
+
const repoIntelligence = await (0, repo_intelligence_v2_1.evaluateLocalRepoIntelligenceV2)({
|
|
1503
|
+
repoRoot,
|
|
1504
|
+
change: proposedChange.envelope,
|
|
1505
|
+
approvedPaths: session.contract.approvedPaths,
|
|
1506
|
+
approvalGrants: session.contract.approvalGrants,
|
|
1507
|
+
});
|
|
1508
|
+
if (result.verdict !== 'block' && repoSymbolPolicy.verdict === 'block') {
|
|
1509
|
+
result = {
|
|
1510
|
+
...result,
|
|
1511
|
+
verdict: 'block',
|
|
1512
|
+
blockType: 'repo_symbol_duplicate_policy',
|
|
1513
|
+
message: `⏸ Neurcode: ${repoSymbolPolicyMessage(repoSymbolPolicy)}`,
|
|
1514
|
+
options: ['narrow', 'replan'],
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
else if (result.verdict === 'ok' && repoSymbolPolicy.verdict === 'warn') {
|
|
1518
|
+
result = {
|
|
1519
|
+
...result,
|
|
1520
|
+
verdict: 'warn',
|
|
1521
|
+
blockType: 'repo_symbol_duplicate_policy',
|
|
1522
|
+
message: `⚠️ Neurcode: ${repoSymbolPolicyMessage(repoSymbolPolicy)}`,
|
|
1523
|
+
options: ['continue', 'replan'],
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
else if (result.verdict === 'warn' && repoSymbolPolicy.verdict === 'warn') {
|
|
1527
|
+
result = {
|
|
1528
|
+
...result,
|
|
1529
|
+
message: `${result.message} ${repoSymbolPolicyMessage(repoSymbolPolicy)}`,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
if (result.verdict !== 'block' && repoIntelligence.evaluation.verdict === 'block') {
|
|
1533
|
+
const first = repoIntelligence.evaluation.findings.find((finding) => finding.verdict === 'block');
|
|
1534
|
+
result = {
|
|
1535
|
+
...result,
|
|
1536
|
+
verdict: 'block',
|
|
1537
|
+
blockType: 'structural_policy_violation',
|
|
1538
|
+
message: `⏸ Neurcode: ${first?.explanation ?? 'A deterministic structural policy blocked this change.'} ${first?.remediation ?? ''}`.trim(),
|
|
1539
|
+
options: ['narrow', 'replan'],
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
else if (result.verdict === 'ok' && repoIntelligence.evaluation.verdict === 'warn') {
|
|
1543
|
+
const first = repoIntelligence.evaluation.findings[0];
|
|
1544
|
+
result = {
|
|
1545
|
+
...result,
|
|
1546
|
+
verdict: 'warn',
|
|
1547
|
+
blockType: 'structural_policy_violation',
|
|
1548
|
+
message: `⚠️ Neurcode: ${first?.explanation ?? 'A deterministic structural policy warned on this change.'} ${first?.remediation ?? ''}`.trim(),
|
|
1549
|
+
options: ['continue', 'replan'],
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1315
1552
|
// ── Record the event ─────────────────────────────────────────────────────
|
|
1316
1553
|
// Tag every check with the agent-plan revision that was active when it ran,
|
|
1317
1554
|
// so the evidence record can answer "which plan version governed this edit?".
|
|
@@ -1353,6 +1590,16 @@ async function handleCheck(cmdCwd) {
|
|
|
1353
1590
|
dependents: architectureEdit.dependents,
|
|
1354
1591
|
message: architectureEdit.message,
|
|
1355
1592
|
},
|
|
1593
|
+
repoSymbolPolicy,
|
|
1594
|
+
repoSymbolPolicySource: proposedSource.sourceKind,
|
|
1595
|
+
proposedChange: proposedChange.envelope,
|
|
1596
|
+
repoIntelligence: repoIntelligence.evidence,
|
|
1597
|
+
structuralPolicy: {
|
|
1598
|
+
configured: repoIntelligence.policyConfigured,
|
|
1599
|
+
schemaVersion: repoIntelligence.policyConfigured
|
|
1600
|
+
? 'neurcode.structural-policy-artifact.v2'
|
|
1601
|
+
: null,
|
|
1602
|
+
},
|
|
1356
1603
|
boundaryVerdict,
|
|
1357
1604
|
activePlanRevision,
|
|
1358
1605
|
planPresent: Boolean(session.contract.agentPlan),
|
|
@@ -1402,6 +1649,7 @@ async function handleCheck(cmdCwd) {
|
|
|
1402
1649
|
// Include machine-readable approvalContext when the block is approval-required,
|
|
1403
1650
|
// so the agent can surface a structured approval request to the human.
|
|
1404
1651
|
denyPreToolUse(enrichedMessage, {
|
|
1652
|
+
repoSymbolPolicy,
|
|
1405
1653
|
...(result.approvalContext ? { approvalContext: result.approvalContext } : {}),
|
|
1406
1654
|
...(result.blockType
|
|
1407
1655
|
? {
|
|
@@ -1440,6 +1688,7 @@ async function handleCheck(cmdCwd) {
|
|
|
1440
1688
|
hookEventName: 'PreToolUse',
|
|
1441
1689
|
permissionDecision: 'allow',
|
|
1442
1690
|
reason,
|
|
1691
|
+
repoSymbolPolicy,
|
|
1443
1692
|
},
|
|
1444
1693
|
}) + '\n');
|
|
1445
1694
|
process.exit(0);
|
|
@@ -1661,12 +1910,28 @@ function sessionHookCommand(program) {
|
|
|
1661
1910
|
const opts = cmd.opts();
|
|
1662
1911
|
await handleStart(opts.dir || process.cwd());
|
|
1663
1912
|
});
|
|
1664
|
-
cmd
|
|
1913
|
+
const check = cmd
|
|
1665
1914
|
.command('check')
|
|
1666
1915
|
.description('Check a pending edit against the active session (PreToolUse hook)')
|
|
1667
|
-
.
|
|
1916
|
+
.option('--trusted-adapter <adapter>', 'Ingress-bound adapter identity declared by the installed host hook')
|
|
1917
|
+
.option('--trusted-timing <timing>', 'Ingress-bound event timing', 'before_write');
|
|
1918
|
+
check.action(async (subOpts) => {
|
|
1668
1919
|
const opts = cmd.opts();
|
|
1669
|
-
|
|
1920
|
+
const adapters = [
|
|
1921
|
+
'claude-code-hooks', 'copilot-hooks', 'generic-mcp', 'codex-mcp',
|
|
1922
|
+
'cursor-mcp', 'vscode-extension', 'github-action',
|
|
1923
|
+
];
|
|
1924
|
+
const timings = ['before_write', 'during_write', 'after_write', 'ci'];
|
|
1925
|
+
// No implicit hard pre-write default: an unspecified adapter is the
|
|
1926
|
+
// non-privileged cooperative generic ingress, never Claude host enforcement.
|
|
1927
|
+
const trustedAdapter = subOpts.trustedAdapter ?? 'generic-mcp';
|
|
1928
|
+
if (!adapters.includes(trustedAdapter)) {
|
|
1929
|
+
throw new Error(`Unsupported trusted hook adapter: ${trustedAdapter}`);
|
|
1930
|
+
}
|
|
1931
|
+
if (!timings.includes(subOpts.trustedTiming)) {
|
|
1932
|
+
throw new Error(`Unsupported trusted hook timing: ${subOpts.trustedTiming}`);
|
|
1933
|
+
}
|
|
1934
|
+
await handleCheck(opts.dir || process.cwd(), trustedAdapter, subOpts.trustedTiming);
|
|
1670
1935
|
});
|
|
1671
1936
|
cmd
|
|
1672
1937
|
.command('finish')
|