snipara-companion 3.2.8 → 3.2.10

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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { ProjectIntentDetectionResult, ProjectRealityCheckResult, ProjectIntelligenceEngineeringLeadPlanSummary } from '@snipara/project-intelligence-contracts';
2
+ import { ProjectIntentDetectionResult, ProjectPolicyDecision, ProjectRealityCheckResult, ProjectIntelligenceEngineeringLeadPlanSummary, ProjectPolicyRule } from '@snipara/project-intelligence-contracts';
3
3
 
4
4
  declare function resolveQueryFromToolInput(toolInput?: string, tool?: string): string | null;
5
5
 
@@ -2030,6 +2030,7 @@ interface ProjectIntelligenceBrief {
2030
2030
  codeImpact?: Record<string, unknown>;
2031
2031
  codeImpactSourceSelection?: CodeGraphSourceSelection;
2032
2032
  localSessionSnapshot?: SessionSnapshot;
2033
+ projectPolicyDecision?: ProjectPolicyDecision;
2033
2034
  verificationPlan?: VerificationPlan;
2034
2035
  judgmentCard?: ProjectIntelligenceJudgmentCard;
2035
2036
  errors: Array<{
@@ -2375,7 +2376,7 @@ declare function formatCompanionEngineeringLeadPlanReport(report: CompanionEngin
2375
2376
  declare function leadPlanCommand(options: LeadPlanCommandOptions): Promise<void>;
2376
2377
 
2377
2378
  type ProjectPolicyGateSeverity = "advisory" | "required_action" | "block";
2378
- type ProjectPolicyGateSurface = "release" | "schema" | "auth" | "billing" | "deploy" | "package_surface";
2379
+ type ProjectPolicyGateSurface = "release" | "schema" | "auth" | "billing" | "deploy" | "package_surface" | "project_policy";
2379
2380
  type ProjectPolicyGateSampleMode = "not_applicable" | "structural" | "explicit_contract" | "sample_gated";
2380
2381
  interface ProjectPolicyGateSampleGate {
2381
2382
  mode: ProjectPolicyGateSampleMode;
@@ -2417,6 +2418,7 @@ interface ProjectPolicyGatesResult {
2417
2418
  release: boolean;
2418
2419
  summary: ProjectPolicyGatesSummary;
2419
2420
  gates: ProjectPolicyGateDecision[];
2421
+ projectPolicyDecision?: ProjectPolicyDecision;
2420
2422
  suggestedCommands: string[];
2421
2423
  }
2422
2424
  interface EvaluateProjectPolicyGatesInput {
@@ -2438,6 +2440,10 @@ interface EvaluateProjectPolicyGatesInput {
2438
2440
  command?: string;
2439
2441
  };
2440
2442
  judgmentCard?: ProjectIntelligenceJudgmentCard;
2443
+ projectPolicy?: {
2444
+ rules?: ProjectPolicyRule[];
2445
+ decision?: ProjectPolicyDecision;
2446
+ };
2441
2447
  }
2442
2448
  declare function evaluateProjectPolicyGates(input: EvaluateProjectPolicyGatesInput): ProjectPolicyGatesResult;
2443
2449
  declare function formatPolicyGateDecision(gateDecision: ProjectPolicyGateDecision): string[];
package/dist/index.js CHANGED
@@ -10634,6 +10634,151 @@ function buildIntentDetectionFromTimeline(events) {
10634
10634
  };
10635
10635
  }
10636
10636
 
10637
+ // ../project-intelligence-contracts/src/project-policy.ts
10638
+ var PROJECT_POLICY_DECISION_VERSION = "snipara.project_policy.decision.v0";
10639
+ var PROJECT_POLICY_RECEIPT_VERSION = "snipara.project_policy.receipt.v0";
10640
+ var VERDICT_RANK = {
10641
+ allow: 0,
10642
+ warn: 1,
10643
+ require_review: 2,
10644
+ block: 3
10645
+ };
10646
+ function evaluateProjectPolicyDecision(input) {
10647
+ const generatedAt = isoTimestamp2(input.now);
10648
+ const actionText = normalizeText2(
10649
+ [
10650
+ input.action.summary,
10651
+ input.action.surface,
10652
+ ...input.action.changedFiles ?? [],
10653
+ ...input.action.commands ?? [],
10654
+ metadataToText(input.action.metadata)
10655
+ ].filter((item) => Boolean(item))
10656
+ );
10657
+ const matchedRules = normalizeRules(input.rules).filter(
10658
+ (rule) => ruleMatchesAction(rule, actionText, input.action)
10659
+ );
10660
+ const verdict = matchedRules.reduce(
10661
+ (current, rule) => strongerVerdict(current, verdictForRule(rule, actionText)),
10662
+ "allow"
10663
+ );
10664
+ const confidence = matchedRules.length === 0 ? 1 : Math.max(...matchedRules.map((rule) => normalizeConfidence(rule.confidence)));
10665
+ const reasonCodes = buildReasonCodes(matchedRules, verdict);
10666
+ const requiredActions = uniqueStrings7(
10667
+ matchedRules.flatMap((rule) => rule.requiredActions ?? [rule.requirement])
10668
+ );
10669
+ const warnings = matchedRules.filter((rule) => verdictForRule(rule, actionText) === "warn").map((rule) => rule.requirement);
10670
+ const actionFingerprint = hashDecisionJsonValue(input.action);
10671
+ const receipt = {
10672
+ version: PROJECT_POLICY_RECEIPT_VERSION,
10673
+ receiptId: buildReceiptId(actionFingerprint, matchedRules),
10674
+ generatedAt,
10675
+ verdict,
10676
+ actionFingerprint,
10677
+ ruleRefs: matchedRules.map((rule) => rule.source.ref),
10678
+ confidence,
10679
+ reasonCodes,
10680
+ overrideAllowed: verdict !== "block",
10681
+ overrideRequiresReason: verdict !== "allow"
10682
+ };
10683
+ return {
10684
+ version: PROJECT_POLICY_DECISION_VERSION,
10685
+ generatedAt,
10686
+ verdict,
10687
+ confidence,
10688
+ matchedRules,
10689
+ reasonCodes,
10690
+ requiredActions,
10691
+ warnings,
10692
+ receipt
10693
+ };
10694
+ }
10695
+ function projectPolicyVerdictToGateSeverity(verdict) {
10696
+ if (verdict === "block") return "block";
10697
+ if (verdict === "require_review") return "required_action";
10698
+ return "advisory";
10699
+ }
10700
+ function normalizeRules(rules) {
10701
+ return rules.filter(
10702
+ (rule) => rule.id.trim() && rule.title.trim() && rule.requirement.trim() && rule.source.reviewStatus !== "rejected" && normalizeConfidence(rule.confidence) >= 0.5
10703
+ );
10704
+ }
10705
+ function ruleMatchesAction(rule, actionText, action) {
10706
+ if (action.surface && rule.scope !== "custom" && action.surface !== rule.scope) {
10707
+ return false;
10708
+ }
10709
+ const anchors = uniqueStrings7([
10710
+ rule.scope,
10711
+ ...rule.anchors,
10712
+ ...rule.forbiddenActions ?? [],
10713
+ ...rule.requiredActions ?? []
10714
+ ]);
10715
+ return anchors.some((anchor) => actionText.includes(normalizeText2(anchor)));
10716
+ }
10717
+ function verdictForRule(rule, actionText) {
10718
+ const forbidden = (rule.forbiddenActions ?? []).some(
10719
+ (item) => actionText.includes(normalizeText2(item))
10720
+ );
10721
+ if (rule.strength === "blocking" && forbidden && normalizeConfidence(rule.confidence) >= 0.8) {
10722
+ return "block";
10723
+ }
10724
+ if (rule.strength === "blocking" || rule.strength === "review_required") {
10725
+ return "require_review";
10726
+ }
10727
+ return "warn";
10728
+ }
10729
+ function strongerVerdict(left, right) {
10730
+ return VERDICT_RANK[right] > VERDICT_RANK[left] ? right : left;
10731
+ }
10732
+ function buildReasonCodes(rules, verdict) {
10733
+ if (rules.length === 0) {
10734
+ return ["project_policy_no_match"];
10735
+ }
10736
+ return uniqueStrings7([
10737
+ `project_policy_${verdict}`,
10738
+ ...rules.map((rule) => `project_policy_${rule.scope}`),
10739
+ ...rules.map((rule) => `project_policy_${rule.strength}`)
10740
+ ]);
10741
+ }
10742
+ function buildReceiptId(actionFingerprint, rules) {
10743
+ const hash = hashDecisionJsonValue({
10744
+ actionFingerprint,
10745
+ rules: rules.map((rule) => [rule.id, rule.source.ref])
10746
+ }).replace(/^sha256:/, "");
10747
+ return `project-policy-${hash.slice(0, 16)}`;
10748
+ }
10749
+ function isoTimestamp2(value) {
10750
+ if (value instanceof Date) return value.toISOString();
10751
+ if (typeof value === "string" && value.trim()) return new Date(value).toISOString();
10752
+ return (/* @__PURE__ */ new Date()).toISOString();
10753
+ }
10754
+ function normalizeConfidence(value) {
10755
+ if (!Number.isFinite(value)) return 0;
10756
+ return Math.max(0, Math.min(1, value));
10757
+ }
10758
+ function normalizeText2(value) {
10759
+ const text = Array.isArray(value) ? value.filter(Boolean).join("\n") : value ?? "";
10760
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
10761
+ }
10762
+ function uniqueStrings7(values) {
10763
+ const seen = /* @__PURE__ */ new Set();
10764
+ const result = [];
10765
+ for (const value of values) {
10766
+ const normalized = value.trim();
10767
+ if (!normalized || seen.has(normalized)) continue;
10768
+ seen.add(normalized);
10769
+ result.push(normalized);
10770
+ }
10771
+ return result;
10772
+ }
10773
+ function metadataToText(metadata) {
10774
+ if (!metadata) return "";
10775
+ try {
10776
+ return JSON.stringify(metadata).slice(0, 12e3);
10777
+ } catch {
10778
+ return "";
10779
+ }
10780
+ }
10781
+
10637
10782
  // src/commands/decision-requests.ts
10638
10783
  var fs12 = __toESM(require("fs"));
10639
10784
  var path11 = __toESM(require("path"));
@@ -10964,7 +11109,7 @@ function memoryEvidenceItem(value, options) {
10964
11109
  summary: options.summary,
10965
11110
  kind: options.kind,
10966
11111
  status: options.status,
10967
- files: uniqueStrings7(evidenceRefs),
11112
+ files: uniqueStrings8(evidenceRefs),
10968
11113
  metadata: compactObject({
10969
11114
  source: options.source,
10970
11115
  bucket: options.bucket,
@@ -11005,7 +11150,7 @@ function reviewQueueItems(result) {
11005
11150
  action: "review_queue_item",
11006
11151
  options: ["accept", "reject", "archive", "invalidate", "keep_pending"],
11007
11152
  recommendation: reviewStatus === "approved" || status === "active" ? "keep_pending" : "accept",
11008
- reasonCodes: uniqueStrings7(["memory_review_queue", reason ?? "", status ?? ""]),
11153
+ reasonCodes: uniqueStrings8(["memory_review_queue", reason ?? "", status ?? ""]),
11009
11154
  evidenceItem: memoryEvidenceItem(item, {
11010
11155
  ref: `memory:${memoryId}`,
11011
11156
  title: `Review memory ${memoryId}`,
@@ -11044,7 +11189,7 @@ function cleanCandidateItems(result) {
11044
11189
  action: `${bucket}_candidate`,
11045
11190
  options,
11046
11191
  recommendation,
11047
- reasonCodes: uniqueStrings7(["memory_clean_candidates", bucket, reason]),
11192
+ reasonCodes: uniqueStrings8(["memory_clean_candidates", bucket, reason]),
11048
11193
  evidenceItem: memoryEvidenceItem(candidate, {
11049
11194
  ref: `memory:${memoryId}`,
11050
11195
  title: `${bucket.replace(/_/g, " ")} memory candidate ${memoryId}`,
@@ -11081,7 +11226,7 @@ function duplicateCandidateItems(result) {
11081
11226
  action: "duplicate_candidate",
11082
11227
  options: ["merge", "supersede", "keep", "inspect"],
11083
11228
  recommendation: keepMemoryId ? "supersede" : "inspect",
11084
- reasonCodes: uniqueStrings7(["memory_duplicate_candidates", `group_${index + 1}`]),
11229
+ reasonCodes: uniqueStrings8(["memory_duplicate_candidates", `group_${index + 1}`]),
11085
11230
  evidenceItem: memoryEvidenceItem(candidate, {
11086
11231
  ref: `memory:${memoryId}`,
11087
11232
  title: `Duplicate memory candidate ${memoryId}`,
@@ -11311,10 +11456,10 @@ function buildMemoryAuditSummary(audit) {
11311
11456
  cleanupCandidateCounts,
11312
11457
  ...typeof compactDryRun.mutated === "boolean" ? { compactDryRunMutated: compactDryRun.mutated } : {},
11313
11458
  ...compactDryRunPlannedActions !== void 0 ? { compactDryRunPlannedActions } : {},
11314
- recommendedActions: uniqueStrings7(recommendedActions)
11459
+ recommendedActions: uniqueStrings8(recommendedActions)
11315
11460
  };
11316
11461
  }
11317
- function uniqueStrings7(values) {
11462
+ function uniqueStrings8(values) {
11318
11463
  return Array.from(new Set(values));
11319
11464
  }
11320
11465
  function printMemoryAuditSummary(summary) {
@@ -11529,7 +11674,7 @@ async function memoryAuditCommand(options) {
11529
11674
  var fs13 = __toESM(require("fs"));
11530
11675
  var path12 = __toESM(require("path"));
11531
11676
  var import_node_child_process2 = require("child_process");
11532
- function uniqueStrings8(values) {
11677
+ function uniqueStrings9(values) {
11533
11678
  return Array.from(new Set((values ?? []).map((value) => value.trim()).filter(Boolean)));
11534
11679
  }
11535
11680
  function slugify(value, fallback) {
@@ -11537,7 +11682,7 @@ function slugify(value, fallback) {
11537
11682
  return slug2 || fallback;
11538
11683
  }
11539
11684
  function keywordHints(value) {
11540
- return uniqueStrings8(
11685
+ return uniqueStrings9(
11541
11686
  value.split(/[^A-Za-z0-9_.:/-]+/).map((part) => part.trim()).filter((part) => part.length >= 4).slice(0, 8)
11542
11687
  );
11543
11688
  }
@@ -11583,17 +11728,17 @@ function expectedItems(section, values, files) {
11583
11728
  }
11584
11729
  function buildEvalCaseArtifact(options = {}) {
11585
11730
  const rootDir = path12.resolve(options.dir ?? process.cwd());
11586
- const files = uniqueStrings8(options.files);
11587
- const commandsRun = uniqueStrings8(options.commandRun);
11731
+ const files = uniqueStrings9(options.files);
11732
+ const commandsRun = uniqueStrings9(options.commandRun);
11588
11733
  const summary = options.summary?.trim() || "Snipara companion local workflow evaluation";
11589
11734
  const id = options.id?.trim() || slugify(summary, "snipara-companion-eval");
11590
11735
  const expected = {};
11591
11736
  const sectionInputs = {
11592
- context: uniqueStrings8(options.context),
11593
- decisions: uniqueStrings8(options.decision),
11594
- impact: uniqueStrings8(options.impact),
11595
- verification: uniqueStrings8(options.verification),
11596
- continuity: uniqueStrings8(options.continuity)
11737
+ context: uniqueStrings9(options.context),
11738
+ decisions: uniqueStrings9(options.decision),
11739
+ impact: uniqueStrings9(options.impact),
11740
+ verification: uniqueStrings9(options.verification),
11741
+ continuity: uniqueStrings9(options.continuity)
11597
11742
  };
11598
11743
  for (const [section, values] of Object.entries(sectionInputs)) {
11599
11744
  if (values.length > 0) {
@@ -13055,7 +13200,7 @@ var SESSION_SNAPSHOT_VERSION = "snipara.session_snapshot.v0";
13055
13200
  var ACTIVITY_RELATIVE_DIR = path16.join(".snipara", "activity");
13056
13201
  var ACTIVITY_TIMELINE_RELATIVE_PATH = path16.join(ACTIVITY_RELATIVE_DIR, "timeline.jsonl");
13057
13202
  var SESSION_SNAPSHOT_RELATIVE_PATH = path16.join(ACTIVITY_RELATIVE_DIR, "session.json");
13058
- function uniqueStrings9(values) {
13203
+ function uniqueStrings10(values) {
13059
13204
  return Array.from(new Set(values.map((value) => value?.trim()).filter(Boolean)));
13060
13205
  }
13061
13206
  function stableJson(value) {
@@ -13128,8 +13273,8 @@ function appendActivityEvent(input) {
13128
13273
  ...input.phaseId ? { phaseId: input.phaseId } : {},
13129
13274
  ...input.actor ? { actor: input.actor } : {},
13130
13275
  ...input.outcome ? { outcome: input.outcome } : {},
13131
- files: uniqueStrings9(input.files ?? []),
13132
- refs: uniqueStrings9(input.refs ?? []),
13276
+ files: uniqueStrings10(input.files ?? []),
13277
+ refs: uniqueStrings10(input.refs ?? []),
13133
13278
  metadata: input.metadata ?? {}
13134
13279
  };
13135
13280
  const timelinePath = getActivityTimelinePath(cwd);
@@ -13224,7 +13369,7 @@ function deriveSessionSnapshotSummary(args) {
13224
13369
  if (args.workflow?.status === "blocked") {
13225
13370
  riskReasons.push("workflow is blocked");
13226
13371
  }
13227
- const touchedFiles = uniqueStrings9(args.latestEvents.flatMap((event) => event.files)).slice(
13372
+ const touchedFiles = uniqueStrings10(args.latestEvents.flatMap((event) => event.files)).slice(
13228
13373
  0,
13229
13374
  20
13230
13375
  );
@@ -13324,7 +13469,7 @@ function readSessionSnapshot(cwd = process.cwd()) {
13324
13469
  }
13325
13470
 
13326
13471
  // src/commands/journal.ts
13327
- function uniqueStrings10(values) {
13472
+ function uniqueStrings11(values) {
13328
13473
  if (!values) {
13329
13474
  return [];
13330
13475
  }
@@ -13352,7 +13497,7 @@ function buildJournalCheckpointEntry(payload) {
13352
13497
  payload.actor ? `Actor: ${payload.actor}` : null,
13353
13498
  payload.attention ? `Attention: ${payload.attention}` : null,
13354
13499
  payload.next ? `Next: ${payload.next}` : null,
13355
- uniqueStrings10(payload.files).length > 0 ? `Files: ${uniqueStrings10(payload.files).join(", ")}` : null
13500
+ uniqueStrings11(payload.files).length > 0 ? `Files: ${uniqueStrings11(payload.files).join(", ")}` : null
13356
13501
  ].filter((line) => Boolean(line));
13357
13502
  return {
13358
13503
  text: lines.join("\n"),
@@ -13448,7 +13593,7 @@ function createRedactor(rootDir) {
13448
13593
  patterns.add(item.name);
13449
13594
  item.pattern.lastIndex = 0;
13450
13595
  }
13451
- const uniqueMatchedPatterns = uniqueStrings11(matchedPatterns);
13596
+ const uniqueMatchedPatterns = uniqueStrings12(matchedPatterns);
13452
13597
  if (uniqueMatchedPatterns.length > 0) {
13453
13598
  redactedValueCount += 1;
13454
13599
  }
@@ -13563,14 +13708,14 @@ function buildRepoState(input, options, rootDir, redactor) {
13563
13708
  const inputRepo = inputRecord(input, "repoState", "repo_state", "repo");
13564
13709
  const detected = detectGitState(rootDir);
13565
13710
  const statusCounts = countGitStatus(detected.statusLines);
13566
- const changedFiles = uniqueStrings11([
13711
+ const changedFiles = uniqueStrings12([
13567
13712
  ...normalizeUnknownStringList(inputRepo.changedFiles),
13568
13713
  ...normalizeUnknownStringList(inputRepo.changed_files),
13569
13714
  ...normalizeUnknownStringList(input.changedFiles),
13570
13715
  ...normalizeUnknownStringList(input.changed_files),
13571
13716
  ...options.changedFiles ?? []
13572
13717
  ]).map((item) => redactor.redact(item).value);
13573
- const recentFiles = uniqueStrings11([
13718
+ const recentFiles = uniqueStrings12([
13574
13719
  ...normalizeUnknownStringList(inputRepo.recentFiles),
13575
13720
  ...normalizeUnknownStringList(inputRepo.recent_files),
13576
13721
  ...normalizeUnknownStringList(input.recentFiles),
@@ -13638,18 +13783,18 @@ function normalizeEvidenceItem(value, section, index, redactor) {
13638
13783
  "command",
13639
13784
  "check"
13640
13785
  ]) ?? fallbackSummary(value, section, index);
13641
- const reasonCodes = uniqueStrings11([
13786
+ const reasonCodes = uniqueStrings12([
13642
13787
  `section_${section}`,
13643
13788
  ...normalizeUnknownStringList(value.reasonCodes),
13644
13789
  ...normalizeUnknownStringList(value.reason_codes)
13645
13790
  ]).map((item) => redactor.redact(item).value);
13646
- const files = uniqueStrings11([
13791
+ const files = uniqueStrings12([
13647
13792
  ...normalizeUnknownStringList(value.files),
13648
13793
  ...normalizeUnknownStringList(value.changedFiles),
13649
13794
  ...normalizeUnknownStringList(value.changed_files),
13650
13795
  ...normalizeUnknownStringList(value.file)
13651
13796
  ]).slice(0, MAX_LIST_ITEMS).map((item) => redactor.redact(item).value);
13652
- const commands = uniqueStrings11([
13797
+ const commands = uniqueStrings12([
13653
13798
  ...normalizeUnknownStringList(value.commands),
13654
13799
  ...normalizeUnknownStringList(value.command),
13655
13800
  ...normalizeUnknownStringList(value.checks),
@@ -13673,7 +13818,7 @@ function normalizeEvidenceItem(value, section, index, redactor) {
13673
13818
  });
13674
13819
  }
13675
13820
  function collectReasonCodes(input, cliValues, items, redactor) {
13676
- return uniqueStrings11([
13821
+ return uniqueStrings12([
13677
13822
  "coding_intelligence_ledger_v0",
13678
13823
  ...normalizeUnknownStringList(input.reasonCodes),
13679
13824
  ...normalizeUnknownStringList(input.reason_codes),
@@ -13704,13 +13849,13 @@ function buildCalibrationMetadata(input, cliValues, redactor) {
13704
13849
  "calibration",
13705
13850
  "calibration_metadata"
13706
13851
  );
13707
- const notes = uniqueStrings11([
13852
+ const notes = uniqueStrings12([
13708
13853
  ...normalizeUnknownStringList(calibration.notes),
13709
13854
  ...normalizeUnknownStringList(calibration.note),
13710
13855
  ...normalizeUnknownStringList(input.calibrationNotes),
13711
13856
  ...cliValues ?? []
13712
13857
  ]).slice(0, MAX_LIST_ITEMS).map((item) => redactor.redact(item).value);
13713
- const caveats = uniqueStrings11([
13858
+ const caveats = uniqueStrings12([
13714
13859
  ...normalizeUnknownStringList(calibration.caveats),
13715
13860
  ...normalizeUnknownStringList(calibration.caveat),
13716
13861
  ...normalizeUnknownStringList(input.calibrationCaveats)
@@ -13907,7 +14052,7 @@ function validIso(value) {
13907
14052
  const date = new Date(value);
13908
14053
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
13909
14054
  }
13910
- function uniqueStrings11(values) {
14055
+ function uniqueStrings12(values) {
13911
14056
  return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
13912
14057
  }
13913
14058
  function compactWhitespace(value) {
@@ -16209,16 +16354,16 @@ function upsertLocalAdaptiveRoutingPolicy(workerRole) {
16209
16354
  mode: stringValue6(existing.mode) ?? "catalog",
16210
16355
  plannerRetainsReasoning: existing.plannerRetainsReasoning !== false,
16211
16356
  preferLocalWorkers: true,
16212
- allowedEndpointTypes: uniqueStrings12([
16357
+ allowedEndpointTypes: uniqueStrings13([
16213
16358
  ...normalizeStringList3(existing.allowedEndpointTypes),
16214
16359
  "local",
16215
16360
  "cloud"
16216
16361
  ]),
16217
- preferredEndpointTypes: uniqueStrings12([
16362
+ preferredEndpointTypes: uniqueStrings13([
16218
16363
  "local",
16219
16364
  ...normalizeStringList3(existing.preferredEndpointTypes)
16220
16365
  ]),
16221
- allowedWorkerClasses: uniqueStrings12([
16366
+ allowedWorkerClasses: uniqueStrings13([
16222
16367
  ...normalizeStringList3(existing.allowedWorkerClasses),
16223
16368
  workerRole,
16224
16369
  "documentation",
@@ -16458,7 +16603,7 @@ function normalizeWorkerProfileLanguages(value) {
16458
16603
  return value.length > 0 ? value : ["typescript"];
16459
16604
  }
16460
16605
  function normalizeWorkerCapabilities(values) {
16461
- return values.length > 0 ? uniqueStrings12(values.map((value) => String(value).trim()).filter(Boolean)) : ["code", "repo_scan", "refactor"];
16606
+ return values.length > 0 ? uniqueStrings13(values.map((value) => String(value).trim()).filter(Boolean)) : ["code", "repo_scan", "refactor"];
16462
16607
  }
16463
16608
  function defaultCapabilitiesForWorker(workerRole) {
16464
16609
  if (workerRole === "documentation") {
@@ -16474,9 +16619,9 @@ function defaultCapabilitiesForWorker(workerRole) {
16474
16619
  }
16475
16620
  function normalizeStringList3(value) {
16476
16621
  const values = Array.isArray(value) ? value : value === void 0 ? [] : [value];
16477
- return uniqueStrings12(values.map(stringValue6).filter((item) => Boolean(item)));
16622
+ return uniqueStrings13(values.map(stringValue6).filter((item) => Boolean(item)));
16478
16623
  }
16479
- function uniqueStrings12(values) {
16624
+ function uniqueStrings13(values) {
16480
16625
  return Array.from(new Set(values));
16481
16626
  }
16482
16627
  function stringValue6(value) {
@@ -16634,7 +16779,7 @@ function numberValue4(value) {
16634
16779
  const parsed = Number(text);
16635
16780
  return Number.isFinite(parsed) ? parsed : void 0;
16636
16781
  }
16637
- function uniqueStrings13(values) {
16782
+ function uniqueStrings14(values) {
16638
16783
  return Array.from(new Set(values.filter((value) => value.trim().length > 0)));
16639
16784
  }
16640
16785
  function normalizeEnum(value) {
@@ -17121,6 +17266,12 @@ function scoreBootstrapBriefEntry(entry, source2, now) {
17121
17266
  }
17122
17267
  if (type === "decision") {
17123
17268
  score += 25;
17269
+ const authorityStatus = bootstrapAuthorityStatus(entry);
17270
+ if (authorityStatus === "canonical") {
17271
+ score += 55;
17272
+ } else if (authorityStatus === "approved" || authorityStatus === "authoritative") {
17273
+ score += 20;
17274
+ }
17124
17275
  } else if (type === "context") {
17125
17276
  score += 20;
17126
17277
  } else if (type === "learning") {
@@ -17166,6 +17317,89 @@ function scoreBootstrapBriefEntry(entry, source2, now) {
17166
17317
  function estimateBootstrapEntryTokens(entry) {
17167
17318
  return Math.max(8, Math.ceil(compactSessionEntryLine(entry).length / 4));
17168
17319
  }
17320
+ function bootstrapEntryType(entry) {
17321
+ return typeof entry.type === "string" ? entry.type.toLowerCase() : "";
17322
+ }
17323
+ function isBootstrapDecisionEntry(entry) {
17324
+ return bootstrapEntryType(entry) === "decision";
17325
+ }
17326
+ function bootstrapAuthorityStatus(entry) {
17327
+ const authority = isRecord9(entry.authority) ? entry.authority : void 0;
17328
+ return (stringValue7(entry.authority_status) ?? stringValue7(authority?.authorityStatus) ?? stringValue7(authority?.level) ?? "").toLowerCase();
17329
+ }
17330
+ function bootstrapSimilarityTokens(entry) {
17331
+ const stopWords = /* @__PURE__ */ new Set([
17332
+ "checkpoint",
17333
+ "workflow",
17334
+ "final",
17335
+ "commit",
17336
+ "phase",
17337
+ "summary",
17338
+ "context",
17339
+ "team",
17340
+ "sync",
17341
+ "handoff",
17342
+ "released",
17343
+ "release"
17344
+ ]);
17345
+ const text = readSessionEntryPreview(entry).toLowerCase().replace(/snipara-companion@\d+\.\d+\.\d+/g, "snipara-companion").replace(/[^a-z0-9]+/g, " ");
17346
+ return new Set(text.split(/\s+/).filter((token) => token.length >= 4 && !stopWords.has(token)));
17347
+ }
17348
+ function buildBootstrapTopicTokens(ranked) {
17349
+ const tokens = /* @__PURE__ */ new Set();
17350
+ for (const candidate of ranked) {
17351
+ if (tokens.size >= 48) {
17352
+ break;
17353
+ }
17354
+ if (candidate.score <= 0 || !isSessionCarryoverEntry(candidate.entry)) {
17355
+ continue;
17356
+ }
17357
+ for (const token of bootstrapSimilarityTokens(candidate.entry)) {
17358
+ tokens.add(token);
17359
+ }
17360
+ }
17361
+ return tokens;
17362
+ }
17363
+ function isDecisionRelevantToBootstrap(entry, topicTokens, hasCarryoverCandidate) {
17364
+ if (!isBootstrapDecisionEntry(entry)) {
17365
+ return false;
17366
+ }
17367
+ if (!hasCarryoverCandidate) {
17368
+ return true;
17369
+ }
17370
+ const text = readBootstrapEntryText(entry).toLowerCase();
17371
+ const hasExplicitTopic = text.includes("control plane") || text.includes("control-plane") || text.includes("lite") || text.includes("session-bootstrap") || text.includes("bootstrap brief");
17372
+ if (hasExplicitTopic) {
17373
+ return true;
17374
+ }
17375
+ const category = typeof entry.category === "string" ? entry.category.toLowerCase() : "";
17376
+ if (category.includes("workflow-phase") || category.includes("final-commit") || category.includes("journal:") || category.includes("team_sync")) {
17377
+ return false;
17378
+ }
17379
+ let overlap = 0;
17380
+ for (const token of bootstrapSimilarityTokens(entry)) {
17381
+ if (topicTokens.has(token)) {
17382
+ overlap += 1;
17383
+ }
17384
+ }
17385
+ return overlap >= 3;
17386
+ }
17387
+ function areBootstrapEntriesSimilar(a, b) {
17388
+ const aTokens = bootstrapSimilarityTokens(a);
17389
+ const bTokens = bootstrapSimilarityTokens(b);
17390
+ if (aTokens.size === 0 || bTokens.size === 0) {
17391
+ return false;
17392
+ }
17393
+ let overlap = 0;
17394
+ for (const token of aTokens) {
17395
+ if (bTokens.has(token)) {
17396
+ overlap += 1;
17397
+ }
17398
+ }
17399
+ const smaller = Math.min(aTokens.size, bTokens.size);
17400
+ const larger = Math.max(aTokens.size, bTokens.size);
17401
+ return overlap / smaller >= 0.6 || overlap >= 8 && overlap / larger >= 0.5;
17402
+ }
17169
17403
  function buildSessionBootstrapBrief(result, options) {
17170
17404
  const normalized = normalizeSessionMemoriesResult(result);
17171
17405
  const now = options.now ?? /* @__PURE__ */ new Date();
@@ -17184,8 +17418,17 @@ function buildSessionBootstrapBrief(result, options) {
17184
17418
  const hasFreshCandidate = ranked.some(
17185
17419
  (candidate) => candidate.score > 0 && !isLikelyStaleBootstrapEntry(candidate.entry, now) && !isLikelyTestMemory(candidate.entry)
17186
17420
  );
17421
+ const topicTokens = buildBootstrapTopicTokens(ranked);
17422
+ const hasCarryoverCandidate = ranked.some(
17423
+ (candidate) => candidate.score > 0 && isSessionCarryoverEntry(candidate.entry) && !isLikelyStaleBootstrapEntry(candidate.entry, now) && !isLikelyTestMemory(candidate.entry)
17424
+ );
17425
+ const hasFreshDecisionCandidate = maxEntries >= 4 && ranked.some(
17426
+ (candidate) => candidate.score > 0 && isDecisionRelevantToBootstrap(candidate.entry, topicTokens, hasCarryoverCandidate) && !isLikelyStaleBootstrapEntry(candidate.entry, now) && !isLikelyTestMemory(candidate.entry)
17427
+ );
17187
17428
  const entries = [];
17188
17429
  let estimatedTokens = 0;
17430
+ let selectedCarryoverCount = 0;
17431
+ let selectedDecisionCount = 0;
17189
17432
  for (const candidate of ranked) {
17190
17433
  if (hasFreshCandidate && (isLikelyStaleBootstrapEntry(candidate.entry, now) || isLikelyTestMemory(candidate.entry))) {
17191
17434
  continue;
@@ -17196,13 +17439,30 @@ function buildSessionBootstrapBrief(result, options) {
17196
17439
  if (seen.has(key)) {
17197
17440
  continue;
17198
17441
  }
17199
- seen.add(key);
17442
+ if (entries.some((entry) => areBootstrapEntriesSimilar(entry, candidate.entry))) {
17443
+ continue;
17444
+ }
17445
+ const isCarryover = isSessionCarryoverEntry(candidate.entry);
17446
+ const isDecision = isBootstrapDecisionEntry(candidate.entry);
17447
+ if (isDecision && !isDecisionRelevantToBootstrap(candidate.entry, topicTokens, hasCarryoverCandidate)) {
17448
+ continue;
17449
+ }
17450
+ if (hasFreshDecisionCandidate && selectedDecisionCount === 0 && isCarryover && selectedCarryoverCount >= maxEntries - 1) {
17451
+ continue;
17452
+ }
17200
17453
  const entryTokens = estimateBootstrapEntryTokens(candidate.entry);
17201
17454
  if (entries.length > 0 && estimatedTokens + entryTokens > budgetTokens) {
17202
17455
  continue;
17203
17456
  }
17457
+ seen.add(key);
17204
17458
  entries.push(candidate.entry);
17205
17459
  estimatedTokens += entryTokens;
17460
+ if (isCarryover) {
17461
+ selectedCarryoverCount += 1;
17462
+ }
17463
+ if (isDecision) {
17464
+ selectedDecisionCount += 1;
17465
+ }
17206
17466
  if (entries.length >= maxEntries) {
17207
17467
  break;
17208
17468
  }
@@ -17835,7 +18095,7 @@ function buildProducerLoopArtifact(options) {
17835
18095
  workflowId ?? "unmanaged",
17836
18096
  options.phase?.id ? `phase:${options.phase.id}` : "final"
17837
18097
  ].join(":");
17838
- const reasonCodes = uniqueStrings13([
18098
+ const reasonCodes = uniqueStrings14([
17839
18099
  "producer_loop_v0",
17840
18100
  options.kind,
17841
18101
  `outcome_${options.outcome}`,
@@ -18027,8 +18287,8 @@ function summarizeProducerLoopArtifactFile(filePath, cwd) {
18027
18287
  relativePath: relativePath2,
18028
18288
  artifactHash: `sha256:${hashContent(content)}`,
18029
18289
  ledgerHash: stringValue7(parsed.ledgerHash),
18030
- reasonCodes: uniqueStrings13(stringArrayValue(ledger?.reasonCodes)),
18031
- files: uniqueStrings13(stringArrayValue(producer.files)),
18290
+ reasonCodes: uniqueStrings14(stringArrayValue(ledger?.reasonCodes)),
18291
+ files: uniqueStrings14(stringArrayValue(producer.files)),
18032
18292
  calibrationStatus: stringValue7(calibration?.status),
18033
18293
  reviewStatus,
18034
18294
  reviewOutcome: stringValue7(review?.outcome),
@@ -18075,10 +18335,10 @@ function buildProducerLoopReport(options = {}) {
18075
18335
  (left, right) => left.generatedAt === right.generatedAt ? left.relativePath.localeCompare(right.relativePath) : left.generatedAt.localeCompare(right.generatedAt)
18076
18336
  );
18077
18337
  const invalidArtifacts = summaries.map((entry) => entry.invalid).filter((entry) => Boolean(entry));
18078
- const producerKinds = uniqueStrings13(artifacts.map((artifact) => artifact.producerKind)).filter(
18338
+ const producerKinds = uniqueStrings14(artifacts.map((artifact) => artifact.producerKind)).filter(
18079
18339
  (kind) => isProducerLoopProducerKind(kind)
18080
18340
  );
18081
- const workflowIds = uniqueStrings13(
18341
+ const workflowIds = uniqueStrings14(
18082
18342
  artifacts.map((artifact) => artifact.workflowId).filter((id) => Boolean(id))
18083
18343
  );
18084
18344
  const reasonCodeCounts = countStringOccurrences(
@@ -18229,7 +18489,7 @@ function readProducerLoopArtifactForReview(filePath) {
18229
18489
  return parsed;
18230
18490
  }
18231
18491
  function applyProducerLoopArtifactReview(artifact, options) {
18232
- const reviewNotes = uniqueStrings13(options.notes ?? []);
18492
+ const reviewNotes = uniqueStrings14(options.notes ?? []);
18233
18493
  const reviewNote = options.status === "sample_rejected" ? "Sample rejected by local operator review." : "Sample reviewed by local operator.";
18234
18494
  const existingCalibration = artifact.calibration ?? {
18235
18495
  status: "sample_unreviewed",
@@ -18244,7 +18504,7 @@ function applyProducerLoopArtifactReview(artifact, options) {
18244
18504
  status: options.status,
18245
18505
  sampleSize: 1,
18246
18506
  hardGateReady: false,
18247
- notes: uniqueStrings13([
18507
+ notes: uniqueStrings14([
18248
18508
  ...stringArrayValue(existingCalibration.notes),
18249
18509
  reviewNote,
18250
18510
  ...reviewNotes.map((note) => `Review note: ${note}`)
@@ -18459,7 +18719,7 @@ function applyDecisionRequestLocally(options) {
18459
18719
  reject: isRejectChoice(options.choice),
18460
18720
  outcome: isRejectChoice(options.choice) ? "false_positive" : "useful",
18461
18721
  reviewer: options.reviewer,
18462
- notes: uniqueStrings13([
18722
+ notes: uniqueStrings14([
18463
18723
  `decision_request:${request.requestId}`,
18464
18724
  ...options.note ? [options.note] : []
18465
18725
  ])
@@ -18482,7 +18742,7 @@ function recurringPolicySuggestionTargetCategory(request) {
18482
18742
  const metadata = isRecord9(item.metadata) ? item.metadata : void 0;
18483
18743
  return stringValue7(metadata?.category) ?? stringValue7(metadata?.type) ?? item.kind;
18484
18744
  }).filter((value) => Boolean(value)) ?? [];
18485
- return uniqueStrings13(evidenceCategories).sort().join("|") || request.evidence.applyPath || request.decision;
18745
+ return uniqueStrings14(evidenceCategories).sort().join("|") || request.evidence.applyPath || request.decision;
18486
18746
  }
18487
18747
  function buildRecurringDecisionPolicySuggestionRequest(resolvedRecords) {
18488
18748
  const grouped = /* @__PURE__ */ new Map();
@@ -18520,12 +18780,12 @@ function buildRecurringDecisionPolicySuggestionRequest(resolvedRecords) {
18520
18780
  status: record.response.choice,
18521
18781
  files: record.request.evidence.files?.slice(0, 12)
18522
18782
  })),
18523
- reasonCodes: uniqueStrings13([
18783
+ reasonCodes: uniqueStrings14([
18524
18784
  "recurring_decision_receipts",
18525
18785
  "never_ask_twice_candidate",
18526
18786
  ...latest.request.evidence.reasonCodes
18527
18787
  ]),
18528
- files: uniqueStrings13(repeated.flatMap((record) => record.request.evidence.files ?? [])),
18788
+ files: uniqueStrings14(repeated.flatMap((record) => record.request.evidence.files ?? [])),
18529
18789
  applyPath: "manual_context_review",
18530
18790
  applyCommand: "Review the suggested rule and add it to the appropriate project policy or AGENTS.md section manually."
18531
18791
  },
@@ -18667,12 +18927,12 @@ function buildProducerTriageRequest(report) {
18667
18927
  summary: `${candidates.length} unreviewed Producer Loop samples; ${report.invalidArtifacts.length} invalid artifacts; hardGateReady remains false.`,
18668
18928
  refs,
18669
18929
  items,
18670
- reasonCodes: uniqueStrings13([
18930
+ reasonCodes: uniqueStrings14([
18671
18931
  "triage_rules_v0",
18672
18932
  "producer_loop_report_valid",
18673
18933
  ...report.invalidArtifacts.length === 0 ? ["no_invalid_artifacts"] : ["invalid_artifacts_present"]
18674
18934
  ]),
18675
- files: uniqueStrings13(candidates.flatMap((artifact) => artifact.files)),
18935
+ files: uniqueStrings14(candidates.flatMap((artifact) => artifact.files)),
18676
18936
  applyPath: "workflow producer-review",
18677
18937
  applyCommand: "snipara-companion workflow decide <request-id> --choose accept_all --reviewer <name>"
18678
18938
  },
@@ -20448,8 +20708,8 @@ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
20448
20708
  runtimeCatalog: catalog,
20449
20709
  routingCard: {
20450
20710
  ...routing.routingCard,
20451
- reasons: uniqueStrings13(reasons),
20452
- warnings: uniqueStrings13([...routing.routingCard.warnings, ...gatewayWarnings])
20711
+ reasons: uniqueStrings14(reasons),
20712
+ warnings: uniqueStrings14([...routing.routingCard.warnings, ...gatewayWarnings])
20453
20713
  }
20454
20714
  };
20455
20715
  } catch (error) {
@@ -20466,7 +20726,7 @@ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
20466
20726
  },
20467
20727
  routingCard: {
20468
20728
  ...routing.routingCard,
20469
- warnings: uniqueStrings13([...routing.routingCard.warnings, warning])
20729
+ warnings: uniqueStrings14([...routing.routingCard.warnings, warning])
20470
20730
  }
20471
20731
  };
20472
20732
  }
@@ -20657,7 +20917,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
20657
20917
  const approvalRequired = booleanValue4(
20658
20918
  isRecord9(resolution?.policyDecision) ? resolution.policyDecision.approvalRequired : void 0
20659
20919
  );
20660
- const gatewayWarnings = uniqueStrings13([
20920
+ const gatewayWarnings = uniqueStrings14([
20661
20921
  ...resolutionWarnings,
20662
20922
  ...selectedCandidate ? [] : ["Local orchestrator did not select a worker candidate and will fail closed."]
20663
20923
  ]);
@@ -20679,7 +20939,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
20679
20939
  ...selectedCandidate?.workerClass ? { recommendedWorkerClass: selectedCandidate.workerClass } : {},
20680
20940
  ...resolutionRejectedReasons ? { rejectedReasons: resolutionRejectedReasons } : {},
20681
20941
  ...approvalRequired !== void 0 ? { humanApprovalRequired: approvalRequired } : {},
20682
- reasons: uniqueStrings13([
20942
+ reasons: uniqueStrings14([
20683
20943
  ...routing.routingCard.reasons,
20684
20944
  ...resolutionReasons,
20685
20945
  ...selectedCandidate ? [
@@ -20688,7 +20948,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
20688
20948
  ...stringValue7(selectedEndpoint?.model) ? [`selected local model ${String(selectedEndpoint?.model)}`] : []
20689
20949
  ] : ["local orchestrator could not resolve a concrete worker candidate"]
20690
20950
  ]),
20691
- warnings: uniqueStrings13([...routing.routingCard.warnings, ...gatewayWarnings])
20951
+ warnings: uniqueStrings14([...routing.routingCard.warnings, ...gatewayWarnings])
20692
20952
  }
20693
20953
  };
20694
20954
  } catch (error) {
@@ -20705,7 +20965,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
20705
20965
  },
20706
20966
  routingCard: {
20707
20967
  ...routing.routingCard,
20708
- warnings: uniqueStrings13([...routing.routingCard.warnings, warning])
20968
+ warnings: uniqueStrings14([...routing.routingCard.warnings, warning])
20709
20969
  }
20710
20970
  };
20711
20971
  }
@@ -22574,8 +22834,8 @@ function buildWorkflowAdaptiveRouting(options, policy = null, intentWarnings = [
22574
22834
  ...routing,
22575
22835
  routingCard: {
22576
22836
  ...routing.routingCard,
22577
- reasons: uniqueStrings13([...routing.routingCard.reasons, ...policyReasons]),
22578
- warnings: uniqueStrings13([...routing.routingCard.warnings, ...policyWarnings])
22837
+ reasons: uniqueStrings14([...routing.routingCard.reasons, ...policyReasons]),
22838
+ warnings: uniqueStrings14([...routing.routingCard.warnings, ...policyWarnings])
22579
22839
  }
22580
22840
  };
22581
22841
  }
@@ -22591,7 +22851,7 @@ function normalizeAdaptiveRoutingMode(value) {
22591
22851
  return normalized === "off" || normalized === "recommend" || normalized === "catalog" ? normalized : null;
22592
22852
  }
22593
22853
  function normalizeAdaptiveWorkerClasses(values) {
22594
- return uniqueStrings13((values ?? []).map(canonicalAdaptiveWorkerClass)).filter(
22854
+ return uniqueStrings14((values ?? []).map(canonicalAdaptiveWorkerClass)).filter(
22595
22855
  (value) => ["documentation", "tests", "review", "coding"].includes(value)
22596
22856
  );
22597
22857
  }
@@ -23891,7 +24151,25 @@ async function loadDocumentCommand(options) {
23891
24151
  printLoadDocumentResult(options.path, result);
23892
24152
  }
23893
24153
  async function sessionBootstrapCommand(options) {
23894
- ensureConfigured();
24154
+ if (!isConfigured()) {
24155
+ if (options.json) {
24156
+ printJson2({
24157
+ critical: { memories: [], count: 0, tokens: 0 },
24158
+ daily: { memories: [], count: 0, tokens: 0 },
24159
+ total_tokens: 0,
24160
+ session_context: {
24161
+ included: Boolean(options.includeSessionContext),
24162
+ max_tokens: options.includeSessionContext ? DEFAULT_SESSION_CONTEXT_TOKENS : 0
24163
+ },
24164
+ session_bootstrap_quality: buildSessionBootstrapQuality({
24165
+ critical: { memories: [], count: 0, tokens: 0 },
24166
+ daily: { memories: [], count: 0, tokens: 0 },
24167
+ total_tokens: 0
24168
+ })
24169
+ });
24170
+ }
24171
+ return;
24172
+ }
23895
24173
  const resolvedContextTokens = options.maxContextTokens !== void 0 ? options.maxContextTokens : options.includeSessionContext ? DEFAULT_SESSION_CONTEXT_TOKENS : 0;
23896
24174
  const client = createClient(15e3);
23897
24175
  const result = await client.getSessionMemories(options.maxCriticalTokens, resolvedContextTokens);
@@ -26800,7 +27078,7 @@ function normalizeCaptureEvent(event, now, index) {
26800
27078
  const reasons = normalizeStringList4(event.reason).map(redactText);
26801
27079
  const evidence = normalizeStringList4(event.evidence).map(redactText);
26802
27080
  const commands = normalizeStringList4(event.commands).map(redactText);
26803
- const redactionPatterns = uniqueStrings14([
27081
+ const redactionPatterns = uniqueStrings15([
26804
27082
  ...summary.patterns,
26805
27083
  ...outcome.patterns,
26806
27084
  ...status.patterns,
@@ -26833,7 +27111,7 @@ function normalizeEventKind(value) {
26833
27111
  return "feedback";
26834
27112
  }
26835
27113
  function reasonCodesForEvent(event, kind, status) {
26836
- return uniqueStrings14([
27114
+ return uniqueStrings15([
26837
27115
  "why_outcome_capture_v1",
26838
27116
  "review_pending_authority",
26839
27117
  `source_${event.kind}`,
@@ -26898,7 +27176,7 @@ function readEventsFromFile(filePath) {
26898
27176
  function stableHash2(parts) {
26899
27177
  return (0, import_node_crypto8.createHash)("sha256").update(parts.join("\0")).digest("hex");
26900
27178
  }
26901
- function uniqueStrings14(values) {
27179
+ function uniqueStrings15(values) {
26902
27180
  return Array.from(new Set(values.filter(Boolean)));
26903
27181
  }
26904
27182
  function compactWhitespace3(value) {
@@ -28149,6 +28427,108 @@ function actionList(value) {
28149
28427
  return reason ? `${priority}${preview3(title, 80)} - ${preview3(reason, 140)}` : `${priority}${preview3(title, 160)}`;
28150
28428
  }).filter(Boolean);
28151
28429
  }
28430
+ function extractProjectPolicyRulesFromResumeContext(resumeContext) {
28431
+ if (!resumeContext) {
28432
+ return [];
28433
+ }
28434
+ return collectRecords(resumeContext).map(recordToProjectPolicyRule).filter((rule) => Boolean(rule));
28435
+ }
28436
+ function collectRecords(value, depth = 0) {
28437
+ if (depth > 5) {
28438
+ return [];
28439
+ }
28440
+ if (Array.isArray(value)) {
28441
+ return value.flatMap((item) => collectRecords(item, depth + 1));
28442
+ }
28443
+ if (!isRecord15(value)) {
28444
+ return [];
28445
+ }
28446
+ return [
28447
+ value,
28448
+ ...Object.values(value).flatMap(
28449
+ (item) => typeof item === "object" && item !== null ? collectRecords(item, depth + 1) : []
28450
+ )
28451
+ ];
28452
+ }
28453
+ function recordToProjectPolicyRule(record) {
28454
+ const type = preview3(record.type ?? record.memory_type ?? record.category, 80).toLowerCase();
28455
+ const content = preview3(record.content ?? record.text ?? record.summary ?? record.decision, 4e3);
28456
+ if (!content || !/(decision|policy|workflow-policy|roadmap)/i.test(type)) {
28457
+ return void 0;
28458
+ }
28459
+ const status = preview3(record.status ?? record.lifecycleStatus ?? "active", 40).toLowerCase();
28460
+ const reviewStatus = preview3(record.reviewStatus ?? record.review_status ?? "approved", 40).toLowerCase().replace("canonical", "approved");
28461
+ if (status && status !== "active") {
28462
+ return void 0;
28463
+ }
28464
+ if (reviewStatus && reviewStatus !== "approved") {
28465
+ return void 0;
28466
+ }
28467
+ const anchors = extractPolicyAnchors(content);
28468
+ if (anchors.length === 0) {
28469
+ return void 0;
28470
+ }
28471
+ const id = preview3(record.memory_id ?? record.id ?? record.source_id ?? anchors[0], 80);
28472
+ const confidence = numberValue8(record.confidence) ?? numberValue8(record.score) ?? 0.8;
28473
+ return {
28474
+ id,
28475
+ title: preview3(record.title ?? content, 120),
28476
+ scope: inferProjectPolicyScope(content, anchors),
28477
+ strength: inferProjectPolicyStrength(content),
28478
+ confidence,
28479
+ source: {
28480
+ kind: "decision_memory",
28481
+ ref: id.startsWith("memory:") ? id : `memory:${id}`,
28482
+ reviewStatus: "approved"
28483
+ },
28484
+ anchors,
28485
+ requirement: content.slice(0, 240),
28486
+ forbiddenActions: extractForbiddenActions(content)
28487
+ };
28488
+ }
28489
+ function extractPolicyAnchors(text) {
28490
+ const normalized = text.toLowerCase();
28491
+ return [
28492
+ ...Array.from(normalized.matchAll(/`([^`]{3,120})`/g), (match) => match[1]),
28493
+ ...Array.from(
28494
+ normalized.matchAll(/\b(?:apps|packages|deploy|docs|scripts|oss)\/[a-z0-9_[\]./-]+/g),
28495
+ (match) => match[0]
28496
+ ),
28497
+ ...["auth", "billing", "schema", "migration", "deploy", "package", "memory", "routing"].filter(
28498
+ (term) => normalized.includes(term)
28499
+ )
28500
+ ].filter((value, index, values) => value && values.indexOf(value) === index);
28501
+ }
28502
+ function inferProjectPolicyScope(text, anchors) {
28503
+ const haystack = `${text}
28504
+ ${anchors.join("\n")}`.toLowerCase();
28505
+ if (/\b(auth|oauth|session|permission|token|secret)\b/.test(haystack)) return "auth";
28506
+ if (/\b(billing|stripe|subscription|checkout|entitlement|quota)\b/.test(haystack)) {
28507
+ return "billing";
28508
+ }
28509
+ if (/\b(schema|migration|prisma|database)\b/.test(haystack)) return "schema";
28510
+ if (/\b(deploy|production|pre-deploy|zero-downtime)\b/.test(haystack)) return "deploy";
28511
+ if (/\b(package|npm|pypi|npx|publish|pack smoke)\b/.test(haystack)) return "package_surface";
28512
+ if (/\b(memory|reviewed memory|decision memory)\b/.test(haystack)) return "memory";
28513
+ if (/\b(routing|orchestrator|worker)\b/.test(haystack)) return "routing";
28514
+ return "custom";
28515
+ }
28516
+ function inferProjectPolicyStrength(text) {
28517
+ const normalized = text.toLowerCase();
28518
+ if (/\b(never|must not|do not|forbid|forbidden|disallow|block)\b/.test(normalized)) {
28519
+ return "blocking";
28520
+ }
28521
+ if (/\b(require review|requires review|approval|required|must)\b/.test(normalized)) {
28522
+ return "review_required";
28523
+ }
28524
+ return "advisory";
28525
+ }
28526
+ function extractForbiddenActions(text) {
28527
+ const normalized = text.toLowerCase();
28528
+ return ["bypass", "skip", "remove", "delete", "disable", "drop", "force"].filter(
28529
+ (term) => normalized.includes(term)
28530
+ );
28531
+ }
28152
28532
  function buildSuggestedCommands2(args) {
28153
28533
  const commands = [
28154
28534
  "snipara-companion team-sync what-changed",
@@ -28241,6 +28621,16 @@ async function buildProjectIntelligenceBrief(options) {
28241
28621
  });
28242
28622
  }
28243
28623
  }
28624
+ const projectPolicyRules = extractProjectPolicyRulesFromResumeContext(brief.resumeContext);
28625
+ if (projectPolicyRules.length > 0) {
28626
+ brief.projectPolicyDecision = evaluateProjectPolicyDecision({
28627
+ action: {
28628
+ summary: options.diffSummary ?? options.task ?? "Project Intelligence brief",
28629
+ changedFiles
28630
+ },
28631
+ rules: projectPolicyRules
28632
+ });
28633
+ }
28244
28634
  brief.verificationPlan = buildVerificationPlan({
28245
28635
  task: options.task,
28246
28636
  changedFiles,
@@ -28406,6 +28796,19 @@ async function projectIntelligenceBriefCommand(options) {
28406
28796
  console.log(import_chalk9.default.bold("Memory Authority And Health"));
28407
28797
  printMemoryHealth2(brief.memoryHealth);
28408
28798
  console.log("");
28799
+ if (brief.projectPolicyDecision) {
28800
+ console.log(import_chalk9.default.bold("Project Policy"));
28801
+ console.log(
28802
+ `Verdict: ${brief.projectPolicyDecision.verdict} (${Math.round(
28803
+ brief.projectPolicyDecision.confidence * 100
28804
+ )}%)`
28805
+ );
28806
+ if (brief.projectPolicyDecision.requiredActions.length > 0) {
28807
+ console.log(`Required: ${brief.projectPolicyDecision.requiredActions[0]}`);
28808
+ }
28809
+ console.log(`Receipt: ${brief.projectPolicyDecision.receipt.receiptId}`);
28810
+ console.log("");
28811
+ }
28409
28812
  console.log(import_chalk9.default.bold("Code Impact"));
28410
28813
  if (brief.codeImpactSourceSelection) {
28411
28814
  console.log(
@@ -29113,9 +29516,46 @@ function gateSummary(gates) {
29113
29516
  function packageReviewCommand2(input) {
29114
29517
  return input.packageReview?.command ?? "npm view snipara-companion version bin dist-tags --json";
29115
29518
  }
29519
+ function gateSurfaceFromPolicyScope(scope) {
29520
+ if (scope === "release" || scope === "schema" || scope === "auth" || scope === "billing" || scope === "deploy" || scope === "package_surface") {
29521
+ return scope;
29522
+ }
29523
+ return "project_policy";
29524
+ }
29116
29525
  function evaluateProjectPolicyGates(input) {
29117
29526
  const changedFiles = normalizeChangedFiles(input.changedFiles);
29118
29527
  const gates = [];
29528
+ const projectPolicyDecision = input.projectPolicy?.decision ?? (input.projectPolicy?.rules?.length ? evaluateProjectPolicyDecision({
29529
+ action: {
29530
+ summary: [input.task, input.diffSummary].filter(Boolean).join("\n"),
29531
+ changedFiles
29532
+ },
29533
+ rules: input.projectPolicy.rules
29534
+ }) : void 0);
29535
+ if (projectPolicyDecision && projectPolicyDecision.verdict !== "allow") {
29536
+ const strongestRule = projectPolicyDecision.matchedRules[0];
29537
+ gates.push(
29538
+ gate({
29539
+ id: "policy:project:decision-consistency",
29540
+ surface: strongestRule ? gateSurfaceFromPolicyScope(strongestRule.scope) : "project_policy",
29541
+ severity: projectPolicyVerdictToGateSeverity(projectPolicyDecision.verdict),
29542
+ title: "Project Policy decision consistency",
29543
+ rationale: "Approved project policy and decision memories can require review or block when the proposed action contradicts high-confidence constraints.",
29544
+ evidence: [
29545
+ `verdict ${projectPolicyDecision.verdict}`,
29546
+ `receipt ${projectPolicyDecision.receipt.receiptId}`,
29547
+ ...projectPolicyDecision.matchedRules.map((rule) => `${rule.source.ref}: ${rule.title}`)
29548
+ ],
29549
+ requiredActions: projectPolicyDecision.requiredActions.length > 0 ? projectPolicyDecision.requiredActions : ["Review the matching project policy decision before proceeding."],
29550
+ suggestedCommands: projectPolicyDecision.verdict === "block" ? ["snipara-companion decisions request --help"] : ["snipara-companion memory reviews --emit-decisions"],
29551
+ sampleGate: explicitContractSampleGate(
29552
+ "Project Policy gates are driven by reviewed decisions and explicit receipts, not statistical outcome thresholds."
29553
+ ),
29554
+ source: "project_policy_decision",
29555
+ reasonCodes: projectPolicyDecision.reasonCodes
29556
+ })
29557
+ );
29558
+ }
29119
29559
  if (input.release) {
29120
29560
  gates.push(
29121
29561
  gate({
@@ -29374,6 +29814,7 @@ function evaluateProjectPolicyGates(input) {
29374
29814
  release: Boolean(input.release),
29375
29815
  summary,
29376
29816
  gates,
29817
+ ...projectPolicyDecision ? { projectPolicyDecision } : {},
29377
29818
  suggestedCommands: suggestedCommands3
29378
29819
  };
29379
29820
  }
@@ -29878,7 +30319,10 @@ async function buildProjectIntelligenceRun(options) {
29878
30319
  skipPackageReview: options.skipPackageReview,
29879
30320
  guard,
29880
30321
  packageReview,
29881
- judgmentCard
30322
+ judgmentCard,
30323
+ projectPolicy: brief.projectPolicyDecision ? {
30324
+ decision: brief.projectPolicyDecision
30325
+ } : void 0
29882
30326
  });
29883
30327
  const verificationEvidence = verificationEvidenceFromRun({
29884
30328
  guard,
@@ -1037,6 +1037,12 @@ checkouts use hosted graph impact. It prints continuity signals, memory health,
1037
1037
  risk and verification hints, degraded surfaces, and the Judgment Card's
1038
1038
  weighted readiness, evidence, and required actions.
1039
1039
 
1040
+ When hosted resume context includes approved decision memories that match the
1041
+ task or changed files, the brief also emits a Project Policy decision receipt
1042
+ with an `allow`, `warn`, `require_review`, or `block` verdict. This is
1043
+ conservative by design: blocks require high-confidence reviewed policy plus a
1044
+ matching forbidden action, and no new default MCP tool is exposed.
1045
+
1040
1046
  Use `reality-check` or `intelligence reality-check` when a local hook, agent, or
1041
1047
  CI adapter needs the contradiction-to-reality gate without a full hosted brief:
1042
1048
 
@@ -1146,7 +1152,7 @@ Semantics:
1146
1152
  - `snipara-companion workflow timeline` = append-only activity timeline from `.snipara/activity/timeline.jsonl`, including workflow, Producer Loop, Decision Request, and Team Sync events emitted by Companion commands; add `--export md` for a redacted Markdown artifact
1147
1153
  - `snipara-companion workflow session` = writes and prints Session Snapshot V0 at `.snipara/activity/session.json` with latest activity, risk, touched files, next action, advisory Intent Detection V0 intent/confidence/signals/suggested mode, workflow/session counts, Producer Loop calibration, decision counts, Team Sync counts, and `hardRoutingAllowed=false`
1148
1154
  - `snipara-companion handoff` = top-level agent-ready Markdown/JSON handoff artifact plus the same local/hosted Team Sync handoff persistence
1149
- - `snipara-companion intelligence brief` = one local Project Intelligence brief that combines local Session Snapshot, hosted resume context, memory health, and code impact for a task
1155
+ - `snipara-companion intelligence brief` = one local Project Intelligence brief that combines local Session Snapshot, hosted resume context, memory health, Project Policy decision receipts, and code impact for a task
1150
1156
  - `snipara-companion intelligence reality-check` = Project Intelligence namespace alias for the same local Reality Check gate
1151
1157
  - `snipara-companion intelligence ledger-export` = structured redacted Coding Intelligence Ledger JSON for replay, review, and commercial proof assets without dumping raw transcripts
1152
1158
  - `snipara-companion run` = production Project Intelligence flow that combines the brief, guard action cards, package review, verification hints, and a final weighted Judgment Card
@@ -1172,7 +1178,7 @@ Semantics:
1172
1178
  - `snipara-companion team-sync resume` = reloads local carryover plus the hosted latest handoff and checkpoint-aware resume guidance when available
1173
1179
  - `snipara-companion final-commit` / `workflow final-commit` = final hosted commit for the managed workflow
1174
1180
  - `snipara-companion code callers/imports/neighbors/shortest-path/impact` = primary code graph surface for agents with shell access. These commands use `--source auto` by default; clean configured checkouts use hosted MCP, dirty/ahead worktrees use the local overlay, and every response reports `sourceSelection` plus agent guidance.
1175
- - `snipara-companion code symbol-card` = direct paid Context `snipara_code_symbol_card` for an important symbol before editing, with an agent guidance summary before raw JSON
1181
+ - `snipara-companion code symbol-card` = direct `snipara_code_symbol_card` for an important symbol before editing, with an agent guidance summary before raw JSON
1176
1182
  - `snipara-companion code impact --source hosted|local` = optional source override for debugging; normal agent instructions should leave `--source auto` in place. Hosted `snipara_code_impact` is the fallback when companion is unavailable or the canonical graph check after push/reindex.
1177
1183
  - `snipara-companion code local impact` = explicit repository-local file-level import impact from the local code overlay; keep this for power-user/debug workflows, and use `workflow impact-gate` when the file set should come from unpushed workflow commits
1178
1184
  - `snipara-companion doctor` = local readiness check for companion version skew, Snipara auth, deterministic hosted tool catalog access, Snipara Sandbox, Snipara Sandbox MCP wiring, provider keys, and Docker
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.2.8",
3
+ "version": "3.2.10",
4
4
  "description": "Local-first CLI that asks your repo what breaks before an AI coding agent edits it.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {