@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.
Files changed (43) hide show
  1. package/README.md +3 -2
  2. package/dist/commands/brain.d.ts.map +1 -1
  3. package/dist/commands/brain.js +451 -0
  4. package/dist/commands/brain.js.map +1 -1
  5. package/dist/commands/policy.d.ts.map +1 -1
  6. package/dist/commands/policy.js +296 -0
  7. package/dist/commands/policy.js.map +1 -1
  8. package/dist/commands/runtime-adapter.d.ts +2 -1
  9. package/dist/commands/runtime-adapter.d.ts.map +1 -1
  10. package/dist/commands/runtime-adapter.js +51 -2
  11. package/dist/commands/runtime-adapter.js.map +1 -1
  12. package/dist/commands/session-hook.d.ts +17 -0
  13. package/dist/commands/session-hook.d.ts.map +1 -1
  14. package/dist/commands/session-hook.js +279 -14
  15. package/dist/commands/session-hook.js.map +1 -1
  16. package/dist/runtime-build.json +4 -4
  17. package/dist/utils/agent-adapter-setup.js +1 -1
  18. package/dist/utils/agent-adapter-setup.js.map +1 -1
  19. package/dist/utils/agent-guard.d.ts +1 -0
  20. package/dist/utils/agent-guard.d.ts.map +1 -1
  21. package/dist/utils/agent-guard.js +18 -6
  22. package/dist/utils/agent-guard.js.map +1 -1
  23. package/dist/utils/git-coverage.d.ts.map +1 -1
  24. package/dist/utils/git-coverage.js +1 -0
  25. package/dist/utils/git-coverage.js.map +1 -1
  26. package/dist/utils/local-repo-brain.d.ts +85 -0
  27. package/dist/utils/local-repo-brain.d.ts.map +1 -1
  28. package/dist/utils/local-repo-brain.js +259 -4
  29. package/dist/utils/local-repo-brain.js.map +1 -1
  30. package/dist/utils/proposed-change-analysis.d.ts +20 -0
  31. package/dist/utils/proposed-change-analysis.d.ts.map +1 -0
  32. package/dist/utils/proposed-change-analysis.js +448 -0
  33. package/dist/utils/proposed-change-analysis.js.map +1 -0
  34. package/dist/utils/repo-intelligence-v2.d.ts +28 -0
  35. package/dist/utils/repo-intelligence-v2.d.ts.map +1 -0
  36. package/dist/utils/repo-intelligence-v2.js +174 -0
  37. package/dist/utils/repo-intelligence-v2.js.map +1 -0
  38. package/dist/utils/v0-governance.d.ts +1 -1
  39. package/dist/utils/v0-governance.d.ts.map +1 -1
  40. package/dist/utils/v0-governance.js +86 -15
  41. package/dist/utils/v0-governance.js.map +1 -1
  42. package/package.json +12 -11
  43. 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
- : input.blockType === 'profile_or_runtime_health_block'
433
- ? 'runtime_health_recovery'
434
- : 'split_tool_call',
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
- : input.blockType === 'profile_or_runtime_health_block'
440
- ? 'Refresh or restart runtime'
441
- : 'Split into one file per tool call',
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
- : 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.'),
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
- .action(async () => {
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
- await handleCheck(opts.dir || process.cwd());
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')