@underactive/pi-topping-moa-fusion 0.1.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 (88) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +437 -0
  4. package/agents/mf-plan.md +43 -0
  5. package/agents/moa-debater.md +37 -0
  6. package/agents/moa-explore.md +56 -0
  7. package/agents/moa-opinion.md +29 -0
  8. package/agents/moa-proposer.md +49 -0
  9. package/agents/moa-synthesizer.md +124 -0
  10. package/agents/moa-verifier.md +67 -0
  11. package/index.ts +3 -0
  12. package/package.json +61 -0
  13. package/src/activityMeter.ts +193 -0
  14. package/src/agents/authoritative.ts +91 -0
  15. package/src/agents/defaults.ts +123 -0
  16. package/src/agents/discovery.ts +119 -0
  17. package/src/config/modelCatalogue.ts +54 -0
  18. package/src/config/planName.ts +74 -0
  19. package/src/config/rosters.ts +118 -0
  20. package/src/config/settings.ts +161 -0
  21. package/src/debate/debateContract.ts +89 -0
  22. package/src/debate/debateFanout.ts +285 -0
  23. package/src/debate/debateFile.ts +38 -0
  24. package/src/debate/debateResults.ts +115 -0
  25. package/src/debate/debateRounds.ts +61 -0
  26. package/src/debate/runDebate.ts +143 -0
  27. package/src/index.ts +283 -0
  28. package/src/moa/conflictContract.ts +49 -0
  29. package/src/moa/conflicts.ts +153 -0
  30. package/src/moa/contextContract.ts +52 -0
  31. package/src/moa/fanout.ts +152 -0
  32. package/src/moa/fanoutWiring.ts +88 -0
  33. package/src/moa/implementationRetry.ts +292 -0
  34. package/src/moa/modelRuntime.ts +87 -0
  35. package/src/moa/orchestration.ts +105 -0
  36. package/src/moa/planInfo.ts +57 -0
  37. package/src/moa/planlessRetry.ts +72 -0
  38. package/src/moa/reviewLoop.ts +170 -0
  39. package/src/moa/runContext.ts +118 -0
  40. package/src/moa/synthesis.ts +420 -0
  41. package/src/moa/verdicts.ts +81 -0
  42. package/src/moa/verification.ts +791 -0
  43. package/src/moa/verificationCriteria.ts +127 -0
  44. package/src/moa/verifyGate.ts +137 -0
  45. package/src/opinion/opinionContract.ts +21 -0
  46. package/src/opinion/opinionFanout.ts +135 -0
  47. package/src/opinion/opinionFile.ts +38 -0
  48. package/src/opinion/opinionResults.ts +73 -0
  49. package/src/opinion/runOpinion.ts +156 -0
  50. package/src/planning/askUserQuestion.ts +83 -0
  51. package/src/planning/instructions.ts +146 -0
  52. package/src/planning/modeState.ts +61 -0
  53. package/src/planning/planFile.ts +273 -0
  54. package/src/planning/planMode.ts +673 -0
  55. package/src/planning/tools/enterPlanMode.ts +165 -0
  56. package/src/planning/tools/exitPlanMode.ts +159 -0
  57. package/src/planning/tools/mfPlanSubagent.ts +311 -0
  58. package/src/planning/tools/shared.ts +19 -0
  59. package/src/planning/tools/writePlan.ts +33 -0
  60. package/src/runtime/activityTracking.ts +141 -0
  61. package/src/runtime/cancelRun.ts +134 -0
  62. package/src/runtime/mutationTripwire.ts +251 -0
  63. package/src/runtime/processPool.ts +55 -0
  64. package/src/runtime/results.ts +103 -0
  65. package/src/runtime/runner.ts +538 -0
  66. package/src/runtime/wire.ts +177 -0
  67. package/src/shared/functionKeys.ts +30 -0
  68. package/src/shared/modelRefs.ts +91 -0
  69. package/src/ui/agentStatus.ts +84 -0
  70. package/src/ui/agentTranscript.ts +112 -0
  71. package/src/ui/cancelOverlay.ts +191 -0
  72. package/src/ui/chrome.ts +151 -0
  73. package/src/ui/conflictOverlay.ts +363 -0
  74. package/src/ui/debateModelPicker.ts +273 -0
  75. package/src/ui/menu.ts +679 -0
  76. package/src/ui/moaModelPicker.ts +900 -0
  77. package/src/ui/moaProgressWidget.ts +910 -0
  78. package/src/ui/moaSetupOverlay.ts +368 -0
  79. package/src/ui/modelLabel.ts +61 -0
  80. package/src/ui/observeOverlay.ts +206 -0
  81. package/src/ui/opinionModelPicker.ts +246 -0
  82. package/src/ui/planReviewOverlay.ts +315 -0
  83. package/src/ui/promptEditor.ts +87 -0
  84. package/src/ui/rosterEditor.ts +310 -0
  85. package/src/ui/shimmer.ts +77 -0
  86. package/src/ui/toolActivity.ts +35 -0
  87. package/src/ui/twoPaneModelThinking.ts +272 -0
  88. package/src/ui/verificationFindingsOverlay.ts +137 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Plan file management — slug generation, path resolution, read/write.
3
+ * Ported from Claude Code's plans.ts.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
10
+
11
+ const MAX_SLUG_RETRIES = 10;
12
+ const MAX_PLAN_SLUG_LENGTH = 100;
13
+ const PLAN_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
+
15
+ export function isValidPlanSlug(slug: unknown): slug is string {
16
+ return typeof slug === "string" && slug.length <= MAX_PLAN_SLUG_LENGTH && PLAN_SLUG_PATTERN.test(slug);
17
+ }
18
+
19
+ // Adjective + adjective + noun word lists for slug generation
20
+ const ADJECTIVES = [
21
+ "brave", "calm", "eager", "fair", "gentle", "happy", "jolly", "kind",
22
+ "lively", "nice", "proud", "quick", "salty", "warm", "witty", "zesty",
23
+ "bold", "clever", "daring", "fancy", "graceful", "keen", "loyal", "merry",
24
+ "noble", "polite", "rapid", "sharp", "swift", "tidy", "vivid", "young",
25
+ "mellifluous", "serene", "tranquil", "radiant", "velvet", "cosmic", "whimsical", "luminous",
26
+ "ancient", "playful", "sturdy", "wondrous", "halcyon", "effervescent", "quizzical", "amber",
27
+ "crimson", "silent", "winsome", "rustic",
28
+ ];
29
+
30
+ const NOUNS = [
31
+ "otter", "falcon", "dolphin", "eagle", "fox", "hawk", "lion", "moose",
32
+ "owl", "panda", "raven", "shark", "tiger", "whale", "wolf", "zebra",
33
+ "badger", "crane", "heron", "iguana", "jaguar", "koala", "lemur", "mink",
34
+ "narwhal", "ocelot", "puma", "quail", "salmon", "toucan", "viper", "yak",
35
+ ];
36
+
37
+ function randomWord(list: string[]): string {
38
+ return list[Math.floor(Math.random() * list.length)];
39
+ }
40
+
41
+ export function generateWordSlug(): string {
42
+ return `${randomWord(ADJECTIVES)}-${randomWord(ADJECTIVES)}-${randomWord(NOUNS)}`;
43
+ }
44
+
45
+ /** Get the plans directory (~/.pi/agent/mf-plan/plans/). Creates it if missing. */
46
+ export function getPlansDirectory(): string {
47
+ const plansPath = path.join(getAgentDir(), "mf-plan", "plans");
48
+ try {
49
+ fs.mkdirSync(plansPath, { recursive: true });
50
+ } catch {
51
+ // ignore
52
+ }
53
+ return plansPath;
54
+ }
55
+
56
+ /** Generate a unique plan slug for this session. Cached after first call. */
57
+ let cachedSlug: string | undefined;
58
+
59
+ export function getPlanSlug(): string {
60
+ if (cachedSlug) return cachedSlug;
61
+
62
+ const plansDir = getPlansDirectory();
63
+ for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
64
+ const slug = generateWordSlug();
65
+ const filePath = path.join(plansDir, `${slug}.md`);
66
+ if (!fs.existsSync(filePath)) {
67
+ cachedSlug = slug;
68
+ return slug;
69
+ }
70
+ }
71
+ // Fallback: use timestamp
72
+ cachedSlug = `plan-${Date.now()}`;
73
+ return cachedSlug;
74
+ }
75
+
76
+ /** Restore a validated slug from persisted state (for /resume). */
77
+ export function setPlanSlug(slug: string): boolean {
78
+ if (!isValidPlanSlug(slug)) return false;
79
+ cachedSlug = slug;
80
+ return true;
81
+ }
82
+
83
+ /** Clear the cached slug when a fresh session starts (for /new). */
84
+ export function resetPlanSlug(): void {
85
+ cachedSlug = undefined;
86
+ }
87
+
88
+ /** Get the full plan file path. */
89
+ export function getPlanFilePath(): string {
90
+ const plansDirectory = path.resolve(getPlansDirectory());
91
+ const slug = getPlanSlug();
92
+ if (!isValidPlanSlug(slug)) throw new Error("Invalid plan slug");
93
+ const filePath = path.resolve(plansDirectory, `${slug}.md`);
94
+ if (!filePath.startsWith(`${plansDirectory}${path.sep}`)) throw new Error("Plan path escapes plans directory");
95
+ return filePath;
96
+ }
97
+
98
+ /** Read the plan content from disk. Returns null if file doesn't exist. */
99
+ export function getPlan(): string | null {
100
+ const filePath = getPlanFilePath();
101
+ try {
102
+ return fs.readFileSync(filePath, { encoding: "utf-8" });
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /** Write content to the plan file. Creates the file if it doesn't exist. */
109
+ export function writePlan(content: string): void {
110
+ const filePath = getPlanFilePath();
111
+ fs.writeFileSync(filePath, content, { encoding: "utf-8" });
112
+ }
113
+
114
+ /** Convert a 4-word summary into a filename-safe slug, if it contains usable words. */
115
+ export function trySlugifyPlanName(text: string): string | undefined {
116
+ const words = text
117
+ .trim()
118
+ .toLowerCase()
119
+ .replace(/[^a-z0-9\s-]/g, " ")
120
+ .split(/\s+/)
121
+ .filter(Boolean)
122
+ .slice(0, 4);
123
+
124
+ const normalized = words.join("-").replace(/-+/g, "-").replace(/^-|-$/g, "");
125
+ const bounded = normalized.slice(0, MAX_PLAN_SLUG_LENGTH).replace(/-+$/g, "");
126
+ return isValidPlanSlug(bounded) ? bounded : undefined;
127
+ }
128
+
129
+ /** Convert a 4-word summary into a filename-safe slug, using a random slug for empty input. */
130
+ export function slugifyPlanName(text: string): string {
131
+ return trySlugifyPlanName(text) ?? generateWordSlug();
132
+ }
133
+
134
+ /** Fallback slug when LLM summarization is unavailable. */
135
+ export function fallbackPlanName(): string {
136
+ return generateWordSlug();
137
+ }
138
+
139
+ export type RepoPlanFileKind = "plan-prompt" | "plan" | "verification" | "criteria";
140
+
141
+ function repoPlanSuffix(kind: RepoPlanFileKind): string {
142
+ // `__verification` and `__criteria` never collide with the `/mf-plan-implement`
143
+ // scan, which matches `__plan.md` while excluding `__plan-prompt.md`.
144
+ if (kind === "plan-prompt") return "__plan-prompt";
145
+ if (kind === "verification") return "__verification";
146
+ if (kind === "criteria") return "__criteria";
147
+ return "__plan";
148
+ }
149
+
150
+ /** Ensure `<CONFIG_DIR_NAME>/mf-plan/` exists in the given repo directory. */
151
+ export function getRepoPlanDirectory(repoCwd: string): string {
152
+ const planDir = path.join(repoCwd, CONFIG_DIR_NAME, "mf-plan");
153
+ try {
154
+ fs.mkdirSync(planDir, { recursive: true });
155
+ } catch {
156
+ // ignore
157
+ }
158
+ return planDir;
159
+ }
160
+
161
+ /** Save content to `<CONFIG_DIR_NAME>/mf-plan/<slug>__plan-prompt.md` or `<slug>__plan.md`. Returns the file path. */
162
+ export function saveRepoPlanFile(
163
+ content: string,
164
+ repoCwd: string,
165
+ baseSlug: string,
166
+ kind: RepoPlanFileKind,
167
+ ): string {
168
+ if (!isValidPlanSlug(baseSlug)) throw new Error("Invalid repository plan slug");
169
+ const planDir = getRepoPlanDirectory(repoCwd);
170
+ const suffix = repoPlanSuffix(kind);
171
+ const filePath = path.join(planDir, `${baseSlug}${suffix}.md`);
172
+ fs.writeFileSync(filePath, content, { encoding: "utf-8" });
173
+ return filePath;
174
+ }
175
+
176
+ /** Read a persisted repository plan artifact, returning undefined on any failure. */
177
+ export function readRepoPlanFile(repoCwd: string, baseSlug: string, kind: RepoPlanFileKind): string | undefined {
178
+ if (!isValidPlanSlug(baseSlug)) return undefined;
179
+ try {
180
+ const content = fs.readFileSync(path.join(getRepoPlanDirectory(repoCwd), `${baseSlug}${repoPlanSuffix(kind)}.md`), "utf8").trim();
181
+ return content || undefined;
182
+ } catch {
183
+ return undefined;
184
+ }
185
+ }
186
+
187
+ /** Verbatim on-disk copies of one run's proposer plans, in `succeeded` order. */
188
+ export interface ProposalFileSet {
189
+ dir: string;
190
+ files: { label: string; path: string }[];
191
+ }
192
+
193
+ // Shared by the writer and the sweeper: the sweeper only recognizes abandoned
194
+ // directories it can match by name, so these must never drift apart.
195
+ const PROPOSAL_DIR_PREFIX = "mf-proposals-";
196
+
197
+ /** How long an abandoned staging directory survives before being swept. */
198
+ const PROPOSAL_DIR_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
199
+
200
+ /**
201
+ * Stage the proposer plans where the synthesizer can re-read them verbatim.
202
+ *
203
+ * These live under `os.tmpdir()`, never the repo: the fan-out mutation tripwire
204
+ * diffs `git status --porcelain`, so an in-repo copy would trip a false "planning
205
+ * agents modified the working tree" alarm and pollute the tree the synthesizer
206
+ * greps. Each run gets its own `mkdtemp` directory holding only its own
207
+ * proposals, so an `ls` cannot expose a sibling run. Filenames are derived from
208
+ * the already-blinded slot label, keeping the synthesizer blind to which model
209
+ * authored which proposal.
210
+ *
211
+ * Returns null if staging fails — callers still inline the proposals, so losing
212
+ * the copies costs recoverability, not correctness.
213
+ */
214
+ export function writeProposalFiles(
215
+ proposals: { label: string; plan: string }[],
216
+ ): ProposalFileSet | null {
217
+ try {
218
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), PROPOSAL_DIR_PREFIX));
219
+ const files = proposals.map(({ label, plan }) => {
220
+ const safeName = label.toLowerCase().replace(/[^\w.-]+/g, "-");
221
+ const filePath = path.join(dir, `${safeName}.md`);
222
+ fs.writeFileSync(filePath, plan, { encoding: "utf-8", mode: 0o600 });
223
+ return { label, path: filePath };
224
+ });
225
+ return { dir, files };
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ /** Remove a run's staged proposal copies. Safe to call twice or with null. */
232
+ export function cleanupProposalFiles(set: ProposalFileSet | null): void {
233
+ if (!set) return;
234
+ try {
235
+ fs.rmSync(set.dir, { recursive: true, force: true });
236
+ } catch {
237
+ // ignore
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Drop staging directories that earlier runs abandoned.
243
+ *
244
+ * `cleanupProposalFiles` runs from a `finally`, which a SIGKILL, crash, or power
245
+ * loss never reaches — those runs leak their directory into the temp dir
246
+ * permanently. The age cutoff is far longer than any planning run could last, so
247
+ * a directory old enough to sweep cannot belong to a live session and concurrent
248
+ * runs are never at risk. Best-effort throughout: a planning run must not fail
249
+ * because the temp dir was unreadable.
250
+ */
251
+ export function sweepStaleProposalFiles(): void {
252
+ const root = os.tmpdir();
253
+ let entries: fs.Dirent[];
254
+ try {
255
+ entries = fs.readdirSync(root, { withFileTypes: true });
256
+ } catch {
257
+ return;
258
+ }
259
+ const cutoff = Date.now() - PROPOSAL_DIR_MAX_AGE_MS;
260
+ for (const entry of entries) {
261
+ // Dirent.isDirectory() reports the entry's own type rather than a symlink
262
+ // target's, so a planted `mf-proposals-*` link cannot redirect the delete
263
+ // somewhere outside the temp dir.
264
+ if (!entry.isDirectory() || !entry.name.startsWith(PROPOSAL_DIR_PREFIX)) continue;
265
+ const dir = path.join(root, entry.name);
266
+ try {
267
+ if (fs.statSync(dir).mtimeMs > cutoff) continue;
268
+ fs.rmSync(dir, { recursive: true, force: true });
269
+ } catch {
270
+ // ignore
271
+ }
272
+ }
273
+ }