@pharaoh-so/mcp 0.3.54 → 0.3.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.mcp.json +1 -1
  2. package/CHANGELOG.md +31 -0
  3. package/LICENSE +1 -1
  4. package/NOTICE +28 -0
  5. package/README.md +5 -5
  6. package/commands/plan.md +11 -11
  7. package/commands/review.md +2 -2
  8. package/dist/auth.d.ts +5 -0
  9. package/dist/credentials.d.ts +8 -0
  10. package/dist/gateway.js +1 -1
  11. package/dist/helpers.d.ts +1 -1
  12. package/dist/helpers.js +18 -2
  13. package/dist/index.js +47 -28
  14. package/dist/install-skills.d.ts +4 -3
  15. package/dist/install-skills.js +12 -8
  16. package/dist/refresh.d.ts +8 -2
  17. package/dist/refresh.js +33 -6
  18. package/dist/setup.d.ts +5 -0
  19. package/dist/setup.js +66 -0
  20. package/inspect-tools.json +89 -36
  21. package/{skills → npx-skills}/brainstorm/SKILL.md +2 -1
  22. package/{skills → npx-skills}/debug/SKILL.md +2 -1
  23. package/{skills → npx-skills}/pharaoh/SKILL.md +13 -6
  24. package/{skills → npx-skills}/plan/SKILL.md +13 -14
  25. package/npx-skills/refactor/SKILL.md +38 -0
  26. package/{skills → npx-skills}/review/SKILL.md +4 -5
  27. package/{skills → npx-skills}/tdd/SKILL.md +2 -1
  28. package/{skills → npx-skills}/verify/SKILL.md +2 -1
  29. package/package.json +6 -4
  30. package/project-memory/bundle.mjs +137 -0
  31. package/project-memory/execute-turn.mjs +240 -0
  32. package/project-memory/hook-config.mjs +91 -0
  33. package/project-memory/hook-events.mjs +141 -0
  34. package/project-memory/local.mjs +463 -0
  35. package/project-memory/memory-api.mjs +182 -0
  36. package/project-memory/pharaoh-memory-hook.mjs +33 -0
  37. package/project-memory/pharaoh-memory.mjs +55 -0
  38. package/project-memory/setup.d.mts +14 -0
  39. package/project-memory/setup.mjs +151 -0
  40. package/project-memory/user-target.mjs +221 -0
  41. package/trace-app.html +222 -101
  42. package/skills/.gitkeep +0 -0
  43. package/skills/refactor/SKILL.md +0 -34
package/trace-app.html CHANGED
@@ -3,94 +3,122 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
6
7
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; img-src 'none'; font-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'">
7
8
  <title>Pharaoh Trace</title>
8
9
  <style>
9
- :root { color-scheme:dark; --stone:#2a2520; --rule:#3a352e; --track:#342e28; --hover:#332d27; --gold:#d4a843; --papyrus:#e8dcc8; --text:#c4b99a; --muted:#9d927b; --red:#f87171; --amber:#f0c855; --green:#4ade80;
10
- --s0:#d4a843; --s1:#b08a3a; --s2:#8c6e31; --s3:#6e5728; --s4:#574522; --s-muted:#5f574a; --s-warning:#f0c855; --s-error:#ef4444;
11
- --sans:"DM Sans",system-ui,-apple-system,"Segoe UI",sans-serif; --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace; }
12
- :root[data-theme="light"] { color-scheme:light; --stone:#f7f3ea; --rule:#e2d8c4; --track:#ece4d3; --hover:#efe8da; --gold:#946c0f; --papyrus:#2a2520; --text:#4a4235; --muted:#71675a; --red:#b42318; --amber:#9a5b00; --green:#15803d;
13
- --s0:#b08515; --s1:#c9a24a; --s2:#d9bb72; --s3:#e6cf98; --s4:#efdfb8; --s-muted:#cfc4ad; --s-warning:#e0a526; --s-error:#d93f2f; }
10
+ /* Host tokens first: styles.variables lands on :root. Each fallback is Claude's default, so a host
11
+ that sends none still gets a themed card. Gold and the slice colours are the card's own; the rest is the host's. */
12
+ :root { color-scheme:light dark;
13
+ --ink:var(--color-text-primary,light-dark(#141413,#faf9f5)); --text:var(--color-text-secondary,light-dark(#3d3d3a,#c2c0b6)); --muted:var(--color-text-tertiary,light-dark(#73726c,#9c9a92));
14
+ --rule:var(--color-border-tertiary,light-dark(rgba(31,30,29,.15),rgba(222,220,209,.15))); --track:var(--color-background-secondary,light-dark(#f5f4ed,#262624)); --focus:var(--color-ring-primary,light-dark(rgba(20,20,19,.7),rgba(250,249,245,.7)));
15
+ --red:var(--color-text-danger,light-dark(#7f2c28,#ee8884)); --amber:var(--color-text-warning,light-dark(#5a4815,#d1a041)); --green:var(--color-text-success,light-dark(#265b19,#7ab948));
16
+ --gold:light-dark(#86610c,#d4a843);
17
+ /* Slices are separate things, so they share one lightness and differ by hue. The order alternates warm and cool, which keeps neighbours apart under colour blindness; it clears red, amber and green, which mean error, warning and ok here. */
18
+ --s0:light-dark(#977100,#cca13e); --s1:light-dark(#7d66b9,#ad97ee); --s2:light-dark(#008d65,#4ac094); --s3:light-dark(#5374c1,#81a5f6); --s4:light-dark(#aa5688,#df86ba);
19
+ --s-muted:var(--color-border-primary,light-dark(rgba(31,30,29,.4),rgba(222,220,209,.4))); --s-warning:var(--color-border-warning,light-dark(#805c1f,#a87829)); --s-error:var(--color-border-danger,light-dark(#a73d39,#cd5c58));
20
+ --sans:var(--font-sans,system-ui,-apple-system,"Segoe UI",sans-serif); --mono:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);
21
+ --type-heading:var(--font-heading-md-size,16px); --type-body:var(--font-text-sm-size,14px); --type-caption:var(--font-text-xs-size,12px); --leading:var(--font-text-sm-line-height,1.4);
22
+ --regular:var(--font-weight-normal,400); --strong:var(--font-weight-semibold,600);
23
+ --line:var(--border-width-regular,.5px) solid var(--rule); --radius:var(--border-radius-md,8px); --radius-small:var(--border-radius-xs,4px); }
24
+ :root[data-theme="light"] { color-scheme:light; } :root[data-theme="dark"] { color-scheme:dark; }
14
25
  * { box-sizing:border-box; }
15
26
  html,body { margin:0; background:transparent; min-width:0; }
27
+ body { padding:var(--safe-top,0) var(--safe-right,0) var(--safe-bottom,0) var(--safe-left,0); }
16
28
  .label,.detail,.badge { overflow-wrap:anywhere; min-width:0; unicode-bidi:plaintext; }
17
- .mark-s { font:600 12px var(--mono); width:1ch; }
18
- #trace { background:var(--stone); color:var(--text); border:1px solid var(--rule); border-radius:6px; padding:9px 12px; font:13px/1.45 var(--sans); min-width:0; }
29
+ .mark-s { font:var(--strong) var(--type-caption) var(--mono); width:1ch; }
30
+ #trace { color:var(--text); border:var(--line); border-radius:var(--radius); padding:10px 12px; font:var(--regular) var(--type-body)/var(--leading) var(--sans); min-width:0; }
19
31
  .head { display:flex; flex-wrap:wrap; align-items:baseline; gap:2px 10px; }
20
- .mark { color:var(--gold); font-size:14px; line-height:1; }
32
+ .mark { color:var(--gold); font-size:var(--type-heading); line-height:1; }
21
33
  .headline { flex:1 1 16em; margin:0; min-width:0; overflow-wrap:anywhere; }
22
34
  #kind { color:var(--muted); }
23
- #target { color:var(--papyrus); font:600 13px/1.45 var(--mono); }
35
+ #target { color:var(--ink); font:var(--strong) var(--type-heading)/var(--leading) var(--mono); }
24
36
  #target,.legend > span,.rows .label { unicode-bidi:isolate; }
25
- #title { color:var(--papyrus); font-weight:600; }
37
+ #title { color:var(--ink); font-size:var(--type-heading); font-weight:var(--strong); }
26
38
  #summary { margin-left:8px; }
27
- .num { color:var(--gold); font:600 12.5px var(--mono); }
28
- .meta { margin-left:auto; display:flex; gap:10px; white-space:nowrap; font-size:12px; }
39
+ .num { color:var(--gold); font-family:var(--mono); font-weight:var(--strong); }
40
+ .meta { margin-left:auto; display:flex; gap:10px; white-space:nowrap; font-size:var(--type-caption); }
29
41
  #verdict[data-state="error"] { color:var(--red); } #verdict[data-state="warning"] { color:var(--amber); } #verdict[data-state="ok"] { color:var(--green); }
30
- #sha { color:var(--muted); font:11.5px var(--mono); }
31
- .bar { position:relative; display:flex; gap:2px; height:6px; margin:8px 0 0; border-radius:3px; overflow:hidden; background:var(--track); }
32
- .bar i,.mini i { display:block; flex:0 1 0; min-width:0; }
33
- .bar i[data-shown] { min-width:3px; }
34
- .s0 { background:var(--s0); } .s1 { background:var(--s1); } .s2 { background:var(--s2); } .s3 { background:var(--s3); } .s4 { background:var(--s4); }
35
- .muted { background:var(--s-muted); } .warning { background:var(--s-warning); } .error { background:var(--s-error); }
36
- .foot { display:flex; align-items:baseline; gap:4px 14px; margin-top:6px; flex-wrap:wrap; }
37
- .legend { display:flex; flex-wrap:wrap; gap:2px 14px; margin:0; color:var(--muted); font-size:12px; min-width:0; flex:1 1 auto; }
42
+ #sha { color:var(--muted); font-family:var(--mono); }
43
+ /* Bars are text. Each cell is a tick-sized window onto a caption-size glyph, set apart from the next, so █ reads as a tick, not a rule.
44
+ Whole pixels: a window's edges round the same way, so every tick lands the same width on a 1x screen. */
45
+ .bar,.sw { --tick-width:5px; --tick-height:8px; }
46
+ .mini,.meter { --tick-width:4px; --tick-height:6px; }
47
+ .bar,.mini,.meter,.sw { font:var(--regular) var(--type-caption)/var(--tick-height) var(--mono); letter-spacing:0; font-variant-ligatures:none; -webkit-user-select:none; user-select:none; }
48
+ .bar,.mini,.meter { display:grid; grid-template-columns:repeat(var(--cols),var(--tick-width)); justify-content:space-between; min-width:0; }
49
+ .bar span,.mini span,.meter span,.sw { position:relative; width:var(--tick-width); height:var(--tick-height); overflow:hidden; }
50
+ .bar span,.mini span,.meter span { color:var(--s-muted); }
51
+ .bar span::after,.mini span::after,.meter span::after { content:attr(data-g); position:absolute; inset:0; color:var(--tone,var(--gold)); }
52
+ .bar { margin:8px 0 0; }
53
+ .s0 { --tone:var(--s0); } .s1 { --tone:var(--s1); } .s2 { --tone:var(--s2); } .s3 { --tone:var(--s3); } .s4 { --tone:var(--s4); }
54
+ .muted { --tone:var(--muted); } .warning { --tone:var(--s-warning); } .error { --tone:var(--s-error); }
55
+ .legend { display:flex; flex-wrap:wrap; gap:2px 14px; margin:6px 0 0; color:var(--muted); font-size:var(--type-caption); min-width:0; }
56
+ .legend:empty { display:none; }
38
57
  #trace[data-reveal] .legend { opacity:0; }
39
- .legend b { color:var(--text); font:500 11.5px var(--mono); margin-left:4px; }
40
- .sw { display:inline-block; width:8px; height:8px; border-radius:1px; margin-right:5px; }
41
- .toggle { margin-left:auto; display:flex; align-items:baseline; gap:4px; border:0; background:none; padding:2px 0; color:var(--gold); font:12px var(--sans); cursor:pointer; white-space:nowrap; }
42
- .toggle:hover { color:var(--papyrus); }
43
- .toggle:focus-visible,summary:focus-visible { outline:2px solid var(--gold); outline-offset:2px; border-radius:2px; }
44
- .chev { display:inline-block; font-size:10px; }
45
- .toggle[aria-expanded="true"] .chev { transform:rotate(90deg); }
46
- .caveat { margin:6px 0 0; color:var(--amber); font-size:12px; overflow-wrap:anywhere; }
58
+ .legend b { color:var(--text); font:var(--strong) var(--type-caption) var(--mono); margin-left:4px; }
59
+ .sw { display:inline-block; color:var(--tone); margin-right:5px; }
60
+ .foot { display:flex; justify-content:flex-end; gap:14px; margin-top:6px; }
61
+ .action { display:flex; align-items:center; gap:4px; border:0; background:none; padding:2px 0; color:var(--gold); font:var(--regular) var(--type-caption) var(--sans); cursor:pointer; white-space:nowrap; }
62
+ .action:hover { color:var(--ink); }
63
+ .action:focus-visible,summary:focus-visible { outline:2px solid var(--focus); outline-offset:2px; border-radius:var(--radius-small); }
64
+ .chev { display:inline-block; }
65
+ .action[aria-expanded="true"] .chev { transform:rotate(90deg); }
66
+ .caveat { margin:6px 0 0; color:var(--amber); font-size:var(--type-caption); overflow-wrap:anywhere; }
47
67
  .caveat span + span::before { content:" · "; color:var(--muted); }
48
- .rows { list-style:none; margin:4px 0 0; padding:0; }
49
- .rows li { display:flex; flex-wrap:wrap; align-items:baseline; gap:0 8px; padding:4px 0 5px; border-top:1px solid var(--rule); min-width:0; }
68
+ .rows { list-style:none; margin:4px 0 0; padding:0; font-size:var(--type-caption); }
69
+ .rows li { display:flex; flex-wrap:wrap; align-items:baseline; gap:0 8px; padding:4px 0 5px; border-top:var(--line); min-width:0; }
50
70
  .rows li:first-child { border-top:0; }
51
- .rows .kind { color:var(--muted); font-size:12.5px; }
52
- .rows .label { color:var(--papyrus); font:12.5px var(--mono); }
53
- .rows .prose { color:var(--text); font:12.5px var(--sans); }
54
- .rows .detail { color:var(--text); font-size:12.5px; }
55
- .rows .end { margin-left:auto; font-size:12px; white-space:nowrap; }
56
- .rows .more { color:var(--muted); font-size:12px; }
57
- .rows .mini { display:flex; gap:1px; flex:1 0 100%; height:3px; margin-top:3px; border-radius:2px; overflow:hidden; background:var(--track); }
71
+ .rows .kind { color:var(--muted); }
72
+ .rows .label { color:var(--ink); font-family:var(--mono); }
73
+ .rows .prose { color:var(--text); font-family:var(--sans); }
74
+ .rows .detail { color:var(--text); }
75
+ .rows .end { margin-left:auto; white-space:nowrap; }
76
+ .rows .more { color:var(--muted); }
77
+ .rows .mini { flex:1 0 100%; margin-top:4px; }
58
78
  [data-state="ok"] > .mark-s { color:var(--green); } [data-state="warning"] > .mark-s, [data-state="pending"] > .mark-s { color:var(--amber); } [data-state="error"] > .mark-s, .end[data-state="error"] { color:var(--red); } .end[data-state="warning"] { color:var(--amber); }
59
79
  .drawer { display:grid; grid-template-rows:0fr; }
60
80
  .drawer[data-open] { grid-template-rows:1fr; }
61
81
  .drawer-inner { overflow:hidden; min-height:0; }
62
- .drawer[data-open] .drawer-inner { border-top:1px solid var(--rule); margin-top:8px; padding-top:4px; }
82
+ .drawer[data-open] .drawer-inner { border-top:var(--line); margin-top:8px; padding-top:4px; }
63
83
  ul.tree,.tree ul { list-style:none; margin:0; padding:0; }
64
- .tree ul { margin:0 0 4px 6px; padding-left:10px; border-left:1px solid var(--rule); }
65
- .tree summary,.leaf { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:baseline; padding:3px 0; font-size:12.5px; min-width:0; }
84
+ .tree ul { margin:0 0 4px 6px; padding-left:10px; border-left:var(--line); }
85
+ .tree summary,.leaf { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:baseline; padding:3px 0; font-size:var(--type-caption); min-width:0; }
66
86
  .tree summary { list-style:none; cursor:pointer; color:var(--text); }
67
87
  .tree summary::-webkit-details-marker { display:none; }
68
- .tree summary:hover { background:var(--hover); }
69
- .tree summary .label::before { content:"▸"; display:inline-block; color:var(--gold); font-size:10px; width:12px; }
88
+ .tree summary:hover { background:var(--track); }
89
+ .tree summary .label::before { content:"▸"; display:inline-block; color:var(--gold); width:1em; }
70
90
  .tree details[open] > summary .label::before { transform:rotate(90deg); }
71
91
  .tree details[open] > summary .label { color:var(--gold); }
72
- .leaf .label { color:var(--papyrus); font:12px var(--mono); }
73
- .tree > li > .leaf { padding-left:12px; }
74
- .tree > li > .leaf .label { color:var(--text); font:12.5px var(--sans); }
75
- .tree .label .detail { color:var(--muted); font:11px var(--mono); margin-left:8px; }
76
- .tree .label .caveat { color:var(--amber); font:11.5px var(--sans); margin:0 0 0 8px; }
77
- .badge { color:var(--muted); font:11.5px var(--mono); text-align:right; white-space:nowrap; }
92
+ .leaf .label { color:var(--ink); font-family:var(--mono); }
93
+ .tree > li > .leaf { padding-left:1em; }
94
+ .tree > li > .leaf .label { color:var(--text); font-family:var(--sans); }
95
+ .tree .label .detail { color:var(--muted); font-family:var(--mono); margin-left:8px; }
96
+ .tree .label .caveat { color:var(--amber); font-family:var(--sans); margin:0 0 0 8px; }
97
+ .badge { color:var(--muted); font-family:var(--mono); text-align:right; white-space:nowrap; }
78
98
  .badge[data-state="ok"] { color:var(--green); } .badge[data-state="warning"],.badge[data-state="pending"] { color:var(--amber); } .badge[data-state="error"] { color:var(--red); }
79
- .meter { display:block; height:3px; margin-top:3px; border-radius:2px; background:var(--track); max-width:240px; }
80
- .meter i { display:block; height:100%; border-radius:2px; background:var(--gold); }
81
- .notes,.provenance { color:var(--muted); font-size:11.5px; margin:6px 0 2px; }
99
+ .meter { margin-top:4px; max-width:190px; }
100
+ .notes,.provenance { color:var(--muted); font-size:var(--type-caption); margin:6px 0 2px; }
82
101
  .notes p { margin:2px 0; overflow-wrap:anywhere; white-space:pre-wrap; }
83
- #fallback { font:11.5px/1.5 var(--mono); color:var(--text); margin:6px 0 0; white-space:pre-wrap; overflow-wrap:anywhere; unicode-bidi:plaintext; }
102
+ #fallback { font:var(--regular) var(--type-caption)/1.5 var(--mono); color:var(--text); margin:6px 0 0; white-space:pre-wrap; overflow-wrap:anywhere; unicode-bidi:plaintext; }
84
103
  @media(max-width:400px) { #trace { padding:8px 10px; } #summary { margin-left:0; } #summary:not([hidden]) { display:block; } .tree summary,.leaf { grid-template-columns:minmax(0,1fr); gap:0; } .badge { text-align:left; } }
104
+ /* A finger needs 44pt: the MCP Apps minimum for anything tappable. */
105
+ @media(pointer:coarse) { .action { min-height:44px; min-width:44px; justify-content:center; } .tree summary { min-height:44px; align-items:center; } .foot { gap:8px; } }
106
+ /* Inline cards can't scroll, so full screen is where long Details go: the page scrolls and they stay open. */
107
+ :root[data-display="fullscreen"],:root[data-display="fullscreen"] body { height:100%; }
108
+ :root[data-display="fullscreen"] body { overflow-y:auto; }
109
+ :root[data-display="fullscreen"] #trace { border:0; border-radius:0; }
110
+ /* The frame is short for a moment before a taller result is sized: a scrollbar then would shrink the card and redraw its bars mid-arrival. */
111
+ :root:not([data-display="fullscreen"]) { scrollbar-width:none; }
85
112
  /* Motion only for people who have not asked to reduce it. */
86
113
  @media(prefers-reduced-motion:no-preference) {
87
- .bar i,.mini i { transition:flex-grow .7s cubic-bezier(.22,.8,.24,1); }
114
+ #trace:not([data-settled]) .bar span::after,#trace:not([data-settled]) .mini span::after { animation:tick 1ms calc(var(--i) * .7s / var(--cols)) backwards; }
115
+ @keyframes tick { from { opacity:0; } }
88
116
  .legend { transition:opacity .4s ease .2s; }
89
117
  .chev { transition:transform .3s ease; }
90
118
  .drawer { transition:grid-template-rows .3s ease; }
91
119
  .tree summary .label::before { transition:transform .2s ease; }
92
- #trace[data-state="pending"] .bar::after,#trace[data-state="waiting"] .bar::after { content:""; position:absolute; top:0; bottom:0; left:0; width:35%; background:linear-gradient(90deg,transparent,var(--gold),transparent); opacity:.55; animation:sweep 1.3s ease-in-out infinite; }
93
- @keyframes sweep { from { transform:translateX(-100%); } to { transform:translateX(290%); } }
120
+ #trace[data-state="pending"] .bar span::after,#trace[data-state="waiting"] .bar span::after { content:"█"; color:var(--gold); opacity:0; animation:walk 1.5s linear calc(var(--i) * 1.5s / var(--cols)) infinite; }
121
+ @keyframes walk { from { opacity:1; } 16%,to { opacity:0; } }
94
122
  }
95
123
  [hidden] { display:none; }
96
124
  </style>
@@ -100,15 +128,16 @@
100
128
  <div class="head">
101
129
  <span class="mark" aria-hidden="true">☥</span>
102
130
  <p class="headline"><span id="kind"></span><span id="target"></span><span id="title">Pharaoh</span><span id="summary" role="status" aria-live="polite">Waiting for the tool</span></p>
103
- <span id="meta" class="meta"><span id="verdict" hidden></span><span id="sha" hidden></span></span>
131
+ <span class="meta"><span id="verdict" hidden></span><span id="sha" hidden></span></span>
104
132
  </div>
105
133
  <div id="bar" class="bar" aria-hidden="true"></div>
106
- <div id="foot" class="foot">
107
- <p id="legend" class="legend"></p>
108
- <button id="toggle" class="toggle" type="button" aria-expanded="false" aria-controls="drawer" hidden><span id="toggle-label">Details</span><span class="chev" aria-hidden="true">▸</span></button>
109
- </div>
134
+ <p id="legend" class="legend"></p>
110
135
  <p id="caveat" class="caveat" hidden></p>
111
136
  <ul id="rows" class="rows" hidden></ul>
137
+ <div id="foot" class="foot" hidden>
138
+ <button id="toggle" class="action" type="button" aria-expanded="false" aria-controls="drawer" hidden><span id="toggle-label">Details</span><span class="chev" aria-hidden="true">▸</span></button>
139
+ <button id="expand" class="action" type="button" hidden></button>
140
+ </div>
112
141
  <div id="drawer" class="drawer" inert>
113
142
  <div class="drawer-inner">
114
143
  <ul id="tree" class="tree" aria-label="Result evidence"></ul>
@@ -127,14 +156,28 @@
127
156
  const LIMITS = { bytes:65536, nodes:120, depth:6, label:512, detail:2048, badge:128, summary:1024, notes:16, note:1024, repo:256, segments:8 };
128
157
  const MAX_TEXT = 32768;
129
158
  const MAX_ROWS = 5;
159
+ // Px from one tick to the next, its width plus the gap: a wider card gets more ticks, not fatter ones.
160
+ const TICK_PITCH = {bar:7,mini:6};
161
+ const TICK_GAP = 2;
162
+ // Lays bars out while the frame has no width yet; the first resize refits them.
163
+ const FALLBACK_WIDTH = 340;
164
+ const METER_CELLS = 32;
165
+ const BLOCKS = ["","▏","▎","▍","▌","▋","▊","▉","█"];
130
166
  const TONES = ["warning","error","muted"];
131
167
  const STATES = ["ok","pending","warning","error"];
168
+ const DISPLAY_MODES = ["inline","fullscreen"];
169
+ // The tokens hosts send, and only those: a host never overwrites the card's own names (--gold, --rule).
170
+ const HOST_VARIABLE = /^--(color|font|border|shadow)-[a-z0-9-]+$/;
171
+ const MAX_HOST_VALUE = 512;
132
172
  // What each tool is doing while it runs, shown with the argument it targets.
133
- const VERBS = { get_blast_radius:"Tracing callers of", get_unused_code:"Checking reachability in", search_functions:"Searching for", get_codebase_map:"Mapping", get_module_context:"Reading module", pharaoh_recon:"Running recon on", pharaoh_account:"Checking account" };
173
+ const VERBS = { get_blast_radius:"Tracing callers of", get_unused_code:"Checking reachability in", search_functions:"Searching for", get_codebase_map:"Mapping", get_module_context:"Reading module", pharaoh_recon:"Running recon on", pharaoh_account:"Checking account", pharaoh_manage_repo:"Updating" };
134
174
  const root = document.getElementById("trace");
135
- const fields = Object.fromEntries(["kind","target","title","summary","meta","verdict","sha","bar","caveat","rows","foot","legend","toggle","toggle-label","drawer","tree","fallback","notes","provenance"].map(id => [id,document.getElementById(id)]));
175
+ const head = root.querySelector(".head");
176
+ const page = document.documentElement;
177
+ const fields = Object.fromEntries(["kind","target","title","summary","verdict","sha","bar","caveat","rows","foot","legend","toggle","toggle-label","expand","drawer","tree","fallback","notes","provenance"].map(id => [id,document.getElementById(id)]));
136
178
  const compact = new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1});
137
179
  const initId = "pharaoh-trace-initialize";
180
+ const displayId = "pharaoh-trace-display-";
138
181
  let tool = "";
139
182
  let ready = false;
140
183
  let closed = false;
@@ -142,6 +185,11 @@
142
185
  let resizeFrame = 0;
143
186
  let lastHeight = 0;
144
187
  let lastWidth = 0;
188
+ let bars = [];
189
+ let hasDetails = false;
190
+ let displayMode = "inline";
191
+ let hostModes = [];
192
+ let displayRequests = 0;
145
193
  const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
146
194
  const string = (value,max) => typeof value === "string" && value.length <= max;
147
195
  const send = message => { if (!closed) window.parent.postMessage({jsonrpc:"2.0",...message},"*"); };
@@ -172,10 +220,13 @@
172
220
  rich(fields.summary,summary);
173
221
  fields.summary.hidden = !summary;
174
222
  }
175
- // The toggle ends the legend line, or the headline when there is no legend.
176
- function place(withLegend) {
177
- (withLegend ? fields.foot : fields.meta).append(fields.toggle);
178
- fields.foot.hidden = !withLegend;
223
+ // Details and full screen sit at the foot of the card; full screen only where the host offers it.
224
+ function showActions() {
225
+ const full = displayMode === "fullscreen";
226
+ fields.toggle.hidden = !hasDetails || full;
227
+ fields.expand.hidden = !(hasDetails || full) || !hostModes.includes(full ? "inline" : "fullscreen");
228
+ fields.expand.textContent = full ? "Exit full screen" : "Full screen";
229
+ fields.foot.hidden = fields.toggle.hidden && fields.expand.hidden;
179
230
  }
180
231
  function setOpen(open) {
181
232
  fields.drawer.toggleAttribute("data-open",open);
@@ -183,16 +234,26 @@
183
234
  fields.toggle.setAttribute("aria-expanded",String(open));
184
235
  fields["toggle-label"].textContent = open ? "Hide" : "Details";
185
236
  }
237
+ function setDisplay(mode) {
238
+ // Hosts repeat the mode in context updates, and Details stay as the user left them.
239
+ if (mode !== displayMode) setOpen(mode === "fullscreen");
240
+ displayMode = mode;
241
+ page.dataset.display = mode;
242
+ showActions();
243
+ }
186
244
  function clearResult() {
187
- setOpen(false);
245
+ setOpen(displayMode === "fullscreen");
188
246
  for (const field of ["bar","rows","legend","tree","notes"]) fields[field].replaceChildren();
189
247
  for (const field of ["verdict","sha","caveat","provenance","fallback"]) display(field,"");
190
248
  delete fields.verdict.dataset.state;
191
249
  fields.sha.title = "";
192
250
  fields.bar.hidden = true;
251
+ bars = [];
252
+ delete root.dataset.settled;
193
253
  fields.rows.hidden = true;
194
254
  fields.notes.hidden = true;
195
- fields.toggle.hidden = true;
255
+ hasDetails = false;
256
+ showActions();
196
257
  }
197
258
  function notes(items) {
198
259
  for (const text of items) fields.notes.append(element("p","",text));
@@ -213,8 +274,8 @@
213
274
  display("verdict",state === "error" ? "Failed" : "Cancelled");
214
275
  }
215
276
  if (notice) notes([notice]);
216
- fields.toggle.hidden = !text;
217
- place(false);
277
+ hasDetails = Boolean(text);
278
+ showActions();
218
279
  }
219
280
  function readableText(result) {
220
281
  if (!record(result) || !Array.isArray(result.content)) return {text:"No readable text was returned. Read the tool response in chat.",omitted:false};
@@ -264,30 +325,70 @@
264
325
  return view.nodes.every(node => visit(node,1));
265
326
  } catch { return false; }
266
327
  }
267
- // Segments start empty and grow to their share once painted: the one motion on arrival.
268
- function segments(target,items) {
269
- const total = items.reduce((sum,item) => sum + item.value,0);
270
- if (!total) return [];
328
+ // Largest remainder: cells sum to exactly cols, and every slice keeps at least one.
329
+ function allocate(values,cols) {
330
+ const total = values.reduce((sum,value) => sum + value,0);
331
+ const exact = values.map(value => value / total * cols);
332
+ const cells = exact.map(share => Math.max(1,Math.floor(share)));
333
+ const spare = cols - cells.reduce((sum,count) => sum + count,0);
334
+ const shortest = exact.map((share,index) => [share - cells[index],index]).sort((a,b) => b[0] - a[0]);
335
+ for (const [,index] of shortest.slice(0,Math.max(0,spare))) cells[index]++;
336
+ for (let over = -spare; over > 0; over--) cells[cells.indexOf(Math.max(...cells))]--;
337
+ return cells;
338
+ }
339
+ // Every cell is a ░ with its real glyph laid over it, so a bar fills in over its own track.
340
+ function cell(target,tone,glyph) {
341
+ const node = element("span",tone,"░");
342
+ if (glyph) node.dataset.g = glyph;
343
+ node.style.setProperty("--i",target.childElementCount);
344
+ target.append(node);
345
+ }
346
+ const blockFor = item => item.tone === "muted" ? "░" : "█";
347
+ const colsFor = ({target,shown},width) => Math.max(shown.length * 2 - 1,Math.floor(((width || FALLBACK_WIDTH) + TICK_GAP) / TICK_PITCH[target.className]));
348
+ // One empty cell separates slices: ticks have no edge of their own to tell two golds apart.
349
+ function drawBar(bar,width) {
350
+ const {target,shown} = bar;
351
+ bar.cols = colsFor(bar,width);
352
+ const counts = allocate(shown.map(item => item.value),bar.cols - shown.length + 1);
353
+ target.style.setProperty("--cols",bar.cols);
354
+ target.replaceChildren();
271
355
  let shade = 0;
272
- return items.filter(item => item.value > 0).map((item,index) => {
273
- const slice = element("i",item.tone || `s${Math.min(shade++,4)}`);
274
- slice.style.transitionDelay = `${index * 70}ms`;
275
- slice.dataset.share = String(item.value / total);
276
- target.append(slice);
277
- return slice;
356
+ shown.forEach((item,position) => {
357
+ if (position) target.append(element("span"));
358
+ const tone = item.tone || `s${Math.min(shade++,4)}`;
359
+ for (let count = counts[position]; count > 0; count--) cell(target,tone,blockFor(item));
278
360
  });
361
+ while (target.childElementCount < bar.cols) cell(target);
362
+ }
363
+ function addBar(target,items) {
364
+ const bar = {target,shown:items.filter(item => item.value > 0)};
365
+ bars.push(bar);
366
+ drawBar(bar,head.clientWidth);
279
367
  }
280
- function grow(slices) {
281
- requestAnimationFrame(() => requestAnimationFrame(() => {
282
- for (const slice of slices) { slice.style.flexGrow = slice.dataset.share; slice.dataset.shown = ""; }
283
- delete root.dataset.reveal;
284
- }));
368
+ // A new column count redraws a bar, without the arrival motion. Smaller changes are the cell spacing's to absorb, so running motion is left alone.
369
+ function refit() {
370
+ const width = head.clientWidth;
371
+ const stale = bars.filter(bar => colsFor(bar,width) !== bar.cols);
372
+ if (!stale.length) return;
373
+ root.dataset.settled = "";
374
+ for (const bar of stale) drawBar(bar,width);
375
+ }
376
+ // The empty bar shown until a result arrives.
377
+ function showTrack() {
378
+ addBar(fields.bar,[]);
379
+ fields.bar.hidden = false;
380
+ }
381
+ // The bars fill by themselves in CSS; the legend fades in once they have painted.
382
+ function reveal() {
383
+ requestAnimationFrame(() => requestAnimationFrame(() => { delete root.dataset.reveal; }));
285
384
  }
286
385
  function legend(items) {
287
386
  let shade = 0;
288
387
  for (const item of items.filter(item => item.value > 0)) {
289
388
  const entry = element("span");
290
- entry.append(element("span",`sw ${item.tone || `s${Math.min(shade++,4)}`}`),item.label,element("b","",compact.format(item.value)));
389
+ const swatch = element("span",`sw ${item.tone || `s${Math.min(shade++,4)}`}`,blockFor(item));
390
+ swatch.setAttribute("aria-hidden","true");
391
+ entry.append(swatch,item.label,element("b","",compact.format(item.value)));
291
392
  fields.legend.append(entry);
292
393
  }
293
394
  }
@@ -298,7 +399,7 @@
298
399
  target.hidden = false;
299
400
  }
300
401
  // Sections (recon) and status lines (states, named checks) stay visible; groups open on demand.
301
- function rowFor(node,slices) {
402
+ function rowFor(node) {
302
403
  const item = element("li");
303
404
  if (node.status) {
304
405
  item.dataset.state = node.status;
@@ -317,7 +418,7 @@
317
418
  if (node.glance?.segments?.length) {
318
419
  const mini = element("span","mini");
319
420
  mini.setAttribute("aria-hidden","true");
320
- slices.push(...segments(mini,node.glance.segments));
421
+ addBar(mini,node.glance.segments);
321
422
  item.append(mini);
322
423
  }
323
424
  return item;
@@ -331,9 +432,11 @@
331
432
  if (node.meter) {
332
433
  const meter = element("span","meter");
333
434
  meter.title = `${node.meter.value} / ${node.meter.total}`;
334
- const fill = element("i");
335
- fill.style.width = `${Math.max(1,Math.round(node.meter.value / node.meter.total * 100))}%`;
336
- meter.append(fill);
435
+ meter.setAttribute("aria-hidden","true");
436
+ meter.style.setProperty("--cols",METER_CELLS);
437
+ // Eighths of a cell, and never none: a tiny value still shows. The window clips the top three to look full.
438
+ const eighths = Math.max(1,Math.round(node.meter.value / node.meter.total * METER_CELLS * 8));
439
+ for (let index = 0; index < METER_CELLS; index++) cell(meter,"",BLOCKS[Math.min(8,Math.max(0,eighths - index * 8))]);
337
440
  label.append(meter);
338
441
  }
339
442
  if (node.glance?.caveat) label.append(element("span","caveat",node.glance.caveat));
@@ -365,24 +468,25 @@
365
468
  }
366
469
  if (freshness) display("provenance",[freshness.mappedCommit ? `Mapped ${freshness.mappedCommit}` : "Mapped commit unknown",freshness.lastAnalyzed ? `as of ${freshness.lastAnalyzed}` : "map time unknown",`status: ${freshness.status}`].join(" · "));
367
470
  caveats([glance.caveat,freshness?.status === "stale" ? "Map is behind the latest push" : ""].filter(Boolean));
368
- const slices = segments(fields.bar,glance.segments || []);
369
- fields.bar.hidden = slices.length === 0;
471
+ const shown = glance.segments?.some(item => item.value > 0);
472
+ if (shown) addBar(fields.bar,glance.segments);
473
+ fields.bar.hidden = !shown;
370
474
  legend(glance.segments || []);
371
475
  // Up to MAX_ROWS rows, warnings and verdicts first, shown in their original order.
372
476
  const candidates = view.nodes.filter(node => node.glance || node.status);
373
477
  const urgent = node => node.status === "error" || node.status === "warning" || node.glance?.verdict || node.glance?.caveat;
374
478
  const chosen = new Set([...new Set([...candidates.filter(urgent),...candidates])].slice(0,MAX_ROWS));
375
479
  const visible = candidates.filter(node => chosen.has(node));
376
- for (const node of visible) fields.rows.append(rowFor(node,slices));
480
+ for (const node of visible) fields.rows.append(rowFor(node));
377
481
  if (candidates.length > visible.length) fields.rows.append(element("li","more",`+${candidates.length - visible.length} more in Details`));
378
482
  fields.rows.hidden = visible.length === 0;
379
483
  for (const node of view.nodes) if (!visible.includes(node) || node.children?.length) fields.tree.append(renderNode(node));
380
484
  // One click on Details shows evidence: the first group is already open inside the drawer.
381
485
  fields.tree.querySelector("details")?.setAttribute("open","");
382
486
  notes(view.notes || []);
383
- fields.toggle.hidden = !fields.tree.childElementCount && fields.notes.hidden && fields.provenance.hidden;
384
- place(fields.legend.childElementCount > 0);
385
- grow(slices);
487
+ hasDetails = fields.tree.childElementCount > 0 || !fields.notes.hidden || !fields.provenance.hidden;
488
+ showActions();
489
+ reveal();
386
490
  }
387
491
  function input(params) {
388
492
  cancelled = false;
@@ -394,7 +498,7 @@
394
498
  headline("","");
395
499
  display("kind",target ? `${verb} ` : `${verb}…`);
396
500
  display("target",target ? `${target}…` : "");
397
- fields.bar.hidden = false;
501
+ showTrack();
398
502
  }
399
503
  function result(params) {
400
504
  if (cancelled) return;
@@ -409,15 +513,24 @@
409
513
  else fallback("Tool response",text.text,[view === undefined ? "" : "Trace metadata could not be displayed; showing the tool's text response.",omission].filter(Boolean).join(" "));
410
514
  }
411
515
  function context(params) {
412
- if (params.theme === "dark" || params.theme === "light") document.documentElement.dataset.theme = params.theme;
516
+ // Updates carry only the fields that changed.
517
+ if (params.theme === "dark" || params.theme === "light") page.dataset.theme = params.theme;
518
+ const variables = params.styles?.variables;
519
+ if (record(variables)) for (const [name,value] of Object.entries(variables)) if (HOST_VARIABLE.test(name) && string(value,MAX_HOST_VALUE)) page.style.setProperty(name,value);
520
+ const insets = params.safeAreaInsets;
521
+ if (record(insets)) for (const side of ["top","right","bottom","left"]) if (Number.isFinite(insets[side]) && insets[side] >= 0) page.style.setProperty(`--safe-${side}`,`${insets[side]}px`);
522
+ if (Array.isArray(params.availableDisplayModes)) hostModes = params.availableDisplayModes;
523
+ if (DISPLAY_MODES.includes(params.displayMode)) setDisplay(params.displayMode);
413
524
  const name = params.toolInfo?.tool?.name;
414
525
  if (string(name,128)) tool = name;
526
+ showActions();
415
527
  }
416
528
  function size() {
417
529
  if (!ready || closed || resizeFrame) return;
418
530
  resizeFrame = requestAnimationFrame(() => {
419
531
  resizeFrame = 0;
420
- const height = Math.ceil(root.getBoundingClientRect().height);
532
+ refit();
533
+ const height = Math.ceil(document.body.getBoundingClientRect().height);
421
534
  const width = Math.ceil(document.documentElement.clientWidth);
422
535
  if (height !== lastHeight || width !== lastWidth) {
423
536
  lastHeight = height;
@@ -432,6 +545,7 @@
432
545
  root.addEventListener("transitionend",size);
433
546
  window.addEventListener("resize",size);
434
547
  fields.toggle.addEventListener("click",() => { setOpen(fields.toggle.getAttribute("aria-expanded") !== "true"); size(); });
548
+ fields.expand.addEventListener("click",() => send({id:`${displayId}${++displayRequests}`,method:"ui/request-display-mode",params:{mode:displayMode === "fullscreen" ? "inline" : "fullscreen"}}));
435
549
  function receive(event) {
436
550
  if (closed || event.source !== window.parent || !record(event.data) || event.data.jsonrpc !== "2.0") return;
437
551
  const message = event.data;
@@ -449,6 +563,12 @@
449
563
  size();
450
564
  return;
451
565
  }
566
+ // The host answers with the mode it actually set, which may not be the one requested.
567
+ if (typeof message.id === "string" && message.id.startsWith(displayId) && message.method === undefined) {
568
+ if (DISPLAY_MODES.includes(message.result?.mode)) setDisplay(message.result.mode);
569
+ size();
570
+ return;
571
+ }
452
572
  const request = typeof message.id === "string" || typeof message.id === "number";
453
573
  if (request) {
454
574
  if (message.method === "ping") send({id:message.id,result:{}});
@@ -473,10 +593,11 @@
473
593
  size();
474
594
  }
475
595
  window.addEventListener("message",receive);
596
+ showTrack();
476
597
  const initTimeout = setTimeout(() => {
477
598
  if (!ready && !closed) fallback("Text response available","Read the tool response in chat.","Trace has not received a host connection.");
478
599
  },10000);
479
- send({id:initId,method:"ui/initialize",params:{appInfo:{name:"Pharaoh Trace",version:"1.2.0"},appCapabilities:{availableDisplayModes:["inline"]},protocolVersion:PROTOCOL_VERSION}});
600
+ send({id:initId,method:"ui/initialize",params:{appInfo:{name:"Pharaoh Trace",version:"1.4.0"},appCapabilities:{availableDisplayModes:DISPLAY_MODES},protocolVersion:PROTOCOL_VERSION}});
480
601
  })();
481
602
  </script>
482
603
  </body>
package/skills/.gitkeep DELETED
File without changes
@@ -1,34 +0,0 @@
1
- ---
2
- name: refactor
3
- prompt-name: safe-refactor
4
- description: "Safe refactoring workflow using Pharaoh codebase knowledge graph. Six-step process: module context, blast radius of downstream callers, reachability verification, dependency mapping, naming conflict detection, and test coverage assessment. Produces a refactoring plan with every caller listed, test files identified, unreachable code flagged, and high-risk paths warned."
5
- version: 0.2.0
6
- homepage: https://pharaoh.so
7
- user-invocable: true
8
- metadata: {"emoji": "☥", "tags": ["refactoring", "blast-radius", "architecture", "pharaoh", "safe-refactor", "test-coverage"]}
9
- ---
10
-
11
- # Safe Refactor
12
-
13
- Step-by-step workflow to safely refactor a function or module with full blast radius awareness. Uses `safe-refactor` — a 6-step process that maps every caller, identifies affected tests, flags unreachable code, and warns about high-risk downstream paths before a single line changes.
14
-
15
- ## When to Use
16
-
17
- Invoke before refactoring any function, module, or file — especially shared utilities, exports used across multiple modules, or anything with an unclear caller graph. Use it whenever the blast radius of a change is unknown.
18
-
19
- ## Workflow
20
-
21
- 1. Call `get_module_context` for the module containing the target to understand its current structure.
22
- 2. Call `get_blast_radius` for the target to identify all downstream callers and affected modules.
23
- 3. Call `search_functions` to check if the refactored version's name already exists elsewhere.
24
-
25
- Do not propose a refactoring plan until all 3 steps are complete.
26
-
27
- ## Output
28
-
29
- A refactoring plan containing:
30
- - **Callers to update:** every function and file that calls the target, with update requirements
31
- - **Tests to change:** test files covering the refactored code, with required modifications
32
- - **Dead code:** unreachable code paths that can be deleted during the refactor
33
- - **High-risk paths:** downstream modules with wide blast radius or high complexity scores
34
- - **Naming conflicts:** existing functions whose names would conflict with the refactored version