klypix-mcp 1.79.0 β†’ 1.80.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.79.0",
3
+ "version": "1.80.0",
4
4
  "description": "Active state management for multi-agent coding: a shared, versioned project brain over MCP.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@ import {
32
32
  brainLensData, lensToMarkdown, deathDateOfCard,
33
33
  statusContextToMarkdown, findFulfillmentCandidates,
34
34
  splitQueryTokens, scoreCardsAgainstQuery, correctionOverlaysFor,
35
- isFastDecayCard, isUnresolvedOpenCard, DECAY_STALE_MS, formatDecayAge,
35
+ isFastDecayCard, isUnresolvedOpenCard, isSkillCard, DECAY_STALE_MS, formatDecayAge,
36
36
  isPlanCard, planFulfillmentFor, PLAN_PAIR_SIM_BRAIN, isAgconfTwinId,
37
37
  readPendingShips, clearPendingShips, pendingShipCards, formatCaptureReceipts,
38
38
  } from './klypix-format.mjs';
@@ -494,9 +494,29 @@ export async function opBrainTaskContext({
494
494
  };
495
495
  });
496
496
  const maxChars = Math.max(800, Math.min(5000, Number(budgetChars) || 2800));
497
+ // Standing rules (2026-08-24 audit): πŸ› οΈ skills are "apply every session",
498
+ // but the ranker's `score <= 0` gate drops a zero-overlap rule BEFORE its +1
499
+ // skill boost applies β€” so the capsule delivered task hits and zero standing
500
+ // rules, and the rule that would have warned the founder about a same-day
501
+ // billing trap never reached any session. This block is UNCONDITIONAL:
502
+ // relevance-ordered when the ranker scored a rule, newest-first otherwise,
503
+ // deduped against the hit list, and prepended so the maxChars tail-cut can
504
+ // never be the reason a rule silently vanished.
505
+ const skillPool = struct.cards.filter((c) => c.type !== 'container'
506
+ && !/^archive$/i.test(c.area || '') && isSkillCard(c));
507
+ const scoreById = new Map(candidatePool.map((hit) => [hit.card.id, hit.score]));
508
+ const standing = skillPool
509
+ .filter((c) => !hitIds.has(c.id))
510
+ .sort((a, b) => (scoreById.get(b.id) || 0) - (scoreById.get(a.id) || 0)
511
+ || (b.createdAt || 0) - (a.createdAt || 0))
512
+ .slice(0, 3);
497
513
  const lines = [
498
514
  `## Compact task context (${entries.length} relevant brain card${entries.length === 1 ? '' : 's'} Β· lexical-fast)`,
499
515
  ];
516
+ if (standing.length) {
517
+ lines.push(`### πŸ› οΈ Standing rules (${skillPool.length} in the brain β€” apply always; full set in the brief)`);
518
+ for (const c of standing) lines.push(`- [${flat(c.area) || 'Notes'}] ${clip(c.text, 220)}`);
519
+ }
500
520
  if (!entries.length) {
501
521
  lines.push('No high-confidence task-specific card matched. Continue from repository evidence; use brain_ask only if deeper project history is needed.');
502
522
  } else {
@@ -521,6 +541,9 @@ export async function opBrainTaskContext({
521
541
  context: {
522
542
  mode: 'lexical-fast',
523
543
  hits: entries,
544
+ ...(standing.length ? {
545
+ standingRules: standing.map((c) => ({ id: c.id, area: flat(c.area) || 'Notes', text: clip(c.text, 220) })),
546
+ } : {}),
524
547
  sufficient: entries.length > 0,
525
548
  durationMs,
526
549
  brain: path.basename(t.file),
@@ -1630,8 +1630,11 @@ export function structToBrief(struct, { recentDays = 14, maxRecent = 40, maxMile
1630
1630
  // πŸ› οΈ Skills β€” reusable how-tos / gotchas / procedures (the '+' marker).
1631
1631
  // Standing reference, NOT a point-in-time event: always shown, never
1632
1632
  // recency-decayed, and excluded from open/milestones/recent so a skill never
1633
- // masquerades as (or ages out like) a decision.
1634
- const skills = rest.filter(isSkillCard);
1633
+ // masquerades as (or ages out like) a decision. Newest first: the render
1634
+ // below can only show maxSkills of them, and an unsorted slice pinned the
1635
+ // OLDEST rules forever while every rule learned since reached no session
1636
+ // (2026-08-24 audit β€” the founder's same-day billing rule was invisible).
1637
+ const skills = rest.filter(isSkillCard).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
1635
1638
  const open = rest.filter(isOpenCard);
1636
1639
  const miles = rest.filter(isMilestoneCard);
1637
1640
  const plain = rest.filter(c => !open.includes(c) && !miles.includes(c) && !skills.includes(c));
@@ -1916,6 +1919,32 @@ export function structToUltraBrief(struct, { freshness = null, briefPath = '.cla
1916
1919
  // line that falls off the bottom of the preview-sized budget.
1917
1920
  const overdueById = findOverdueOpenCards(struct).byId;
1918
1921
  const openSorted = open.slice().sort((a, b) => (overdueById.has(b.id) ? 1 : 0) - (overdueById.has(a.id) ? 1 : 0));
1922
+ // πŸ› οΈ Standing rules tier (2026-08-24): the ultra brief used to render
1923
+ // skills as a COUNT in the tail β€” so the one surface every session reads
1924
+ // carried zero of the rules that are supposed to "fire every session".
1925
+ // Newest first (the most recently learned trap is the likeliest live one),
1926
+ // BEFORE the open list: opens are greedy to the budget floor, so anything
1927
+ // placed after them never lands. Small cap β€” this is a reminder tier, the
1928
+ // full set stays in the brief file. BUDGET FENCE (adversarial review of
1929
+ // 10f43e1, reproduced live at the 1800-char SessionStart default): this
1930
+ // tier must never be the reason a ⏰ OVERDUE line fell off β€” price the
1931
+ // opens header, every overdue line, and the overflow line FIRST, and hold
1932
+ // that budget back from the skills tier. Rules yield to deadlines.
1933
+ if (skills.length) {
1934
+ const openHeader = `## Open questions & goals (${open.length}${overdueById.size ? `, ${overdueById.size} ⏰ overdue` : ''})`;
1935
+ const overdueLines = openSorted.filter(c => overdueById.has(c.id)).map(c => `- ⏰ OVERDUE ${fr(c)}${head(c)}`);
1936
+ const reserve = open.length
1937
+ ? ['', openHeader, ...overdueLines, `- …and ${open.length} more β€” in the full brief.`]
1938
+ .reduce((s, l) => s + l.length + 1, 0)
1939
+ : 0;
1940
+ const pushIfFenced = (l) => (used + l.length + 1 > budget - reserve) ? false : (push(l), true);
1941
+ if (pushIfFenced('') && pushIfFenced(`## πŸ› οΈ Standing rules (${skills.length} β€” newest first, apply always)`)) {
1942
+ const newest = skills.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
1943
+ let shown = 0;
1944
+ for (const c of newest.slice(0, 5)) { if (!pushIfFenced(`- ${fr(c)}${head(c)}`)) break; shown++; }
1945
+ if (shown < skills.length) pushIfFenced(`- …and ${skills.length - shown} more standing rule(s) β€” in the full brief.`);
1946
+ }
1947
+ }
1919
1948
  if (open.length && pushIf('') && pushIf(`## Open questions & goals (${open.length}${overdueById.size ? `, ${overdueById.size} ⏰ overdue` : ''})`)) {
1920
1949
  let shown = 0;
1921
1950
  for (const c of openSorted) { if (!pushIf(`- ${overdueById.has(c.id) ? '⏰ OVERDUE ' : ''}${fr(c)}${head(c)}`)) break; shown++; }