pi-magi-theme 0.1.1 → 0.1.3

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 CHANGED
@@ -52,15 +52,27 @@ Every symbol stands for something real the agent is doing.
52
52
  | the seven seals | the end of an age | **context window usage**, one seal per seventh | panel |
53
53
  | breaking the seals → seventh seal opened | apocalypse and renewal | **context compaction** running → done | panel + footer |
54
54
 
55
+ ## The seventh seal: smart compaction
56
+
57
+ The theme does not compact anything itself: it shows who does. For better compaction install [pi-smart-compact](https://www.npmjs.com/package/pi-smart-compact):
58
+
59
+ ```bash
60
+ pi install npm:pi-smart-compact
61
+ ```
62
+
63
+ It extracts files, errors, decisions and open loops locally (no LLM calls), then synthesizes and verifies the summary. Point its `summaryModel` at a local model to keep compaction free. When it is installed, the seals name it while they break (`✶ BREAKING THE SEALS · smart-compact · 4s`) and the seventh seal reports who actually produced the summary: `smart-compact`, or `pi native` if it fell back to pi's own compactor.
64
+
55
65
  ## The council
56
66
 
57
67
  `/magi <question>` asks three models in parallel, each with its own nature, then shows the votes and a majority verdict:
58
68
 
59
69
  | Unit | Nature | Looks at |
60
70
  |------|--------|----------|
61
- | MELCHIOR | PRAGMATIST | simplest working solution, effort vs value, reuse, YAGNI |
62
- | BALTHASAR | GUARDIAN | failure modes, security, operability, maintainability |
63
- | CASPAR | VISIONARY | reframing the problem, alternatives, DX, evolution |
71
+ | MELCHIOR | PRAGMATIST | what solves the problem, the simplest path, effort vs value, what already exists (in software: reuse, YAGNI, shipping) |
72
+ | BALTHASAR | GUARDIAN | what can go wrong and for whom, reversibility, hidden costs (in software: failure modes, security, operability) |
73
+ | CASPAR | VISIONARY | whether the question is framed right, alternatives, people's experience, long-term direction (in software: design, DX, evolution) |
74
+
75
+ Each nature is a lens, not a specialty, so the council answers any question, not only software ones. Every MAGI first answers the question, then judges it through its lens, naming concrete tools, numbers and scenarios from your question instead of generic advice. Votes: **APPROVE** = go ahead or clear recommendation; **CONDITIONAL** = only if the named conditions hold, or when information is missing (it says what it needs); **REJECT** = a concrete problem, with what to do instead. A MAGI never rejects because a topic is outside its nature. Answers come back in the language of your question.
64
76
 
65
77
  Each MAGI also gets the recent conversation as context. Full opinions are added to the chat (not sent to the agent).
66
78
 
@@ -200,7 +200,6 @@ const state = {
200
200
  phaseSince: Date.now(),
201
201
  toolName: "",
202
202
  turns: 0,
203
- tools: 0,
204
203
  toolOk: 0, // CHESED
205
204
  toolFail: 0, // GEBURAH
206
205
  lastFailAt: 0,
@@ -209,6 +208,7 @@ const state = {
209
208
  lastRunMs: 0,
210
209
  compacting: false,
211
210
  compactSince: 0,
211
+ compactBy: "", // who opens the seals: "smart-compact" (pi-smart-compact package) or "pi native"
212
212
  rebornAt: 0,
213
213
  };
214
214
 
@@ -646,7 +646,9 @@ class MagiPanel implements Component {
646
646
  if (state.compacting || now - state.rebornAt < REBIRTH_MS) {
647
647
  // the seals break while the context is compacted; then the world is remade
648
648
  const reborn = !state.compacting;
649
- title = reborn ? "SEVENTH SEAL OPENED · REBORN" : `BREAKING THE SEALS ${secsSince(state.compactSince, now)}s`;
649
+ title = reborn
650
+ ? "SEVENTH SEAL OPENED · REBORN"
651
+ : `SEALS · ${state.compactBy || "compacting"} ${secsSince(state.compactSince, now)}s`;
650
652
  link = reborn ? "success" : f % 4 < 2 ? "warning" : "error";
651
653
  hub = reborn ? "═MAGI═" : ["─SEAL─", "━SEAL━"][f % 2]!;
652
654
  units = names.map((name, i) =>
@@ -748,7 +750,7 @@ class MagiPanel implements Component {
748
750
  const stats = tokenStats();
749
751
  out.push(
750
752
  this.frameLine(
751
- ` ${th.fg("dim", "TURNS".padEnd(9))}${th.fg("text", String(state.turns).padEnd(6))}${th.fg("dim", "TOOLS ")}${th.fg("text", String(state.tools))}`,
753
+ ` ${th.fg("dim", "TURNS".padEnd(9))}${th.fg("text", String(state.turns))}`,
752
754
  inner,
753
755
  ),
754
756
  );
@@ -771,14 +773,13 @@ class MagiPanel implements Component {
771
773
  inner,
772
774
  ),
773
775
  );
774
- if (state.toolOk + state.toolFail) {
775
- out.push(
776
- this.frameLine(
777
- ` ${th.fg("dim", "CHESED".padEnd(9))}${th.fg("success", `✓${state.toolOk}`.padEnd(6))}${th.fg("dim", "GEBURAH ")}${th.fg(state.toolFail ? "error" : "dim", `✗${state.toolFail}`)}`,
778
- inner,
779
- ),
780
- );
781
- }
776
+ // TOOLS: CHESED (mercy) = succeeded, GEBURAH (severity) = failed
777
+ out.push(
778
+ this.frameLine(
779
+ ` ${th.fg("dim", "TOOLS".padEnd(7))}${th.fg("success", `✓${state.toolOk}`)}${th.fg("dim", " CHESED ")}${th.fg(state.toolFail ? "error" : "dim", `✗${state.toolFail}`)}${th.fg("dim", " GEBURAH")}`,
780
+ inner,
781
+ ),
782
+ );
782
783
 
783
784
  // SEALS: the context window, one seal per seventh
784
785
  const usage = liveCtx?.getContextUsage?.();
@@ -856,41 +857,57 @@ function voteTone(v: string | null | undefined): "success" | "warning" | "error"
856
857
  return v === "APPROVE" ? "success" : v === "CONDITIONAL" ? "warning" : "error";
857
858
  }
858
859
 
859
- /** Three minds, three useful engineering viewpoints. */
860
+ /**
861
+ * Three minds, three lenses. A lens works on any subject (a nature, not a specialty), so no MAGI
862
+ * rejects a question just because it is not "its" topic; software is where each lens gets sharpest.
863
+ */
860
864
  const MAGI = [
861
865
  {
862
866
  unit: "MELCHIOR",
863
867
  nature: "PRAGMATIST",
864
868
  persona:
865
- "You are MELCHIOR, the pragmatist of the MAGI council: a senior engineer. Judge by: does it solve the actual problem, " +
866
- "the simplest solution that works, effort versus value, reuse of what already exists (stdlib, current stack, existing code), " +
867
- "time to ship. Call out over-engineering, speculative abstractions and unnecessary dependencies.",
869
+ "You are MELCHIOR, the pragmatist of the MAGI council. Your lens works on any subject: what actually solves the problem " +
870
+ "at hand, the simplest path that works, effort and cost versus value, what can be done now with what already exists, " +
871
+ "and what is unnecessary. In software this means: reuse the current stack and existing code, avoid over-engineering, " +
872
+ "speculative abstractions and extra dependencies, ship sooner.",
868
873
  },
869
874
  {
870
875
  unit: "BALTHASAR",
871
876
  nature: "GUARDIAN",
872
877
  persona:
873
- "You are BALTHASAR, the guardian of the MAGI council: a protective reliability and security architect. Judge by: failure modes, " +
874
- "security, data safety, operability (monitoring, rollback, being paged at 3am), maintainability for the team, " +
875
- "backward compatibility and hidden long-term costs. Say what will break and how to prevent it.",
878
+ "You are BALTHASAR, the guardian of the MAGI council. Your lens works on any subject: what can go wrong, how badly and " +
879
+ "for whom, whether the choice can be undone, which safety nets are missing, the hidden and long-term costs, and what " +
880
+ "must be protected. In software this means: failure modes, security, data safety, operability (monitoring, rollback, " +
881
+ "being paged at 3am), maintainability and backward compatibility.",
876
882
  },
877
883
  {
878
884
  unit: "CASPAR",
879
885
  nature: "VISIONARY",
880
886
  persona:
881
- "You are CASPAR, the visionary of the MAGI council: a creative, lateral-thinking architect. Judge by: is there a better framing of " +
882
- "the problem, more elegant or unconventional alternatives, developer and user experience, and how the design will evolve " +
883
- "over the next year. Always propose at least one alternative the other two would likely miss.",
887
+ "You are CASPAR, the visionary of the MAGI council. Your lens works on any subject: whether the question is framed right, " +
888
+ "better or unconventional alternatives, the experience of the people involved, and where the choice leads over time " +
889
+ "and what it unlocks. In software this means: design alternatives, developer and user experience, how the system " +
890
+ "evolves over the next year. Always name at least one concrete alternative the other two would likely miss.",
884
891
  },
885
892
  ] as const satisfies readonly { unit: MagiUnit; nature: string; persona: string }[];
886
893
 
887
- const MAGI_RULES = `You are one of the three MAGI deliberating on a question from a software engineer (coding, software architecture, infrastructure).
888
- Answer strictly from your own nature: the other two MAGI cover the other viewpoints.
889
- Reply in the same language as the question.
894
+ const MAGI_RULES = `You are one of the three MAGI. The council answers whatever the user asks: mostly software engineering, but not only.
895
+
896
+ Your nature is a lens, not a specialty, so competence is never a reason to reject. Whatever the subject, first work out the best answer to the question itself, then judge it through your lens. The other two MAGI cover the other lenses: stay in yours.
897
+
898
+ Be specific to this question. Every bullet must name something concrete from the question or the conversation: a tool, a number, a scenario, a step, a cost. Never write advice that would fit any question, such as "consider the trade-offs", "it depends", "ensure security" or "test properly".
899
+
900
+ How to vote:
901
+ - APPROVE: you would go ahead as asked, or you have a clear recommendation.
902
+ - CONDITIONAL: you would go ahead only if specific conditions hold, and you name them. When information is missing, vote CONDITIONAL and say exactly what you need to know and how each answer changes your recommendation.
903
+ - REJECT: your lens finds a concrete problem that makes the proposal a bad idea, and you say what to do instead. Never reject because the topic is outside software or outside your nature, or because details are missing.
904
+ For open questions (which one, how to), give your recommendation and vote on how confident you are in it.
905
+
906
+ Write in the language of the "Question for the MAGI", even though these instructions and the conversation may be in English.
890
907
  Output format, no preamble:
891
908
  VOTE: APPROVE | CONDITIONAL | REJECT
892
- - then at most 5 short bullet points (about 120 words total)
893
- For CONDITIONAL, the bullets must state the conditions. If the question is open-ended rather than yes/no, give your recommendation and vote on the direction the question implies.`;
909
+ - first bullet: your direct answer or recommendation
910
+ - then up to 4 bullets from your lens: 5 bullets at most in total, about 120 words`;
894
911
 
895
912
  interface MagiOpinion {
896
913
  unit: string;
@@ -964,6 +981,40 @@ function conversationExcerpt(ctx: ExtensionContext, maxChars = 6000): string {
964
981
  return joined.length > maxChars ? "…" + joined.slice(-maxChars) : joined;
965
982
  }
966
983
 
984
+ /** Common function words per language, used to name the reply language explicitly. */
985
+ const LANGUAGE_HINTS: readonly [string, readonly string[]][] = [
986
+ ["Italian", ["il", "lo", "la", "gli", "di", "che", "per", "non", "una", "con", "sono", "come", "perché", "è", "dovrei", "meglio", "mettiamo", "questo", "quale"]],
987
+ ["Spanish", ["el", "los", "las", "que", "para", "por", "es", "cómo", "debería", "mejor", "este", "cuál"]],
988
+ ["French", ["le", "les", "des", "est", "pour", "avec", "dois", "comment", "mieux", "ce", "quel"]],
989
+ ["German", ["der", "die", "das", "und", "ist", "nicht", "für", "mit", "ich", "soll", "wie", "besser"]],
990
+ ["English", ["the", "is", "should", "we", "for", "with", "and", "to", "of", "how", "which", "better"]],
991
+ ];
992
+
993
+ /**
994
+ * Guesses the question's language from function words; undefined when unsure.
995
+ * ponytail: stopword heuristic for five languages, swap in a real detector if other languages matter.
996
+ */
997
+ function guessLanguage(text: string): string | undefined {
998
+ const words = text.toLowerCase().match(/\p{L}+/gu) ?? [];
999
+ const scores = LANGUAGE_HINTS.map(([lang, hints]) => [lang, words.filter((w) => hints.includes(w)).length] as const).sort(
1000
+ (a, b) => b[1] - a[1],
1001
+ );
1002
+ const [best, second] = scores;
1003
+ return best![1] >= 2 && best![1] > second![1] ? best![0] : undefined;
1004
+ }
1005
+
1006
+ /** The user message each MAGI receives. The language reminder sits after the question, where the model reads it last. */
1007
+ function councilPrompt(project: string, excerpt: string, question: string): string {
1008
+ const lang = guessLanguage(question);
1009
+ const reminder = lang
1010
+ ? `(Write your whole answer in ${lang}, even if technical terms in the question are English.)`
1011
+ : "(Write your whole answer in the language of this question, even if technical terms in it are English.)";
1012
+ return (
1013
+ `Project: ${project}\n\nRecent conversation (context only, may be empty):\n<conversation>\n${excerpt}\n</conversation>\n\n` +
1014
+ `Question for the MAGI:\n${question}\n\n${reminder}`
1015
+ );
1016
+ }
1017
+
967
1018
  function resolveModel(ctx: ExtensionContext, ref?: string): Model<any> | undefined {
968
1019
  if (!ref) return ctx.model;
969
1020
  const slash = ref.indexOf("/");
@@ -1121,7 +1172,9 @@ function footerLeft(th: Theme, now = Date.now()): string {
1121
1172
  th.fg("warning", "◉".repeat(broken)) +
1122
1173
  dim("○".repeat(7 - broken)) +
1123
1174
  th.fg("warning", " BREAKING THE SEALS") +
1124
- dim(` · compacting context ${secsSince(state.compactSince, now)}s`)
1175
+ dim(" · ") +
1176
+ th.fg("text", state.compactBy || "compacting") +
1177
+ dim(` · ${secsSince(state.compactSince, now)}s`)
1125
1178
  );
1126
1179
  }
1127
1180
  if (now - state.rebornAt < REBIRTH_MS) {
@@ -1129,7 +1182,7 @@ function footerLeft(th: Theme, now = Date.now()): string {
1129
1182
  th.fg("success", "✶ ") +
1130
1183
  renderPath(th, lights(() => (step % 2 ? "on" : "hot"))) +
1131
1184
  th.fg("success", " SEVENTH SEAL OPENED") +
1132
- dim(" · context compacted, the world is remade")
1185
+ dim(` · context compacted by ${state.compactBy || "pi"}, the world is remade`)
1133
1186
  );
1134
1187
  }
1135
1188
  if (state.phase === "tool" || now - state.lastFailAt < FAIL_FLASH_MS) {
@@ -1397,7 +1450,6 @@ export default function (pi: ExtensionAPI) {
1397
1450
  liveCtx = ctx;
1398
1451
  setPhase("tool");
1399
1452
  state.toolName = event.toolName ?? "";
1400
- state.tools++;
1401
1453
  repaint();
1402
1454
  });
1403
1455
 
@@ -1424,15 +1476,21 @@ export default function (pi: ExtensionAPI) {
1424
1476
  });
1425
1477
 
1426
1478
  // The seven seals: compaction breaks them, and the context is reborn.
1479
+ // When pi-smart-compact is installed it owns the compaction; this theme only watches and names it.
1480
+ const hasSmartCompact = () => pi.getCommands().some((c) => c.name.replace(/^\//, "") === "smart-compact");
1481
+
1427
1482
  pi.on("session_before_compact", async () => {
1428
1483
  state.compacting = true;
1429
1484
  state.compactSince = Date.now();
1485
+ state.compactBy = hasSmartCompact() ? "smart-compact" : "pi native";
1430
1486
  repaint();
1431
1487
  });
1432
1488
 
1433
- pi.on("session_compact", async () => {
1489
+ pi.on("session_compact", async (event) => {
1434
1490
  state.compacting = false;
1435
1491
  state.rebornAt = Date.now();
1492
+ // fromExtension: an extension supplied the summary; otherwise pi's own compactor did (e.g. smart-compact fell back)
1493
+ state.compactBy = event.fromExtension ? (hasSmartCompact() ? "smart-compact" : "extension") : "pi native";
1436
1494
  repaint();
1437
1495
  });
1438
1496
 
@@ -1452,9 +1510,7 @@ export default function (pi: ExtensionAPI) {
1452
1510
 
1453
1511
  const cfg = loadMagiConfig();
1454
1512
  const project = (ctx.cwd ?? "").split("/").filter(Boolean).pop() ?? "";
1455
- const prompt =
1456
- `Project: ${project}\n\nRecent conversation (context only, may be empty):\n<conversation>\n${conversationExcerpt(ctx)}\n</conversation>\n\n` +
1457
- `Question for the MAGI:\n${question}`;
1513
+ const prompt = councilPrompt(project, conversationExcerpt(ctx), question);
1458
1514
 
1459
1515
  const controller = new AbortController();
1460
1516
  const opinions: (MagiOpinion | undefined)[] = MAGI.map(() => undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-magi-theme",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "MAGI theme + extension for pi: Tree of Life header, three-model /magi council, golem tool animations, seven-seal context gauge, llama-swap telemetry",
5
5
  "keywords": [
6
6
  "pi-package",