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 +15 -3
- package/extensions/magi/index.ts +89 -33
- package/package.json +1 -1
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 |
|
|
62
|
-
| BALTHASAR | GUARDIAN | failure modes, security, operability
|
|
63
|
-
| CASPAR | VISIONARY |
|
|
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
|
|
package/extensions/magi/index.ts
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
/**
|
|
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
|
|
866
|
-
"the simplest
|
|
867
|
-
"
|
|
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
|
|
874
|
-
"
|
|
875
|
-
"
|
|
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
|
|
882
|
-
"
|
|
883
|
-
"
|
|
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
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
-
|
|
893
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|