@hobin/developer 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.
@@ -0,0 +1,391 @@
1
+ import {
2
+ DynamicBorder,
3
+ type ExtensionCommandContext,
4
+ type Theme,
5
+ type ThemeColor,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ Container,
9
+ matchesKey,
10
+ type SelectItem,
11
+ SelectList,
12
+ Text,
13
+ truncateToWidth,
14
+ visibleWidth,
15
+ wrapTextWithAnsi,
16
+ } from "@earendil-works/pi-tui";
17
+
18
+ import {
19
+ protocolState,
20
+ type DeveloperMode,
21
+ type DeveloperState,
22
+ type PendingQuestion,
23
+ type ProtocolState,
24
+ } from "./state.ts";
25
+
26
+ export type DeveloperAction = "status" | "questions" | DeveloperMode;
27
+
28
+ function modeName(mode: DeveloperMode): string {
29
+ if (mode === "on") return "adaptive";
30
+ return mode;
31
+ }
32
+
33
+ function protocolColor(value: ProtocolState): ThemeColor {
34
+ if (value === "blocked") return "error";
35
+ if (value === "needs-evidence") return "warning";
36
+ if (value === "needs-judgment") return "accent";
37
+ return "dim";
38
+ }
39
+
40
+ function modeColor(mode: DeveloperMode): ThemeColor {
41
+ if (mode === "strict") return "warning";
42
+ if (mode === "on") return "accent";
43
+ return "dim";
44
+ }
45
+
46
+ export function renderDeveloperFooter(state: DeveloperState, theme: Theme): string {
47
+ const currentProtocol = protocolState(state);
48
+ const target = state.activeRoute?.owner ?? "none";
49
+ return (
50
+ theme.fg("accent", "developer") +
51
+ theme.fg("dim", " · ") +
52
+ theme.fg(modeColor(state.mode), modeName(state.mode)) +
53
+ theme.fg("dim", " · ") +
54
+ theme.fg(protocolColor(currentProtocol), currentProtocol) +
55
+ theme.fg("dim", ` · ${target}`)
56
+ );
57
+ }
58
+
59
+ export function developerActionItems(state: DeveloperState): SelectItem[] {
60
+ const currentProtocol = protocolState(state);
61
+ const items: SelectItem[] = [
62
+ {
63
+ value: "status",
64
+ label: "Inspect status",
65
+ description: `${modeName(state.mode)} · ${currentProtocol} · ${state.pendingQuestions.length} open`,
66
+ },
67
+ ];
68
+ if (state.pendingQuestions.length > 0) {
69
+ items.push({
70
+ value: "questions",
71
+ label: "Revisit an open question",
72
+ description: `Choose from ${state.pendingQuestions.length} unresolved question(s) and prepare the editor`,
73
+ });
74
+ }
75
+ items.push(
76
+ {
77
+ value: "on",
78
+ label: state.mode === "on" ? "Adaptive mode (active)" : "Adaptive mode",
79
+ description: "Route judgments adaptively while preserving the current active tool set",
80
+ },
81
+ {
82
+ value: "strict",
83
+ label: state.mode === "strict" ? "Strict mode (active)" : "Strict mode",
84
+ description: "Require a direct route before Pi built-in mutation tools become active",
85
+ },
86
+ {
87
+ value: "off",
88
+ label: state.mode === "off" ? "Off (active)" : "Turn off",
89
+ description: "Clear Developer protocol state and remove its persistent UI",
90
+ },
91
+ );
92
+ return items;
93
+ }
94
+
95
+ export function pendingQuestionItems(questions: PendingQuestion[]): SelectItem[] {
96
+ return questions.map((question) => ({
97
+ value: question.id,
98
+ label: question.question,
99
+ description: `${question.status} · ${question.id}`,
100
+ }));
101
+ }
102
+
103
+ interface SelectDialogOptions {
104
+ title: string;
105
+ subtitle: string;
106
+ items: SelectItem[];
107
+ width: number;
108
+ maxVisible: number;
109
+ maxPrimaryColumnWidth?: number;
110
+ }
111
+
112
+ async function showSelectDialog(
113
+ ctx: ExtensionCommandContext,
114
+ options: SelectDialogOptions,
115
+ ): Promise<string | undefined> {
116
+ const result = await ctx.ui.custom<string | null>(
117
+ (tui, theme, _keybindings, done) => {
118
+ const container = new Container();
119
+ const title = new Text("", 1, 0);
120
+ const subtitle = new Text("", 1, 0);
121
+ const hint = new Text("", 1, 0);
122
+ const updateText = () => {
123
+ title.setText(theme.fg("accent", theme.bold(options.title)));
124
+ subtitle.setText(theme.fg("muted", options.subtitle));
125
+ hint.setText(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"));
126
+ };
127
+ updateText();
128
+
129
+ const list = new SelectList(
130
+ options.items,
131
+ Math.min(options.items.length, options.maxVisible),
132
+ {
133
+ selectedPrefix: (text) => theme.fg("accent", text),
134
+ selectedText: (text) => theme.fg("accent", text),
135
+ description: (text) => theme.fg("muted", text),
136
+ scrollInfo: (text) => theme.fg("dim", text),
137
+ noMatch: (text) => theme.fg("warning", text),
138
+ },
139
+ options.maxPrimaryColumnWidth
140
+ ? { minPrimaryColumnWidth: 24, maxPrimaryColumnWidth: options.maxPrimaryColumnWidth }
141
+ : undefined,
142
+ );
143
+ list.onSelect = (item) => done(item.value);
144
+ list.onCancel = () => done(null);
145
+
146
+ container.addChild(new DynamicBorder((text) => theme.fg("borderAccent", text)));
147
+ container.addChild(title);
148
+ container.addChild(subtitle);
149
+ container.addChild(list);
150
+ container.addChild(hint);
151
+ container.addChild(new DynamicBorder((text) => theme.fg("borderAccent", text)));
152
+
153
+ return {
154
+ render(width: number) {
155
+ return container.render(width);
156
+ },
157
+ invalidate() {
158
+ updateText();
159
+ container.invalidate();
160
+ },
161
+ handleInput(data: string) {
162
+ list.handleInput(data);
163
+ tui.requestRender();
164
+ },
165
+ };
166
+ },
167
+ {
168
+ overlay: true,
169
+ overlayOptions: {
170
+ anchor: "center",
171
+ width: options.width,
172
+ maxHeight: Math.min(options.maxVisible + 7, 20),
173
+ margin: 1,
174
+ },
175
+ },
176
+ );
177
+ return result ?? undefined;
178
+ }
179
+
180
+ export async function showDeveloperActionSelector(
181
+ ctx: ExtensionCommandContext,
182
+ state: DeveloperState,
183
+ ): Promise<DeveloperAction | undefined> {
184
+ const result = await showSelectDialog(ctx, {
185
+ title: "Developer control",
186
+ subtitle: `Current · ${modeName(state.mode)} · ${protocolState(state)}`,
187
+ items: developerActionItems(state),
188
+ width: 78,
189
+ maxVisible: 6,
190
+ });
191
+ if (result === "status" || result === "questions" || result === "on" || result === "strict" || result === "off") {
192
+ return result;
193
+ }
194
+ return undefined;
195
+ }
196
+
197
+ export async function showPendingQuestionSelector(
198
+ ctx: ExtensionCommandContext,
199
+ questions: PendingQuestion[],
200
+ ): Promise<string | undefined> {
201
+ if (questions.length === 0) return undefined;
202
+ return showSelectDialog(ctx, {
203
+ title: "Revisit an open Developer question",
204
+ subtitle: "Selection prepares an editable prompt; protocol state changes only after the route is judged",
205
+ items: pendingQuestionItems(questions),
206
+ width: 96,
207
+ maxVisible: 10,
208
+ maxPrimaryColumnWidth: 52,
209
+ });
210
+ }
211
+
212
+ export class DeveloperWidget {
213
+ private readonly state: DeveloperState;
214
+ private readonly theme: Theme;
215
+
216
+ constructor(state: DeveloperState, theme: Theme) {
217
+ this.state = state;
218
+ this.theme = theme;
219
+ }
220
+
221
+ render(width: number): string[] {
222
+ const lines: string[] = [];
223
+ if (this.state.activeRoute) {
224
+ lines.push(
225
+ truncateToWidth(
226
+ `${this.theme.fg("accent", "◆ route")} ${this.theme.fg("muted", "·")} ${this.theme.fg("accent", this.state.activeRoute.owner)} ${this.theme.fg("muted", this.state.activeRoute.question)}`,
227
+ width,
228
+ "…",
229
+ ),
230
+ );
231
+ }
232
+ for (const question of this.state.pendingQuestions.slice(0, 3)) {
233
+ lines.push(
234
+ truncateToWidth(
235
+ `${this.theme.fg(question.status === "blocked" ? "error" : "warning", "? open")} ${this.theme.fg("dim", "·")} ${this.theme.fg("muted", question.question)}`,
236
+ width,
237
+ "…",
238
+ ),
239
+ );
240
+ }
241
+ if (this.state.pendingQuestions.length > 3) {
242
+ lines.push(this.theme.fg("dim", ` +${this.state.pendingQuestions.length - 3} more open questions`));
243
+ }
244
+ return lines;
245
+ }
246
+
247
+ invalidate(): void {}
248
+ }
249
+
250
+ export interface DeveloperStatusView {
251
+ state: DeveloperState;
252
+ activeTools: string[];
253
+ availableSkills: string[];
254
+ }
255
+
256
+ export class DeveloperStatusPanel {
257
+ private cachedWidth?: number;
258
+ private cachedLines?: string[];
259
+ private readonly view: DeveloperStatusView;
260
+ private readonly theme: Theme;
261
+ private readonly onClose: () => void;
262
+
263
+ constructor(view: DeveloperStatusView, theme: Theme, onClose: () => void) {
264
+ this.view = view;
265
+ this.theme = theme;
266
+ this.onClose = onClose;
267
+ }
268
+
269
+ handleInput(data: string): void {
270
+ if (matchesKey(data, "escape") || matchesKey(data, "enter") || matchesKey(data, "ctrl+c")) {
271
+ this.onClose();
272
+ }
273
+ }
274
+
275
+ render(width: number): string[] {
276
+ if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
277
+
278
+ const panelWidth = Math.max(20, width);
279
+ const innerWidth = Math.max(18, panelWidth - 2);
280
+ const rows: string[] = [];
281
+ const border = (text: string) => this.theme.fg("borderMuted", text);
282
+ const row = (content = "") => `${border("│")}${truncateToWidth(content, innerWidth, "…", true)}${border("│")}`;
283
+ const addWrapped = (label: string, value: string, color: ThemeColor = "muted", maxLines = 2) => {
284
+ const prefix = ` ${this.theme.fg("dim", `${label} ·`)} `;
285
+ const wrapped = wrapTextWithAnsi(prefix + this.theme.fg(color, value), innerWidth).slice(0, maxLines);
286
+ for (const line of wrapped) rows.push(row(line));
287
+ };
288
+ const section = (title: string) => rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
289
+
290
+ rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
291
+ rows.push(row(` ${this.theme.fg("accent", this.theme.bold("Developer status"))}`));
292
+ rows.push(row());
293
+
294
+ const state = this.view.state;
295
+ const currentProtocol = protocolState(state);
296
+ rows.push(
297
+ row(
298
+ ` mode ${this.theme.fg(modeColor(state.mode), modeName(state.mode))}` +
299
+ this.theme.fg("dim", " · ") +
300
+ `protocol ${this.theme.fg(protocolColor(currentProtocol), currentProtocol)}` +
301
+ this.theme.fg("dim", " · ") +
302
+ `target ${this.theme.fg("muted", state.activeRoute?.owner ?? "none")}`,
303
+ ),
304
+ );
305
+ rows.push(row());
306
+
307
+ section("Active route");
308
+ if (state.activeRoute) {
309
+ addWrapped("id", state.activeRoute.routeId, "dim", 1);
310
+ addWrapped("question", state.activeRoute.question, "text");
311
+ addWrapped("reason", state.activeRoute.reason);
312
+ addWrapped("skill", state.activeRoute.methodLocation ?? "direct action", "dim", 1);
313
+ addWrapped("known evidence", String(state.activeRoute.knownEvidence.length), "muted", 1);
314
+ } else {
315
+ addWrapped("state", "No route is currently waiting for judgment.", "dim", 1);
316
+ }
317
+
318
+ rows.push(row());
319
+ section(`Open questions · ${state.pendingQuestions.length}`);
320
+ if (state.pendingQuestions.length === 0) {
321
+ addWrapped("state", "No unresolved Developer questions.", "dim", 1);
322
+ } else {
323
+ for (const question of state.pendingQuestions.slice(0, 4)) {
324
+ addWrapped(
325
+ question.status === "blocked" ? "blocked" : "needs evidence",
326
+ question.question,
327
+ question.status === "blocked" ? "error" : "warning",
328
+ 1,
329
+ );
330
+ }
331
+ if (state.pendingQuestions.length > 4) {
332
+ addWrapped("more", String(state.pendingQuestions.length - 4), "dim", 1);
333
+ }
334
+ }
335
+
336
+ rows.push(row());
337
+ section("Last judgment");
338
+ if (state.lastJudgment) {
339
+ addWrapped("status", state.lastJudgment.status, protocolColor(
340
+ state.lastJudgment.status === "blocked"
341
+ ? "blocked"
342
+ : state.lastJudgment.status === "needs-evidence"
343
+ ? "needs-evidence"
344
+ : "idle",
345
+ ), 1);
346
+ addWrapped("result", state.lastJudgment.result, "muted");
347
+ addWrapped(
348
+ "evidence",
349
+ `${state.lastJudgment.basis.length} basis · ${state.lastJudgment.artifacts.length} artifacts`,
350
+ "dim",
351
+ 1,
352
+ );
353
+ } else {
354
+ addWrapped("state", "No judgment has been recorded on this branch.", "dim", 1);
355
+ }
356
+
357
+ rows.push(row());
358
+ addWrapped(
359
+ "resources",
360
+ `${this.view.availableSkills.length} skills · ${this.view.activeTools.length} active tools`,
361
+ "dim",
362
+ 1,
363
+ );
364
+ rows.push(row());
365
+ rows.push(row(` ${this.theme.fg("dim", "enter/esc close · /develop questions revisits open work")}`));
366
+ rows.push(border(`╰${"─".repeat(innerWidth)}╯`));
367
+
368
+ this.cachedWidth = width;
369
+ this.cachedLines = rows;
370
+ return rows;
371
+ }
372
+
373
+ invalidate(): void {
374
+ this.cachedWidth = undefined;
375
+ this.cachedLines = undefined;
376
+ }
377
+ }
378
+
379
+ export async function showDeveloperStatus(
380
+ ctx: ExtensionCommandContext,
381
+ view: DeveloperStatusView,
382
+ ): Promise<void> {
383
+ await ctx.ui.custom<void>((_tui, theme, _keybindings, done) =>
384
+ new DeveloperStatusPanel(view, theme, () => done()));
385
+ }
386
+
387
+ export function prepareQuestionPrompt(ctx: ExtensionCommandContext, question: PendingQuestion): void {
388
+ const prompt = `Revisit Developer question ${question.id}: ${question.question}`;
389
+ const current = ctx.ui.getEditorText();
390
+ ctx.ui.setEditorText(current.trim() ? `${current.trimEnd()}\n\n${prompt}` : prompt);
391
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@hobin/developer",
3
+ "version": "0.1.0",
4
+ "description": "Adaptive product-development reasoning and evidence for Pi.",
5
+ "type": "module",
6
+ "keywords": ["pi-package", "pi", "developer", "skills"],
7
+ "license": "MIT",
8
+ "author": {
9
+ "name": "dev-hobin",
10
+ "url": "https://github.com/dev-hobin"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/dev-hobin/agent.git",
15
+ "directory": "packages/developer"
16
+ },
17
+ "homepage": "https://github.com/dev-hobin/agent/tree/main/packages/developer#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/dev-hobin/agent/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": ["extensions", "skills", "README.md", "LICENSE"],
25
+ "engines": {
26
+ "node": ">=22.19.0"
27
+ },
28
+ "scripts": {
29
+ "check": "node scripts/check-package.mjs && node --test tests/*.test.ts",
30
+ "eval": "node scripts/eval-rpc.mjs",
31
+ "eval:json": "node scripts/eval-json.mjs"
32
+ },
33
+ "pi": {
34
+ "extensions": ["./extensions/developer.ts"],
35
+ "skills": ["./skills"]
36
+ },
37
+ "peerDependencies": {
38
+ "@earendil-works/pi-ai": "*",
39
+ "@earendil-works/pi-coding-agent": "*",
40
+ "@earendil-works/pi-tui": "*",
41
+ "typebox": "*"
42
+ }
43
+ }
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: abstraction-review
3
+ description: "Judge whether a concrete wished interface, helper, API, workflow rule, boundary, or structural candidate is stable enough to keep, revise, split, reject, or defer. Use when a concrete candidate and the pressure behind it can be inspected, including when its contract or evidence may be incomplete."
4
+ ---
5
+
6
+ # Abstraction Review
7
+
8
+ Decide whether a concrete abstraction candidate is safe to rely on.
9
+
10
+ ## Core Question
11
+
12
+ Is this candidate stable enough to keep, or should it be revised, split,
13
+ rejected, or deferred?
14
+
15
+ ## Inputs
16
+
17
+ - Concrete candidate and the pressure it should remove
18
+ - Caller language, representative cases, and expected contract
19
+ - Hidden detail and failure mode
20
+ - Requirement, model, design, code, diff, or tests when available
21
+
22
+ ## Output
23
+
24
+ Lead with the caller's concrete failure mode; keep review labels secondary.
25
+ Produce review timing, layer, contract, hidden detail, output artifact, stop
26
+ check, evidence and gaps, and one decision: `keep`, `revise-surface`,
27
+ `revise-model`, `split`, `reject`, or `defer`. When used inside a larger task,
28
+ return:
29
+
30
+ ```text
31
+ Status: resolved | needs-evidence | not-applicable | blocked
32
+ Result: review decision and its concrete failure mode
33
+ Basis: candidate source, callers, cases, code, tests, and assumptions
34
+ Open questions: missing contract or evidence, or none
35
+ Artifacts: boundary, invariant, table, graph, trace, or repair rule
36
+ ```
37
+
38
+ Return only this skill's judgment for the question at hand; leave subsequent
39
+ routing to the caller.
40
+
41
+ ## Completion
42
+
43
+ Finish when the candidate can satisfy this sentence or is rejected/deferred for
44
+ failing it:
45
+
46
+ ```text
47
+ Given <input artifact>, derive <output artifact> by <rule>, then check
48
+ <observable stop>. If it fails, repair the named layer or contract.
49
+ ```
50
+
51
+ Revisit when new callers, cases, implementation evidence, or failures change
52
+ the contract or stop check.
53
+
54
+ ## Method
55
+
56
+ 1. Identify the candidate, its source, and confidence.
57
+ 2. State the pressure it is supposed to remove. Bias toward `reject` or `defer`
58
+ when no pressure is visible.
59
+ 3. Review pre-implementation intent or post-implementation evidence explicitly.
60
+ 4. Choose the tested layer: Language, Unit, Law, Boundary, Engine, Time, or Run.
61
+ 5. State the contract in caller language and the detail callers may ignore.
62
+ 6. Name the reusable output artifact left by the review.
63
+ 7. Define an observable stop check using cases, properties, tests, diffs,
64
+ behavior, traces, or command output.
65
+ 8. Decide without converting missing evidence into polished approval.
66
+
67
+ ## Missing Evidence
68
+
69
+ Return `needs-evidence` when a caller, representative case, or stop check can
70
+ stabilize the judgment. Return `not-applicable` when there is no concrete
71
+ candidate yet. Return `blocked` when product meaning needed for the contract is
72
+ human-owned.
73
+
74
+ ## Boundary
75
+
76
+ Do not discover structural movement, create the original design surface, decide
77
+ timing, implement the change, or perform final completion review.
78
+
79
+ ## References
80
+
81
+ - Read [the field card](references/field-card.md) for substantial or auditable
82
+ reviews.
83
+ - Read [the recipe cards](references/recipe-cards.md) when the candidate needs a construction or
84
+ repair rule.
85
+ - Read [the repair table](references/repair-table.md) when a stop check fails.
86
+ - Read [the worked examples](references/worked-examples.md) for concrete
87
+ calibration.