omegon 0.6.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.
Files changed (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared debug logging for Omegon extensions.
3
+ *
4
+ * Output goes to a log file (~/.pi/agent/omegon-debug.log) so it doesn't
5
+ * corrupt the TUI. Tail the file in a separate terminal to watch live:
6
+ * tail -f ~/.pi/agent/omegon-debug.log
7
+ *
8
+ * Controlled by PI_DEBUG environment variable:
9
+ * PI_DEBUG=1 — all extensions
10
+ * PI_DEBUG=dashboard — only dashboard
11
+ * PI_DEBUG=openspec,cleave — comma-separated list
12
+ *
13
+ * Each log line: [HH:mm:ss.SSS scope:tag] {json}
14
+ */
15
+
16
+ import { appendFileSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { homedir } from "node:os";
19
+
20
+ const PI_DEBUG = process.env.PI_DEBUG ?? "";
21
+ const debugAll = PI_DEBUG === "1" || PI_DEBUG === "*" || PI_DEBUG === "true";
22
+ const debugScopes = new Set(
23
+ debugAll ? [] : PI_DEBUG.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean),
24
+ );
25
+
26
+ const LOG_DIR = join(homedir(), ".pi", "agent");
27
+ const LOG_PATH = join(LOG_DIR, "omegon-debug.log");
28
+ let dirEnsured = false;
29
+
30
+ function ensureDir(): void {
31
+ if (dirEnsured) return;
32
+ try {
33
+ mkdirSync(LOG_DIR, { recursive: true });
34
+ } catch {
35
+ // best effort
36
+ }
37
+ dirEnsured = true;
38
+ }
39
+
40
+ function isEnabled(scope: string): boolean {
41
+ if (debugAll) return true;
42
+ if (debugScopes.size === 0) return false;
43
+ return debugScopes.has(scope.toLowerCase());
44
+ }
45
+
46
+ /** Path to the debug log file, for display to users. */
47
+ export const DEBUG_LOG_PATH = LOG_PATH;
48
+
49
+ /**
50
+ * Log a debug message to the Omegon debug log file.
51
+ *
52
+ * @param scope - Extension name (e.g. "dashboard", "openspec", "cleave")
53
+ * @param tag - Sub-tag for the message (e.g. "render", "emitState", "session_start")
54
+ * @param data - Optional structured data to include
55
+ */
56
+ export function debug(scope: string, tag: string, data?: Record<string, unknown>): void {
57
+ if (!isEnabled(scope)) return;
58
+ ensureDir();
59
+ const ts = new Date().toISOString().slice(11, 23); // HH:mm:ss.SSS
60
+ const prefix = `[${ts} ${scope}:${tag}]`;
61
+ const line = data && Object.keys(data).length > 0
62
+ ? `${prefix} ${JSON.stringify(data)}\n`
63
+ : `${prefix}\n`;
64
+ try {
65
+ appendFileSync(LOG_PATH, line);
66
+ } catch {
67
+ // best effort — don't crash extensions over logging
68
+ }
69
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * defaults — Auto-configure Omegon defaults on first install
3
+ *
4
+ * - Sets theme to "default" if no theme is configured
5
+ * - Deploys global AGENTS.md to ~/.pi/agent/ for cross-project directives
6
+ *
7
+ * Guards:
8
+ * - Only writes settings/AGENTS.md if not already present or if managed by Omegon
9
+ * - Never overwrites a user-authored AGENTS.md (detected by absence of marker comment)
10
+ */
11
+
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import * as crypto from "node:crypto";
15
+ import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
16
+
17
+ /**
18
+ * Resolve the agent directory the same way pi's getAgentDir() does.
19
+ * In standalone mode (PI_CODING_AGENT_DIR set to omegon root) this points
20
+ * directly at the omegon repo, so theme/AGENTS.md deployment becomes
21
+ * identity copies and is skipped by the content-equality guards.
22
+ */
23
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
24
+ const AGENT_DIR = (() => {
25
+ const env = process.env.PI_CODING_AGENT_DIR;
26
+ if (env) {
27
+ if (env === "~") return home;
28
+ if (env.startsWith("~/")) return path.join(home, env.slice(2));
29
+ return env;
30
+ }
31
+ return path.join(home, ".pi", "agent");
32
+ })();
33
+
34
+ const SETTINGS_PATH = path.join(AGENT_DIR, "settings.json");
35
+ const GLOBAL_AGENTS_PATH = path.join(AGENT_DIR, "AGENTS.md");
36
+ const THEMES_DIR = path.join(AGENT_DIR, "themes");
37
+
38
+ /** Themes shipped with Omegon — deployed to ~/.pi/agent/themes/ */
39
+ const BUNDLED_THEMES = ["alpharius.json"] as const;
40
+
41
+ /** Marker embedded in the deployed AGENTS.md to identify Omegon ownership */
42
+ const PIKIT_MARKER = "<!-- managed by omegon -->";
43
+ const PIKIT_MARKER_LEGACY = "<!-- managed by pi-kit -->"; // legacy — still treated as owned
44
+
45
+ /** Hash file tracks the last content we deployed, so we detect user edits */
46
+ const HASH_PATH = path.join(AGENT_DIR, ".agents-md-hash");
47
+
48
+ /** Path to the template shipped with the Omegon package */
49
+ const TEMPLATE_PATH = path.join(import.meta.dirname, "..", "config", "AGENTS.md");
50
+
51
+ function contentHash(content: string): string {
52
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
53
+ }
54
+
55
+ /**
56
+ * Alpharius palette anchor values — must stay in sync with themes/alpharius.json vars.
57
+ * Emitted via OSC 10/11 to clamp the terminal's native fg/bg so that pi-tui's full
58
+ * \x1b[0m line-resets fall through to Alpharius colors rather than the user's terminal theme.
59
+ *
60
+ * OSC 10 = set default foreground color
61
+ * OSC 11 = set default background color
62
+ * OSC 110/111 = restore saved fg/bg (most terminals support this as a reset)
63
+ *
64
+ * Kitty DOES honor OSC 10/11 (confirmed experimentally — overrides conf-based bg at runtime).
65
+ * All modern terminals (Kitty, iTerm2, WezTerm, Alacritty, foot, VTE, xterm) respect these.
66
+ */
67
+ const ALPHARIUS_FG = "#c4d8e4";
68
+ const ALPHARIUS_BG = "#02030a";
69
+
70
+ function emitOsc10_11(fg: string, bg: string): void {
71
+ process.stdout.write(`\x1b]10;${fg}\x07\x1b]11;${bg}\x07`);
72
+ }
73
+
74
+ function restoreTerminalColors(): void {
75
+ // OSC 110 = restore saved default foreground, OSC 111 = restore saved default background
76
+ process.stdout.write("\x1b]110\x07\x1b]111\x07");
77
+ }
78
+
79
+ /** Ownership marker written by scripts/export-kitty-theme.ts into alpharius.conf */
80
+ const KITTY_OWNERSHIP_MARKER = "# Generated from themes/alpharius.json by scripts/export-kitty-theme.ts";
81
+
82
+ /** Installed location — standard Kitty themes directory */
83
+ const KITTY_CONF_PATH = path.join(
84
+ process.env.HOME || process.env.USERPROFILE || "~",
85
+ ".config", "kitty", "themes", "alpharius.conf",
86
+ );
87
+
88
+ /** Source of truth shipped with Omegon */
89
+ const KITTY_CONF_SRC = path.join(import.meta.dirname, "..", "themes", "alpharius.conf");
90
+
91
+ let kittyWarnedThisSession = false;
92
+
93
+ /**
94
+ * Gently sync alpharius.conf to Kitty's themes directory.
95
+ *
96
+ * Rules:
97
+ * - File missing → install it, notify
98
+ * - File present, owned → update if stale, notify with reload hint
99
+ * - File present, unowned → warn once, never touch
100
+ */
101
+ function syncKittyTheme(notify: (msg: string, level: "info" | "warning") => void): void {
102
+ try {
103
+ if (!fs.existsSync(KITTY_CONF_SRC)) return;
104
+ const srcContent = fs.readFileSync(KITTY_CONF_SRC, "utf8");
105
+
106
+ if (!fs.existsSync(KITTY_CONF_PATH)) {
107
+ // Not installed — write it
108
+ fs.mkdirSync(path.dirname(KITTY_CONF_PATH), { recursive: true });
109
+ fs.writeFileSync(KITTY_CONF_PATH, srcContent, "utf8");
110
+ notify("Omegon: installed alpharius.conf to Kitty themes — add `include themes/alpharius.conf` to kitty.conf", "info");
111
+ return;
112
+ }
113
+
114
+ const existing = fs.readFileSync(KITTY_CONF_PATH, "utf8");
115
+
116
+ if (!existing.includes(KITTY_OWNERSHIP_MARKER)) {
117
+ // User-authored — don't touch, warn once
118
+ if (!kittyWarnedThisSession) {
119
+ kittyWarnedThisSession = true;
120
+ notify(
121
+ "Omegon: ~/.config/kitty/themes/alpharius.conf exists but wasn't generated by Omegon — skipping update",
122
+ "warning",
123
+ );
124
+ }
125
+ return;
126
+ }
127
+
128
+ if (existing !== srcContent) {
129
+ // Owned and stale — update
130
+ fs.writeFileSync(KITTY_CONF_PATH, srcContent, "utf8");
131
+ notify("Omegon: Kitty theme updated — reload with Ctrl+Shift+F5", "info");
132
+ }
133
+ // else: up to date — no-op
134
+ } catch {
135
+ // Best effort
136
+ }
137
+ }
138
+
139
+ export default function (pi: ExtensionAPI) {
140
+ pi.on("session_start", async (_event, ctx) => {
141
+ // --- Terminal color anchoring (OSC 10/11) ---
142
+ // Clamp the terminal's native fg/bg to Alpharius values so that pi-tui's \x1b[0m
143
+ // line-resets (hardcoded in pi-tui applyLineResets) don't bleed through a
144
+ // lighter/different terminal background. OSC 10/11 works on all modern terminals
145
+ // including Kitty (confirmed — overrides conf-based colors at runtime).
146
+ emitOsc10_11(ALPHARIUS_FG, ALPHARIUS_BG);
147
+
148
+ // Belt-and-suspenders: if Kitty remote control is available, also push via @ set-colors.
149
+ // This survives cases where OSC sequences are swallowed by multiplexers.
150
+ try {
151
+ const { execSync } = await import("child_process");
152
+ execSync(`kitty @ set-colors background=${ALPHARIUS_BG} foreground=${ALPHARIUS_FG}`, {
153
+ timeout: 1000, stdio: "ignore",
154
+ });
155
+ } catch { /* not Kitty or remote control disabled — OSC 10/11 covers it */ }
156
+
157
+ // --- Kitty theme sync ---
158
+ if (ctx.hasUI) {
159
+ syncKittyTheme((msg, level) => ctx.ui.notify(msg, level));
160
+ } else {
161
+ syncKittyTheme(() => {}); // silent in non-UI mode (e.g. pi -p children)
162
+ }
163
+
164
+ // --- Terminal tab title branding ---
165
+ // Replace the core π symbol with Ω in the terminal tab title.
166
+ // This fires after the core title is set, so it overwrites it.
167
+ if (ctx.hasUI) {
168
+ const sessionName = ctx.sessionManager.getSessionName();
169
+ const cwdBasename = path.basename(ctx.cwd);
170
+ const title = sessionName
171
+ ? `Ω - ${sessionName} - ${cwdBasename}`
172
+ : `Ω - ${cwdBasename}`;
173
+ ctx.ui.setTitle(title);
174
+ }
175
+
176
+ // --- Theme default ---
177
+ try {
178
+ const raw = fs.readFileSync(SETTINGS_PATH, "utf8");
179
+ const settings = JSON.parse(raw);
180
+
181
+ let changed = false;
182
+
183
+ // Always enforce alpharius — Omegon is opinionated about its own TUI.
184
+ // Override "default" and absent theme; leave other explicit choices alone.
185
+ if (!settings.theme || settings.theme === "default") {
186
+ settings.theme = "alpharius";
187
+ changed = true;
188
+ }
189
+
190
+ if (changed) {
191
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
192
+ if (ctx.hasUI) {
193
+ ctx.ui.notify("Omegon: activated alpharius theme (restart pi to apply)", "info");
194
+ }
195
+ }
196
+ } catch {
197
+ // Best effort
198
+ }
199
+
200
+ // --- Theme deployment ---
201
+ // Copy bundled themes to ~/.pi/agent/themes/, overwriting on every session start
202
+ // so updates in the repo propagate automatically.
203
+ try {
204
+ fs.mkdirSync(THEMES_DIR, { recursive: true });
205
+ for (const themeFile of BUNDLED_THEMES) {
206
+ const src = path.join(import.meta.dirname, "..", "themes", themeFile);
207
+ const dst = path.join(THEMES_DIR, themeFile);
208
+ if (!fs.existsSync(src)) continue;
209
+ const srcContent = fs.readFileSync(src, "utf8");
210
+ const dstContent = fs.existsSync(dst) ? fs.readFileSync(dst, "utf8") : null;
211
+ if (srcContent !== dstContent) {
212
+ fs.writeFileSync(dst, srcContent, "utf8");
213
+ if (ctx.hasUI) {
214
+ ctx.ui.notify(`Omegon: updated theme ${themeFile} (restart to apply)`, "info");
215
+ }
216
+ }
217
+ }
218
+ } catch {
219
+ // Best effort
220
+ }
221
+
222
+ // --- Global AGENTS.md deployment ---
223
+ try {
224
+ if (!fs.existsSync(TEMPLATE_PATH)) return;
225
+ fs.mkdirSync(AGENT_DIR, { recursive: true });
226
+ const template = fs.readFileSync(TEMPLATE_PATH, "utf8");
227
+ const deployContent = `${template.trimEnd()}\n\n${PIKIT_MARKER}\n`;
228
+
229
+ if (fs.existsSync(GLOBAL_AGENTS_PATH)) {
230
+ const existing = fs.readFileSync(GLOBAL_AGENTS_PATH, "utf8");
231
+
232
+ if (existing.includes(PIKIT_MARKER) || existing.includes(PIKIT_MARKER_LEGACY)) {
233
+ // We own this file — check if user has edited it since last deploy
234
+ if (existing !== deployContent) {
235
+ const lastHash = fs.existsSync(HASH_PATH) ? fs.readFileSync(HASH_PATH, "utf8").trim() : null;
236
+ const existingHash = contentHash(existing);
237
+
238
+ if (!lastHash) {
239
+ // First run with hash tracking — adopt current content as baseline
240
+ // so we don't overwrite edits made before the hash mechanism existed
241
+ fs.writeFileSync(HASH_PATH, existingHash, "utf8");
242
+ if (ctx.hasUI) {
243
+ ctx.ui.notify(
244
+ "Omegon: AGENTS.md template updated. Changes will apply on next session start.",
245
+ "info",
246
+ );
247
+ }
248
+ } else if (lastHash !== existingHash) {
249
+ // File was modified externally — warn, don't overwrite
250
+ if (ctx.hasUI) {
251
+ ctx.ui.notify(
252
+ "Omegon: ~/.pi/agent/AGENTS.md has local edits. Remove the omegon marker to keep them, or delete the file to re-deploy.",
253
+ "warning",
254
+ );
255
+ }
256
+ } else {
257
+ // File matches our last deploy — safe to update
258
+ fs.writeFileSync(GLOBAL_AGENTS_PATH, deployContent, "utf8");
259
+ fs.writeFileSync(HASH_PATH, contentHash(deployContent), "utf8");
260
+ }
261
+ }
262
+ }
263
+ // else: user-authored file (no marker), don't touch it
264
+ } else {
265
+ // No AGENTS.md exists — deploy ours
266
+ fs.writeFileSync(GLOBAL_AGENTS_PATH, deployContent, "utf8");
267
+ fs.writeFileSync(HASH_PATH, contentHash(deployContent), "utf8");
268
+ if (ctx.hasUI) {
269
+ ctx.ui.notify("Omegon: deployed global directives to ~/.pi/agent/AGENTS.md", "info");
270
+ }
271
+ }
272
+ } catch {
273
+ // Best effort — don't break startup
274
+ }
275
+ });
276
+
277
+ pi.on("session_shutdown", async () => {
278
+ // Restore the terminal's original fg/bg on exit so the user's shell prompt
279
+ // and other programs see their own configured colors again.
280
+ restoreTerminalColors();
281
+ });
282
+ }
@@ -0,0 +1,161 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
5
+
6
+ import type { DesignNode, DesignTree } from "./types.ts";
7
+ import { getAllOpenQuestions, countAcceptanceCriteria } from "./tree.ts";
8
+ import { sharedState, DASHBOARD_UPDATE_EVENT } from "../shared-state.ts";
9
+ import type { DesignTreeDashboardState } from "../shared-state.ts";
10
+ import type { DesignAssessmentResult, DesignPipelineCounts } from "../dashboard/types.ts";
11
+ import type { DesignSpecBinding } from "../openspec/archive-gate.ts";
12
+ import { debug } from "../debug.ts";
13
+
14
+ /** Read assessment.json from openspec/design/<id>/assessment.json if it exists. */
15
+ function readAssessmentResult(cwd: string, nodeId: string): DesignAssessmentResult | null {
16
+ const assessmentPath = path.join(cwd, "openspec", "design", nodeId, "assessment.json");
17
+ if (!fs.existsSync(assessmentPath)) return null;
18
+ try {
19
+ const raw = JSON.parse(fs.readFileSync(assessmentPath, "utf-8"));
20
+ return {
21
+ outcome: raw.outcome ?? "ambiguous",
22
+ timestamp: raw.timestamp ?? "",
23
+ summary: raw.summary,
24
+ };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /** Build archivedIds Set by scanning openspec/design-archive/ exactly once. */
31
+ function buildArchivedIds(cwd: string): Set<string> {
32
+ const archiveDir = path.join(cwd, "openspec", "design-archive");
33
+ const ids = new Set<string>();
34
+ if (!fs.existsSync(archiveDir)) return ids;
35
+ for (const entry of fs.readdirSync(archiveDir, { withFileTypes: true })) {
36
+ if (!entry.isDirectory()) continue;
37
+ const match = entry.name.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
38
+ if (match) ids.add(match[1]);
39
+ }
40
+ return ids;
41
+ }
42
+
43
+ /** Inline binding resolver that uses a pre-built archivedIds set — no extra readdirSync. */
44
+ function resolveBindingInline(cwd: string, nodeId: string, archivedIds: Set<string>): DesignSpecBinding {
45
+ const designDir = path.join(cwd, "openspec", "design", nodeId);
46
+ const active =
47
+ fs.existsSync(designDir) &&
48
+ fs.statSync(designDir).isDirectory() &&
49
+ fs.readdirSync(designDir).length > 0;
50
+ const archived = archivedIds.has(nodeId);
51
+ return { active, archived: archived && !active, missing: !active && !archived };
52
+ }
53
+
54
+ export function emitDesignTreeState(pi: ExtensionAPI, dt: DesignTree, focused: DesignNode | null): void {
55
+ if (dt.nodes.size === 0) return;
56
+ const cwd = process.cwd();
57
+ const allNodes = Array.from(dt.nodes.values());
58
+ // Exclude implemented nodes from the active dashboard view — they're done work.
59
+ // Deferred nodes remain visible: they are future work, not OBE.
60
+ const nodes = allNodes.filter((n) => n.status !== "implemented");
61
+
62
+ // C1 fix: scan design-archive once outside the map loop.
63
+ const archivedIds = buildArchivedIds(cwd);
64
+
65
+ // Compute design pipeline funnel counts across ALL nodes
66
+ const pipelineCounts: DesignPipelineCounts = {
67
+ needsSpec: 0,
68
+ designing: 0,
69
+ decided: 0,
70
+ implementing: 0,
71
+ done: allNodes.filter((n) => n.status === "implemented").length,
72
+ };
73
+
74
+ const NEUTRAL_SENTINEL: DesignSpecBinding = { active: false, archived: false, missing: false };
75
+
76
+ const enrichedNodes = nodes.map((n) => {
77
+ const isSeedLike = n.status === "seed";
78
+ const isActivePhase = ["exploring", "decided", "implementing"].includes(n.status);
79
+ // W3 fix: deferred/blocked also receive the neutral sentinel (not undefined)
80
+ const isPassive = n.status === "deferred" || n.status === "blocked";
81
+
82
+ // Resolve binding for non-seed nodes
83
+ let designSpec: DesignSpecBinding;
84
+ let acSummary: ReturnType<typeof countAcceptanceCriteria> | null = null;
85
+ let assessmentResult: DesignAssessmentResult | null = null;
86
+
87
+ if (isSeedLike || isPassive) {
88
+ // Seeds/deferred/blocked have no active binding — emit neutral sentinel
89
+ designSpec = NEUTRAL_SENTINEL;
90
+ } else {
91
+ // isActivePhase: exploring, decided, implementing
92
+ // C1 fix: use inline resolver with pre-built archivedIds
93
+ designSpec = resolveBindingInline(cwd, n.id, archivedIds);
94
+ acSummary = countAcceptanceCriteria(n);
95
+ assessmentResult = readAssessmentResult(cwd, n.id);
96
+ }
97
+
98
+ // Accumulate pipeline counts
99
+ // C3 fix: deferred/blocked fall into needsSpec so funnel totals reconcile
100
+ if (n.status === "decided") {
101
+ pipelineCounts.decided++;
102
+ } else if (n.status === "implementing") {
103
+ pipelineCounts.implementing++;
104
+ } else if (n.status === "exploring" || n.status === "seed") {
105
+ const bound = designSpec.active || designSpec.archived;
106
+ if (bound) {
107
+ pipelineCounts.designing++;
108
+ } else {
109
+ pipelineCounts.needsSpec++;
110
+ }
111
+ } else {
112
+ // deferred / blocked — no spec yet, count as needsSpec
113
+ pipelineCounts.needsSpec++;
114
+ }
115
+
116
+ return {
117
+ id: n.id,
118
+ title: n.title,
119
+ status: n.status,
120
+ questionCount: n.open_questions.length,
121
+ filePath: n.filePath,
122
+ branches: n.branches ?? [],
123
+ openspecChange: n.openspec_change ?? null,
124
+ designSpec,
125
+ acSummary,
126
+ assessmentResult,
127
+ };
128
+ });
129
+
130
+ const state: DesignTreeDashboardState = {
131
+ // W1 fix: nodeCount reflects ALL nodes so implementedCount/nodeCount ratios are correct
132
+ nodeCount: allNodes.length,
133
+ decidedCount: nodes.filter((n) => n.status === "decided").length,
134
+ exploringCount: nodes.filter((n) => n.status === "exploring" || n.status === "seed").length,
135
+ implementingCount: nodes.filter((n) => n.status === "implementing").length,
136
+ implementedCount: allNodes.filter((n) => n.status === "implemented").length,
137
+ blockedCount: nodes.filter((n) => n.status === "blocked").length,
138
+ deferredCount: nodes.filter((n) => n.status === "deferred").length,
139
+ openQuestionCount: getAllOpenQuestions(dt).length,
140
+ focusedNode: focused
141
+ ? {
142
+ id: focused.id,
143
+ title: focused.title,
144
+ status: focused.status,
145
+ questions: [...focused.open_questions],
146
+ branch: focused.branches?.[0],
147
+ branchCount: focused.branches?.length ?? 0,
148
+ filePath: focused.filePath,
149
+ }
150
+ : null,
151
+ nodes: enrichedNodes,
152
+ implementingNodes: nodes
153
+ .filter((n) => n.status === "implementing")
154
+ .map((n) => ({ id: n.id, title: n.title, branch: n.branches?.[0], filePath: n.filePath })),
155
+ designPipeline: pipelineCounts,
156
+ };
157
+
158
+ sharedState.designTree = state;
159
+ debug("design-tree", "emitState", { nodeCount: nodes.length, decided: state.decidedCount, exploring: state.exploringCount });
160
+ pi.events.emit(DASHBOARD_UPDATE_EVENT, { source: "design-tree" });
161
+ }