@rol1n/pi-kit 0.0.0-stage → 0.1.1
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 +73 -2
- package/ask-u-questions/README.md +26 -0
- package/ask-u-questions/dialog-ui.ts +80 -0
- package/ask-u-questions/index.ts +92 -0
- package/ask-u-questions/multi-choice.ts +258 -0
- package/ask-u-questions/single-choice.ts +180 -0
- package/ask-u-questions/tool-renderers.ts +67 -0
- package/ask-u-questions/types.ts +192 -0
- package/continue-signal/README.md +20 -0
- package/continue-signal/index.ts +69 -0
- package/custom-providers/README.md +20 -0
- package/custom-providers/asr/handlers/dashscope.ts +154 -0
- package/custom-providers/asr/index.ts +37 -0
- package/custom-providers/asr/models.ts +18 -0
- package/custom-providers/asr/types.ts +20 -0
- package/custom-providers/chat/index.ts +33 -0
- package/custom-providers/chat/magpie.ts +258 -0
- package/custom-providers/chat/models.ts +111 -0
- package/custom-providers/classifier/index.ts +6 -0
- package/custom-providers/classifier/models.ts +1 -0
- package/custom-providers/image/handlers/dashscope.ts +240 -0
- package/custom-providers/image/handlers/openrouter.ts +185 -0
- package/custom-providers/image/index.ts +96 -0
- package/custom-providers/image/models.ts +71 -0
- package/custom-providers/image/types.ts +22 -0
- package/custom-providers/index.ts +64 -0
- package/custom-providers/types.ts +17 -0
- package/image-generate/README.md +25 -0
- package/image-generate/files.ts +49 -0
- package/image-generate/index.ts +220 -0
- package/image-generate/model-selector.ts +150 -0
- package/memory-log/README.md +54 -0
- package/memory-log/agent/consolidator/prompt.ts +47 -0
- package/memory-log/agent/consolidator/tools.ts +160 -0
- package/memory-log/agent/cost.ts +32 -0
- package/memory-log/agent/index.ts +42 -0
- package/memory-log/agent/observer/prompt.ts +73 -0
- package/memory-log/agent/observer/tool.ts +73 -0
- package/memory-log/commands/compact.ts +35 -0
- package/memory-log/commands/consolidate.ts +47 -0
- package/memory-log/commands/status.ts +50 -0
- package/memory-log/config.ts +253 -0
- package/memory-log/debug-log.ts +76 -0
- package/memory-log/hooks/compaction-hook.ts +161 -0
- package/memory-log/hooks/compaction-trigger.ts +160 -0
- package/memory-log/hooks/consolidator-trigger.ts +143 -0
- package/memory-log/hooks/observer-trigger.ts +176 -0
- package/memory-log/ids.ts +78 -0
- package/memory-log/index.ts +149 -0
- package/memory-log/ledger/fold.ts +77 -0
- package/memory-log/ledger/index.ts +7 -0
- package/memory-log/ledger/pool.ts +38 -0
- package/memory-log/ledger/progress.ts +181 -0
- package/memory-log/ledger/projection.ts +115 -0
- package/memory-log/ledger/render.ts +38 -0
- package/memory-log/ledger/serialize.ts +161 -0
- package/memory-log/ledger/types.ts +201 -0
- package/memory-log/memory/index-render.ts +45 -0
- package/memory-log/memory/paths.ts +120 -0
- package/memory-log/memory/session.ts +64 -0
- package/memory-log/runtime.ts +127 -0
- package/memory-log/spawn/launch.ts +133 -0
- package/memory-log/spawn/runs.ts +83 -0
- package/memory-log/tokens.ts +66 -0
- package/memory-log/ui/status-controller.ts +225 -0
- package/memory-log/ui/timeline.ts +103 -0
- package/model-vision/README.md +22 -0
- package/multi-harness/README.md +63 -0
- package/multi-harness/adapters.mjs +72 -0
- package/multi-harness/harness-done.mjs +14 -0
- package/multi-harness/index.ts +375 -0
- package/multi-harness/ipc.mjs +37 -0
- package/multi-harness/opencode-plugin.mjs +99 -0
- package/multi-harness/process-runner.mjs +115 -0
- package/multi-harness/turn-hook.mjs +29 -0
- package/multi-harness/worker-bridge.ts +52 -0
- package/oai-subquota/README.md +22 -0
- package/oai-subquota/index.ts +286 -0
- package/oai-subquota/snapshot.ts +293 -0
- package/oai-subquota/ui.ts +183 -0
- package/package.json +92 -4
- package/prompt-snipaste/README.md +27 -0
- package/prompt-snipaste/index.ts +143 -0
- package/prompt-snipaste/menu.ts +159 -0
- package/prompt-snipaste/prompts/ask-questions.md +8 -0
- package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
- package/prompt-snipaste/prompts/diagnose-report.md +7 -0
- package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
- package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
- package/prompt-snipaste/prompts/session-kickoff.md +7 -0
- package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
- package/prompt-snipaste/renderers.ts +87 -0
- package/prompt-snipaste/snippets.ts +206 -0
- package/prompt-snipaste/types.ts +18 -0
- package/screenshot/README.md +14 -0
- package/srp-theme/README.md +18 -0
- package/srp-theme/footer.ts +193 -0
- package/srp-theme/index.ts +172 -0
- package/srp-theme/tps-meter.ts +441 -0
- package/subagent/README.md +35 -0
- package/subagent/bridge.ts +171 -0
- package/subagent/index.ts +590 -0
- package/subagent/mux.ts +621 -0
- package/subagent/session.ts +166 -0
- package/subagent/ui.ts +100 -0
- package/tui-asr/README.md +21 -0
- package/tui-asr/dictation.ts +272 -0
- package/tui-asr/index.ts +181 -0
- package/tui-asr/input.ts +87 -0
- package/tui-asr/model-selector.ts +163 -0
- package/tui-asr/status.ts +79 -0
- package/web-reach/README.md +19 -0
- package/web-reach/html.ts +83 -0
- package/web-reach/index.ts +82 -0
- package/web-reach/page.ts +214 -0
- package/web-reach/rsc.ts +161 -0
- package/web-reach/search.ts +85 -0
- package/web-reach/transport.ts +35 -0
- package/windows-wheel-fix/README.md +12 -0
- package/windows-wheel-fix/index.ts +44 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI observability for SRP Observational Memory.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type WorkerType = "observer" | "consolidator";
|
|
6
|
+
|
|
7
|
+
export interface FooterGauges {
|
|
8
|
+
/** Raw tokens accrued toward the next observer chunk. */
|
|
9
|
+
nextValue: number;
|
|
10
|
+
nextMax: number;
|
|
11
|
+
/** Active pool tokens accrued toward the consolidation threshold. */
|
|
12
|
+
poolValue: number;
|
|
13
|
+
poolMax: number;
|
|
14
|
+
/** Live context-window tokens toward the compaction threshold. */
|
|
15
|
+
ctxValue: number;
|
|
16
|
+
ctxMax: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Theme {
|
|
20
|
+
fg(color: string, text: string): string;
|
|
21
|
+
bg?(color: string, text: string): string;
|
|
22
|
+
bold?(text: string): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StatusUI {
|
|
26
|
+
setStatus(key: string, text: string | undefined): void;
|
|
27
|
+
setWidget(
|
|
28
|
+
key: string,
|
|
29
|
+
content: string[] | undefined,
|
|
30
|
+
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
31
|
+
): void;
|
|
32
|
+
theme: Theme;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type WorkerState =
|
|
36
|
+
| { kind: "running" }
|
|
37
|
+
| { kind: "done"; delta?: number }
|
|
38
|
+
| { kind: "error" };
|
|
39
|
+
|
|
40
|
+
const WIDGET_KEY = "memory-log";
|
|
41
|
+
const LEGACY_WIDGET_KEY = "srp-memory";
|
|
42
|
+
const LEGACY_WORKERS_WIDGET_KEY = "srp-memory-workers";
|
|
43
|
+
const LEGACY_STATUS_KEY = "srp-memory";
|
|
44
|
+
const SPINNER_FRAMES = ["◐", "◓", "◑", "◒"] as const;
|
|
45
|
+
const WORKER_SEP = " ";
|
|
46
|
+
|
|
47
|
+
export interface StatusControllerOptions {
|
|
48
|
+
spinnerIntervalMs?: number;
|
|
49
|
+
settleMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface WorkerEntry {
|
|
53
|
+
type: WorkerType;
|
|
54
|
+
state: WorkerState;
|
|
55
|
+
settleTimer?: ReturnType<typeof setTimeout>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class StatusController {
|
|
59
|
+
private ui: StatusUI | undefined;
|
|
60
|
+
private frame = 0;
|
|
61
|
+
private readonly workers = new Map<string, WorkerEntry>();
|
|
62
|
+
private spinnerTimer: ReturnType<typeof setInterval> | undefined;
|
|
63
|
+
private gauges: FooterGauges | undefined;
|
|
64
|
+
private cost: { costUsd: number; runs: number } | undefined;
|
|
65
|
+
private readonly spinnerIntervalMs: number;
|
|
66
|
+
private readonly settleMs: number;
|
|
67
|
+
|
|
68
|
+
constructor(options: StatusControllerOptions = {}) {
|
|
69
|
+
this.spinnerIntervalMs = options.spinnerIntervalMs ?? 120;
|
|
70
|
+
this.settleMs = options.settleMs ?? 5000;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
attach(ui: StatusUI): void {
|
|
74
|
+
this.ui = ui;
|
|
75
|
+
this.ui.setStatus(LEGACY_STATUS_KEY, undefined);
|
|
76
|
+
this.ui.setStatus(WIDGET_KEY, undefined);
|
|
77
|
+
this.ui.setWidget(LEGACY_WORKERS_WIDGET_KEY, undefined);
|
|
78
|
+
this.ui.setWidget(LEGACY_WIDGET_KEY, undefined);
|
|
79
|
+
this.renderWidget();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
detach(): void {
|
|
83
|
+
this.stopSpinner();
|
|
84
|
+
for (const entry of this.workers.values()) {
|
|
85
|
+
if (entry.settleTimer) clearTimeout(entry.settleTimer);
|
|
86
|
+
}
|
|
87
|
+
this.workers.clear();
|
|
88
|
+
this.gauges = undefined;
|
|
89
|
+
this.cost = undefined;
|
|
90
|
+
this.ui?.setWidget(WIDGET_KEY, undefined);
|
|
91
|
+
this.ui?.setWidget(LEGACY_WORKERS_WIDGET_KEY, undefined);
|
|
92
|
+
this.ui?.setWidget(LEGACY_WIDGET_KEY, undefined);
|
|
93
|
+
if (this.ui) this.ui.setStatus(LEGACY_STATUS_KEY, undefined);
|
|
94
|
+
if (this.ui) this.ui.setStatus(WIDGET_KEY, undefined);
|
|
95
|
+
this.ui = undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setGauges(gauges: FooterGauges | undefined): void {
|
|
99
|
+
this.gauges = gauges;
|
|
100
|
+
this.renderWidget();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setCost(costUsd: number, runs: number): void {
|
|
104
|
+
this.cost = { costUsd, runs };
|
|
105
|
+
this.renderWidget();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
workerStart(type: WorkerType, runId: string): void {
|
|
109
|
+
if (!this.ui) return;
|
|
110
|
+
const existing = this.workers.get(runId);
|
|
111
|
+
if (existing?.settleTimer) clearTimeout(existing.settleTimer);
|
|
112
|
+
this.workers.set(runId, { type, state: { kind: "running" } });
|
|
113
|
+
this.startSpinner();
|
|
114
|
+
this.renderWidget();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
workerDone(runId: string, delta?: number): void {
|
|
118
|
+
this.settle(runId, { kind: "done", delta });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
workerError(runId: string): void {
|
|
122
|
+
this.settle(runId, { kind: "error" });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private settle(runId: string, state: WorkerState): void {
|
|
126
|
+
if (!this.ui) return;
|
|
127
|
+
const entry = this.workers.get(runId);
|
|
128
|
+
if (!entry) return;
|
|
129
|
+
if (entry.settleTimer) clearTimeout(entry.settleTimer);
|
|
130
|
+
entry.state = state;
|
|
131
|
+
this.renderWidget();
|
|
132
|
+
entry.settleTimer = setTimeout(() => {
|
|
133
|
+
this.workers.delete(runId);
|
|
134
|
+
this.renderWidget();
|
|
135
|
+
if (!this.hasRunningWorker()) this.stopSpinner();
|
|
136
|
+
}, this.settleMs);
|
|
137
|
+
(entry.settleTimer as any)?.unref?.();
|
|
138
|
+
if (!this.hasRunningWorker()) this.stopSpinner();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private hasRunningWorker(): boolean {
|
|
142
|
+
for (const entry of this.workers.values()) {
|
|
143
|
+
if (entry.state.kind === "running") return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private startSpinner(): void {
|
|
149
|
+
if (this.spinnerTimer) return;
|
|
150
|
+
this.spinnerTimer = setInterval(() => {
|
|
151
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
152
|
+
if (this.hasRunningWorker()) this.renderWidget();
|
|
153
|
+
}, this.spinnerIntervalMs);
|
|
154
|
+
(this.spinnerTimer as any)?.unref?.();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private stopSpinner(): void {
|
|
158
|
+
if (!this.spinnerTimer) return;
|
|
159
|
+
clearInterval(this.spinnerTimer);
|
|
160
|
+
this.spinnerTimer = undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private gaugeBar(value: number, max: number, cells = 6): string {
|
|
164
|
+
const theme = this.ui!.theme;
|
|
165
|
+
const frac = max <= 0 ? 0 : Math.max(0, value / max);
|
|
166
|
+
const filled = Math.min(cells, Math.round(Math.min(1, frac) * cells));
|
|
167
|
+
const fillColor = frac >= 1 ? "warning" : "dim";
|
|
168
|
+
return (
|
|
169
|
+
theme.fg(fillColor, "▕") +
|
|
170
|
+
theme.fg(fillColor, "█".repeat(filled)) +
|
|
171
|
+
theme.fg(fillColor, "░".repeat(cells - filled)) +
|
|
172
|
+
theme.fg(fillColor, "▏")
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private renderWorkersLine(): string | undefined {
|
|
177
|
+
if (this.workers.size === 0) return undefined;
|
|
178
|
+
const theme = this.ui!.theme;
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
for (const entry of this.workers.values()) {
|
|
181
|
+
if (entry.state.kind === "running") {
|
|
182
|
+
parts.push(`${theme.fg("accent", SPINNER_FRAMES[this.frame])} ${theme.fg("accent", `[${entry.type}]`)}`);
|
|
183
|
+
} else if (entry.state.kind === "error") {
|
|
184
|
+
parts.push(`${theme.fg("error", "✗")} ${theme.fg("muted", `[${entry.type}]`)}`);
|
|
185
|
+
} else {
|
|
186
|
+
const delta =
|
|
187
|
+
entry.state.delta && entry.state.delta > 0
|
|
188
|
+
? ` ${theme.fg("success", `+${entry.state.delta}`)}`
|
|
189
|
+
: "";
|
|
190
|
+
parts.push(`${theme.fg("success", "✓")} ${theme.fg("muted", `[${entry.type}]`)}${delta}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return parts.length > 0 ? parts.join(WORKER_SEP) : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private renderMemLine(): string {
|
|
197
|
+
const theme = this.ui?.theme;
|
|
198
|
+
if (!theme) return "memory-log";
|
|
199
|
+
const base = `${theme.fg("success", "● mem")}`;
|
|
200
|
+
const g = this.gauges;
|
|
201
|
+
if (!g) return base;
|
|
202
|
+
const next = `${theme.fg("muted", "O")}${this.gaugeBar(g.nextValue, g.nextMax)}`;
|
|
203
|
+
const pool = `${theme.fg("muted", "C")}${this.gaugeBar(g.poolValue, g.poolMax)}`;
|
|
204
|
+
const ctx = `${theme.fg("muted", "X")}${this.gaugeBar(g.ctxValue, g.ctxMax)}`;
|
|
205
|
+
const cost = this.cost && this.cost.costUsd > 0 ? ` ${theme.fg("dim", `$${this.cost.costUsd.toFixed(3)}`)}` : "";
|
|
206
|
+
return `${base} ${next} ${pool} ${ctx}${cost}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private renderWidget(): void {
|
|
210
|
+
const ui = this.ui;
|
|
211
|
+
if (!ui) return;
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
const workersLine = this.renderWorkersLine();
|
|
215
|
+
if (workersLine) {
|
|
216
|
+
lines.push(workersLine);
|
|
217
|
+
}
|
|
218
|
+
const memLine = this.renderMemLine();
|
|
219
|
+
if (memLine) {
|
|
220
|
+
lines.push(memLine);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ui.setWidget(WIDGET_KEY, lines.length > 0 ? lines : undefined, { placement: "aboveEditor" });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
import {
|
|
3
|
+
entryIndexById,
|
|
4
|
+
foldLedger,
|
|
5
|
+
isObservationsRecordedEntry,
|
|
6
|
+
rawTokensAfterIndex,
|
|
7
|
+
rawTokensSinceObservationCoverage,
|
|
8
|
+
type Entry,
|
|
9
|
+
} from "../ledger/index.ts";
|
|
10
|
+
|
|
11
|
+
/** Glyphs for the timeline strip. */
|
|
12
|
+
const GLYPH = {
|
|
13
|
+
consolidated: "▓", // observations promoted to .memory (long-term)
|
|
14
|
+
partial: "▚", // chunk straddling the pool-target boundary (some obs promoted)
|
|
15
|
+
pool: "▒", // observed, still in the short-term buffer
|
|
16
|
+
raw: "░", // raw history not yet distilled into observations
|
|
17
|
+
cut: "┊", // compaction cutoff (verbatim tail began here)
|
|
18
|
+
tip: "▶", // live branch tip
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/** A committed observation chunk, in branch order. */
|
|
22
|
+
type Chunk = { coversUpToIndex: number; timestamps: string[] };
|
|
23
|
+
|
|
24
|
+
function fmtK(tokens: number): string {
|
|
25
|
+
if (tokens < 1000) return `${tokens}`;
|
|
26
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectChunks(branch: Entry[]): Chunk[] {
|
|
30
|
+
const indexById = entryIndexById(branch);
|
|
31
|
+
const chunks: Chunk[] = [];
|
|
32
|
+
for (const entry of branch) {
|
|
33
|
+
if (!isObservationsRecordedEntry(entry)) continue;
|
|
34
|
+
const coversUpToIndex = indexById.get(entry.data.coversUpToId);
|
|
35
|
+
if (coversUpToIndex === undefined) continue;
|
|
36
|
+
chunks.push({ coversUpToIndex, timestamps: entry.data.observations.map((o) => o.timestamp) });
|
|
37
|
+
}
|
|
38
|
+
return chunks;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Branch indices of every compaction entry's `firstKeptEntryId` (the verbatim-tail cutoffs). */
|
|
42
|
+
function compactionCutIndices(branch: Entry[]): number[] {
|
|
43
|
+
const indexById = entryIndexById(branch);
|
|
44
|
+
const cuts: number[] = [];
|
|
45
|
+
for (const entry of branch) {
|
|
46
|
+
if (entry.type !== "compaction") continue;
|
|
47
|
+
const idx = entry.firstKeptEntryId ? indexById.get(entry.firstKeptEntryId) : undefined;
|
|
48
|
+
if (idx !== undefined) cuts.push(idx);
|
|
49
|
+
}
|
|
50
|
+
return cuts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function chunkGlyph(chunk: Chunk, dropped: Set<string>): string {
|
|
54
|
+
let droppedCount = 0;
|
|
55
|
+
for (const ts of chunk.timestamps) if (dropped.has(ts)) droppedCount++;
|
|
56
|
+
if (droppedCount === 0) return GLYPH.pool;
|
|
57
|
+
if (droppedCount === chunk.timestamps.length) return GLYPH.consolidated;
|
|
58
|
+
return GLYPH.partial;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function wrap(cells: string[], width: number): string {
|
|
62
|
+
if (cells.length === 0) return "";
|
|
63
|
+
const rows: string[] = [];
|
|
64
|
+
for (let i = 0; i < cells.length; i += width) rows.push(cells.slice(i, i + width).join(""));
|
|
65
|
+
return rows.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render the full session as a horizontal strip.
|
|
70
|
+
*/
|
|
71
|
+
export function renderTimeline(branch: Entry[], config: Config, width = 60): string {
|
|
72
|
+
const folded = foldLedger(branch);
|
|
73
|
+
const dropped = folded.droppedObservationTimestamps;
|
|
74
|
+
const chunks = collectChunks(branch);
|
|
75
|
+
|
|
76
|
+
const cells: string[] = chunks.map((chunk) => chunkGlyph(chunk, dropped));
|
|
77
|
+
const tailTokens = rawTokensSinceObservationCoverage(branch);
|
|
78
|
+
const tailCells = tailTokens > 0 ? Math.ceil(tailTokens / config.chunkTokens) : 0;
|
|
79
|
+
for (let i = 0; i < tailCells; i++) cells.push(GLYPH.raw);
|
|
80
|
+
|
|
81
|
+
const cutPositions = compactionCutIndices(branch)
|
|
82
|
+
.map((cutIdx) => {
|
|
83
|
+
let pos = 0;
|
|
84
|
+
for (const chunk of chunks) if (chunk.coversUpToIndex < cutIdx) pos++;
|
|
85
|
+
return Math.min(pos, cells.length);
|
|
86
|
+
})
|
|
87
|
+
.sort((a, b) => b - a);
|
|
88
|
+
for (const pos of cutPositions) cells.splice(pos, 0, GLYPH.cut);
|
|
89
|
+
|
|
90
|
+
const consolidatedChunks = chunks.filter((c) => chunkGlyph(c, dropped) === GLYPH.consolidated).length;
|
|
91
|
+
const poolChunks = chunks.filter((c) => chunkGlyph(c, dropped) === GLYPH.pool).length;
|
|
92
|
+
const rawTotal = rawTokensAfterIndex(branch, -1);
|
|
93
|
+
const compactions = cutPositions.length;
|
|
94
|
+
|
|
95
|
+
const strip = cells.length > 0 ? `${wrap(cells, width)}${GLYPH.tip}` : "(timeline empty)";
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
`memory-log timeline · 1 cell ≈ ${fmtK(config.chunkTokens)} tok · cumulative ${fmtK(rawTotal)} raw · ${compactions} compaction${compactions === 1 ? "" : "s"}`,
|
|
99
|
+
strip,
|
|
100
|
+
``,
|
|
101
|
+
` ${GLYPH.consolidated} .memory (${consolidatedChunks}) ${GLYPH.pool} pool (${poolChunks}) ${GLYPH.raw} raw ${GLYPH.cut} compaction cut ${GLYPH.tip} tip`,
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# model-vision
|
|
2
|
+
|
|
3
|
+
Analyze a local image or public HTTP(S) image URL with a vision model. Registers the `model_vision` tool for OCR, UI inspection, and visual Q&A.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pi install npm:@rol1n/model-vision
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Available as a standalone package (not enabled in `@rol1n/pi-kit` by default).
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"image": "./screenshots/error.png",
|
|
18
|
+
"prompt": "Read the error message and describe the relevant UI."
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Supports PNG, JPEG, WebP, GIF, and BMP up to 10 MiB. Private/local network URLs are blocked. Default model fallback chain is configured in `DEFAULT_VISION_MODELS` in `index.ts`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Multi Harness
|
|
2
|
+
|
|
3
|
+
A small delegation runtime with one tool, `multi_harness`. `pi`, `codex`, `claude`, and `opencode` are equal CLI peers. The original `subagent` extension remains a separate, lean Pi-only tool.
|
|
4
|
+
|
|
5
|
+
The runtime accepts **one named worker and an existing cwd**. Those decisions belong to the caller. An independently installed `worktree-studio` skill can coordinate isolated implementations and their acceptance review.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi install npm:@rol1n/multi-harness
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Also included in `@rol1n/pi-kit`. Enable one copy of the extension.
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
Load `multi-harness` through the aggregate package, or run from this repository:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pi --no-extensions -e ./multi-harness/index.ts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Example tool calls:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{"action":"spawn","name":"reviewer","harness":"claude","cwd":"/existing/project","task":"Review error handling without changing files. Report concrete findings with file paths."}
|
|
27
|
+
{"action":"spawn","name":"implementer","harness":"pi","cwd":"/existing/other-directory","task":"Implement the approved fix and run the specified checks.","model":"provider/model"}
|
|
28
|
+
{"action":"spawn","name":"opencode-reviewer","harness":"opencode","cwd":"/existing/project","task":"Review the changes and report evidence without editing files."}
|
|
29
|
+
{"action":"send","target":"reviewer","task":"Review the revised implementation in the same session."}
|
|
30
|
+
{"action":"status"}
|
|
31
|
+
{"action":"stop","target":"implementer"}
|
|
32
|
+
{"action":"close","target":"reviewer"}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `spawn` is the default action; `name`, `harness`, and `task` are required. `cwd` defaults to the parent directory and must already exist. Names are reserved for the session, including after closing.
|
|
36
|
+
- `model` is optional and passed to the selected CLI. Pi defaults to the parent model; external CLIs use their installed defaults. Only the selected executable is checked on demand.
|
|
37
|
+
- `spawn` and `send` return after dispatch. **Turn completion is pushed to the real parent Pi session via `pi.sendMessage`**, not by a background wrapper writing a log. Do not poll terminals or run waiting shell loops.
|
|
38
|
+
- With tmux/Zellij, each worker runs its **native interactive TUI**. Turn settlement leaves the application open and idle. `send` continues that same application; `close` exits idle TUIs. `stop` terminates active work and its runtime, preserving files.
|
|
39
|
+
- Without a supported mux, workers use silent child processes. `send` starts another invocation that resumes the same persisted CLI session. Set `PI_MULTI_HARNESS_BACKEND=process` to force this fallback. This is not a live TUI in headless mode.
|
|
40
|
+
- Pi follow-ups use a file inbox and native `pi.sendUserMessage`. OpenCode uses a local server plugin, its native SDK inbox, and the same visible TUI session (no key injection). Codex uses its reported thread ID with `codex queue`; when no ID is available, mux input uses bracketed paste. Claude uses idle-only bracketed paste. Avoid simultaneous human typing and coordinator dispatch; startup trust/permission dialogs may require manual attention. Zellij input transport still requires validation with your installed Zellij/terminal build.
|
|
41
|
+
|
|
42
|
+
## Notifications and state
|
|
43
|
+
|
|
44
|
+
Artifacts live in `~/.pi/agent/multi-harness/<worker-id>/`: task prompts, CLI session data/IDs, console/event logs in process mode, completion records, and short TUI screen captures. Working files remain in the caller's `cwd`. No automatic Git report is generated.
|
|
45
|
+
|
|
46
|
+
`harness-done "summary"` is injected into the worker PATH; `studio-done` is a compatibility alias. It records a proposed summary, **not an early completion event**. Native Codex/Claude hooks, OpenCode's root-session idle events, Pi's `agent_settled`, or the headless process exit publish the authoritative completion. This avoids racing a still-running CLI or notifying twice. Every dispatch has a task ID; stale and duplicate completions are ignored.
|
|
47
|
+
|
|
48
|
+
The parent subscribes to directory `fs.watch` events, with no periodic terminal capture or completion polling. Screen capture happens once per completed turn. Watcher failures are surfaced rather than silently claiming delivery. A worker's final text is a claim: independently review its files and test results.
|
|
49
|
+
|
|
50
|
+
Default turn timeout: 15 minutes (`maxMinutes`: 1–120). Headless workers are stopped on timeout. A native TUI stays open and cannot receive a new dispatch until it settles or is stopped. Timeouts do not cap tokens or spending; missing usage/pricing is not zero cost.
|
|
51
|
+
|
|
52
|
+
Session entries record only worker artifact locations. On session restore/reload, the extension reattaches notification watchers for saved workers. A `controller.lock` prevents another session from controlling the same worker concurrently. After an unclean crash, verify the recorded owner PID is no longer running before manually removing that lock. Normal shutdown releases watchers/locks and stops active tasks; idle TUIs remain open and can be recovered by resuming the parent session.
|
|
53
|
+
|
|
54
|
+
## Permissions and platform boundaries
|
|
55
|
+
|
|
56
|
+
- This tool dispatches directly; user confirmation for expensive or risky workflows belongs to the caller/skill. It is not a sandbox.
|
|
57
|
+
- Per the configured workflow, Codex runs with `--sandbox danger-full-access -a never`, and Claude with `--permission-mode bypassPermissions`. Workers have the invoking user's filesystem/network privileges. Headless Claude has a $0.50 CLI API budget; interactive mode does not impose that limit. Use only in trusted environments.
|
|
58
|
+
- OpenCode uses `--auto`: automatically approve requests not explicitly denied by its configuration. Existing deny rules remain effective. Native mode injects the dependency-free local `opencode-plugin.mjs` through a merged `OPENCODE_CONFIG_CONTENT`; it never edits project/global config files or installs packages. Existing content must be valid JSON. Headless mode uses `opencode run --format json` and resumes by the exact reported `--session` ID. The adapter targets the OpenCode 1.x CLI/plugin API (source-checked against v1.19.3); 2.x API compatibility is not assumed. Install and authenticate the CLI yourself. DeepSeek Harness (`dsh`) is not integrated.
|
|
59
|
+
- Pi's project-resource trust flags apply only to loading project resources, not filesystem access. A different cwd does not inherit the parent's project trust automatically.
|
|
60
|
+
- Windows requires PowerShell 7 (`pwsh`) for external CLI invocation. Encoded PowerShell commands preserve Unicode, multiline tasks, JSON, and quoted paths. CLI hooks and `harness-done.cmd`/`.ps1` wrappers use the same native Windows Node installation. A mux must be available for visible TUIs; otherwise process fallback applies.
|
|
61
|
+
- Existing worktrees, reports, and sessions created by the removed `worktree-studio` extension are left untouched. Its old `the_road_not_taken` tool and two-route manifests are not imported into this runtime. Synchronize platform resources/reload before using the new tool; old running workers should be finished before upgrading.
|
|
62
|
+
|
|
63
|
+
Validation scripts are kept outside the repository. Check extension loading from this repository with `pi --no-extensions -e ./multi-harness/index.ts --list-models '<filter>'`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
export const HARNESSES = Object.freeze(["pi", "codex", "claude", "opencode"]);
|
|
5
|
+
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const quotePs = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
7
|
+
const quoteSh = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
8
|
+
|
|
9
|
+
/** Avoid cmd/npm shim argument re-parsing and preserve Unicode and JSON on Windows. */
|
|
10
|
+
export function commandSpec(argv, platform = process.platform) {
|
|
11
|
+
if (platform !== "win32") return { command: argv[0], args: argv.slice(1) };
|
|
12
|
+
const script = `$OutputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); & ${argv.map(quotePs).join(" ")}; if ($null -eq $LASTEXITCODE) { exit 1 }; exit $LASTEXITCODE`;
|
|
13
|
+
return { command: "pwsh", args: ["-NoProfile", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Add a dependency-free local plugin for native OpenCode only; never rewrite user config. */
|
|
17
|
+
export function buildEnv(config, inherited = process.env) {
|
|
18
|
+
if (config.harness !== "opencode" || !config.interactive) return inherited;
|
|
19
|
+
let content;
|
|
20
|
+
try { content = inherited.OPENCODE_CONFIG_CONTENT ? JSON.parse(inherited.OPENCODE_CONFIG_CONTENT) : {}; }
|
|
21
|
+
catch { throw new Error("OPENCODE_CONFIG_CONTENT must be valid JSON to append the multi-harness plugin."); }
|
|
22
|
+
if (!content || typeof content !== "object" || Array.isArray(content) || (content.plugin !== undefined && !Array.isArray(content.plugin))) {
|
|
23
|
+
throw new Error("OpenCode config must be an object with an optional plugin array.");
|
|
24
|
+
}
|
|
25
|
+
const plugin = pathToFileURL(join(extensionDir, "opencode-plugin.mjs")).href;
|
|
26
|
+
return { ...inherited, PI_MULTI_HARNESS_CWD: config.cwd,
|
|
27
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({ ...content, plugin: [...(content.plugin ?? []).filter((item) => item !== plugin), plugin] }) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildArgs(config) {
|
|
31
|
+
const { harness, cwd, prompt, sessionFile, model, trusted, artifactDir, interactive, resume, sessionId } = config;
|
|
32
|
+
const modelArgs = model ? ["--model", model] : [];
|
|
33
|
+
switch (harness) {
|
|
34
|
+
case "pi":
|
|
35
|
+
return ["pi", ...(interactive ? ["--tui-mode", "regular", "-e", join(extensionDir, "worker-bridge.ts")] : ["--print"]),
|
|
36
|
+
trusted ? "--approve" : "--no-approve", "--session", sessionFile, ...modelArgs, prompt];
|
|
37
|
+
case "codex": {
|
|
38
|
+
const base = ["codex", "--sandbox", "danger-full-access", "-a", "never"];
|
|
39
|
+
if (interactive) {
|
|
40
|
+
const hook = JSON.stringify([process.execPath, join(extensionDir, "turn-hook.mjs"), artifactDir, "codex"]);
|
|
41
|
+
return [...base, "--cd", cwd, ...modelArgs, "-c", `notify=${hook}`, prompt];
|
|
42
|
+
}
|
|
43
|
+
return [...base, "exec", ...(resume ? ["resume", sessionId] : ["--cd", cwd]), "--skip-git-repo-check", "--json", ...modelArgs, "-"];
|
|
44
|
+
}
|
|
45
|
+
case "claude": {
|
|
46
|
+
const base = ["claude", "--permission-mode", "bypassPermissions", ...modelArgs];
|
|
47
|
+
if (interactive) {
|
|
48
|
+
const hook = (kind) => {
|
|
49
|
+
const args = [process.execPath, join(extensionDir, "turn-hook.mjs"), artifactDir, kind];
|
|
50
|
+
// Claude command hooks use a shell, not necessarily the interactive pane's shell.
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
const script = `& ${args.map(quotePs).join(" ")}; exit $LASTEXITCODE`;
|
|
53
|
+
return `pwsh -NoProfile -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
|
|
54
|
+
}
|
|
55
|
+
return args.map(quoteSh).join(" ");
|
|
56
|
+
};
|
|
57
|
+
const settings = { hooks: {
|
|
58
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: hook("claude-start"), timeout: 10 }] }],
|
|
59
|
+
Stop: [{ hooks: [{ type: "command", command: hook("claude"), timeout: 10 }] }],
|
|
60
|
+
} };
|
|
61
|
+
return [...base, "--session-id", sessionId, "--settings", JSON.stringify(settings), prompt];
|
|
62
|
+
}
|
|
63
|
+
return [...base, "-p", "--verbose", "--output-format", "stream-json", "--max-budget-usd", "0.50",
|
|
64
|
+
...(resume ? ["--resume", sessionId] : ["--session-id", sessionId])];
|
|
65
|
+
}
|
|
66
|
+
case "opencode":
|
|
67
|
+
return interactive
|
|
68
|
+
? ["opencode", cwd, "--auto", ...modelArgs, "--prompt", prompt]
|
|
69
|
+
: ["opencode", "run", "--dir", cwd, "--format", "json", "--auto", ...modelArgs, ...(resume ? ["--session", sessionId] : [])];
|
|
70
|
+
default: throw new Error(`Unsupported harness: ${harness}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readJson, writeJson } from "./ipc.mjs";
|
|
4
|
+
|
|
5
|
+
const directory = process.env.PI_MULTI_HARNESS_DIR;
|
|
6
|
+
if (!directory) throw new Error("harness-done must run inside a multi_harness worker.");
|
|
7
|
+
const request = readJson(join(directory, "request.json"));
|
|
8
|
+
if (!request) throw new Error("No task is assigned to this worker.");
|
|
9
|
+
writeJson(join(directory, "proposal.json"), {
|
|
10
|
+
id: request.id,
|
|
11
|
+
summary: process.argv.slice(2).join(" ").trim() || "Task finished; inspect the artifacts for verification.",
|
|
12
|
+
at: Date.now(),
|
|
13
|
+
});
|
|
14
|
+
console.log("Summary recorded. The coordinator will be notified when this turn settles; keep the TUI open.");
|