@hobin/developer 0.1.1 → 0.1.3

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 (33) hide show
  1. package/README.md +61 -20
  2. package/SOURCES.md +104 -0
  3. package/extensions/developer.ts +202 -44
  4. package/extensions/references/behavior-preserving-structural-change.md +149 -0
  5. package/extensions/skills.ts +1 -16
  6. package/extensions/state.ts +96 -10
  7. package/extensions/tui.ts +140 -25
  8. package/package.json +11 -25
  9. package/skills/abstraction-review/references/field-card.md +7 -0
  10. package/skills/abstraction-review/references/recipe-cards.md +67 -0
  11. package/skills/abstraction-review/references/repair-table.md +5 -0
  12. package/skills/model/SKILL.md +6 -1
  13. package/skills/model/references/problem-modeling.md +294 -235
  14. package/skills/model/references/worked-models-and-specialized-techniques.md +218 -0
  15. package/skills/naming-judgment/SKILL.md +5 -3
  16. package/skills/naming-judgment/references/domain-naming.md +202 -0
  17. package/skills/schedule/SKILL.md +1 -1
  18. package/skills/schedule/references/structural-change-timing.md +80 -13
  19. package/skills/signal/SKILL.md +2 -2
  20. package/skills/signal/references/structural-movement.md +202 -0
  21. package/skills/sketch/SKILL.md +35 -8
  22. package/skills/sketch/references/abstraction-barriers-and-closure.md +160 -0
  23. package/skills/sketch/references/abstraction-composition-and-state.md +184 -0
  24. package/skills/sketch/references/composition-generative-recursion-and-accumulators.md +196 -0
  25. package/skills/sketch/references/data-driven-design.md +177 -0
  26. package/skills/sketch/references/data-shape-template-catalog.md +241 -0
  27. package/skills/sketch/references/generic-operations-and-languages.md +134 -0
  28. package/skills/sketch/references/processes-state-and-time.md +171 -0
  29. package/skills/sketch/references/responsibility-and-variation.md +245 -0
  30. package/skills/verify/references/verifier-selection-and-pass-but-wrong.md +61 -3
  31. package/skills/naming-judgment/references/elements-of-clojure-naming.md +0 -74
  32. package/skills/signal/references/flocking-and-structural-movement.md +0 -157
  33. package/skills/sketch/references/design-recipe-and-abstraction-barriers.md +0 -169
@@ -1,6 +1,8 @@
1
- export const PROTOCOL = "developer/v2" as const;
1
+ export const PROTOCOL = "developer/v3" as const;
2
+ export const PREVIOUS_PROTOCOL = "developer/v2" as const;
2
3
  export const LEGACY_PROTOCOL = "developer/v1" as const;
3
4
  export const MODE_ENTRY = "developer.mode" as const;
5
+ export const FOCUS_ENTRY = "developer.question-focus" as const;
4
6
  export const ROUTE_TOOL = "developer_route_question" as const;
5
7
  export const JUDGMENT_TOOL = "developer_record_judgment" as const;
6
8
  export const LEGACY_ROUTE_TOOL = "route_question" as const;
@@ -9,6 +11,7 @@ export const LEGACY_JUDGMENT_TOOL = "record_judgment" as const;
9
11
  export type DeveloperMode = "off" | "on" | "strict";
10
12
  export type JudgmentStatus = "resolved" | "needs-evidence" | "not-applicable" | "blocked";
11
13
  export type PendingQuestionStatus = "needs-evidence" | "blocked";
14
+ export type DirectExecutionProfile = "ordinary" | "behavior-preserving-structure";
12
15
 
13
16
  export interface ModeEvent {
14
17
  protocol: typeof PROTOCOL;
@@ -16,6 +19,18 @@ export interface ModeEvent {
16
19
  mode: DeveloperMode;
17
20
  }
18
21
 
22
+ export interface FocusEvent {
23
+ protocol: typeof PROTOCOL;
24
+ kind: "focus";
25
+ questionId: string;
26
+ }
27
+
28
+ export interface DirectStepContract {
29
+ movement: string;
30
+ stopCondition: string;
31
+ verification: string;
32
+ }
33
+
19
34
  export interface RouteEvent {
20
35
  protocol: typeof PROTOCOL;
21
36
  kind: "route";
@@ -26,6 +41,8 @@ export interface RouteEvent {
26
41
  knownEvidence: string[];
27
42
  targetQuestionId?: string;
28
43
  methodLocation?: string;
44
+ executionProfile?: DirectExecutionProfile;
45
+ directStep?: DirectStepContract;
29
46
  }
30
47
 
31
48
  export interface PendingQuestion {
@@ -46,9 +63,10 @@ export interface JudgmentEvent {
46
63
  basis: string[];
47
64
  openedQuestions: PendingQuestion[];
48
65
  artifacts: string[];
66
+ changedArtifacts: boolean;
49
67
  }
50
68
 
51
- export type DeveloperEvent = ModeEvent | RouteEvent | JudgmentEvent;
69
+ export type DeveloperEvent = ModeEvent | FocusEvent | RouteEvent | JudgmentEvent;
52
70
 
53
71
  export interface DeveloperState {
54
72
  mode: DeveloperMode;
@@ -56,19 +74,25 @@ export interface DeveloperState {
56
74
  lastRoute?: RouteEvent;
57
75
  lastJudgment?: JudgmentEvent;
58
76
  pendingQuestions: PendingQuestion[];
77
+ focusedQuestionId?: string;
78
+ implementationFramingRequired: boolean;
79
+ verificationRequired: boolean;
59
80
  }
60
81
 
61
82
  export const initialState = (): DeveloperState => ({
62
83
  mode: "off",
63
84
  pendingQuestions: [],
85
+ implementationFramingRequired: false,
86
+ verificationRequired: false,
64
87
  });
65
88
 
66
- export type ProtocolState = "idle" | "needs-judgment" | "needs-evidence" | "blocked";
89
+ export type ProtocolState = "idle" | "needs-judgment" | "needs-evidence" | "needs-verification" | "blocked";
67
90
 
68
91
  export function protocolState(state: DeveloperState): ProtocolState {
69
92
  if (state.activeRoute) return "needs-judgment";
70
93
  if (state.pendingQuestions.some((question) => question.status === "blocked")) return "blocked";
71
94
  if (state.pendingQuestions.length > 0) return "needs-evidence";
95
+ if (state.verificationRequired) return "needs-verification";
72
96
  return "idle";
73
97
  }
74
98
 
@@ -82,12 +106,19 @@ export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent
82
106
  return { ...state, mode: event.mode };
83
107
  }
84
108
 
109
+ if (event.kind === "focus") {
110
+ if (!state.pendingQuestions.some((question) => question.id === event.questionId)) return state;
111
+ return { ...state, focusedQuestionId: event.questionId };
112
+ }
113
+
85
114
  if (event.kind === "route") {
86
115
  if (state.activeRoute) return state;
87
116
  return {
88
117
  ...state,
89
118
  activeRoute: event,
90
119
  lastRoute: event,
120
+ focusedQuestionId:
121
+ event.targetQuestionId === state.focusedQuestionId ? undefined : state.focusedQuestionId,
91
122
  };
92
123
  }
93
124
 
@@ -104,7 +135,7 @@ export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent
104
135
  pending = upsertQuestion(pending, {
105
136
  id: route.targetQuestionId,
106
137
  question: route.question,
107
- status: event.status,
138
+ status: event.status === "blocked" ? "blocked" : "needs-evidence",
108
139
  sourceRouteId: route.routeId,
109
140
  });
110
141
  }
@@ -112,18 +143,35 @@ export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent
112
143
  pending = upsertQuestion(pending, {
113
144
  id: `question:${route.routeId}`,
114
145
  question: route.question,
115
- status: event.status,
146
+ status: event.status === "blocked" ? "blocked" : "needs-evidence",
116
147
  sourceRouteId: route.routeId,
117
148
  });
118
149
  }
119
150
 
120
151
  for (const question of event.openedQuestions) pending = upsertQuestion(pending, question);
121
152
 
153
+ const closesFramingGate =
154
+ (route.owner === "sketch" || route.owner === "signal") &&
155
+ (event.status === "resolved" || event.status === "not-applicable");
156
+ const opensFramingGate = route.owner === "model" && event.status === "resolved";
157
+ let implementationFramingRequired = state.implementationFramingRequired;
158
+ if (opensFramingGate) implementationFramingRequired = true;
159
+ if (closesFramingGate) implementationFramingRequired = false;
160
+
161
+ let verificationRequired = state.verificationRequired;
162
+ if (route.owner === "direct" && event.changedArtifacts) verificationRequired = true;
163
+ if (route.owner === "verify" && event.status === "resolved") verificationRequired = false;
164
+
122
165
  return {
123
166
  ...state,
124
167
  activeRoute: undefined,
125
168
  lastJudgment: judgment,
126
169
  pendingQuestions: pending,
170
+ focusedQuestionId: pending.some((question) => question.id === state.focusedQuestionId)
171
+ ? state.focusedQuestionId
172
+ : undefined,
173
+ implementationFramingRequired,
174
+ verificationRequired,
127
175
  };
128
176
  }
129
177
 
@@ -143,6 +191,26 @@ function isJudgmentStatus(value: unknown): value is JudgmentStatus {
143
191
  return value === "resolved" || value === "needs-evidence" || value === "not-applicable" || value === "blocked";
144
192
  }
145
193
 
194
+ function isDirectExecutionProfile(value: unknown): value is DirectExecutionProfile {
195
+ return value === "ordinary" || value === "behavior-preserving-structure";
196
+ }
197
+
198
+ function parseDirectStep(value: unknown): DirectStepContract | undefined {
199
+ if (!isObject(value)) return undefined;
200
+ if (
201
+ typeof value.movement !== "string" ||
202
+ typeof value.stopCondition !== "string" ||
203
+ typeof value.verification !== "string"
204
+ ) {
205
+ return undefined;
206
+ }
207
+ return {
208
+ movement: value.movement,
209
+ stopCondition: value.stopCondition,
210
+ verification: value.verification,
211
+ };
212
+ }
213
+
146
214
  function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
147
215
  if (!isObject(value)) return undefined;
148
216
  if (
@@ -163,13 +231,22 @@ function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
163
231
 
164
232
  export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefined {
165
233
  if (!isObject(value)) return undefined;
166
- if (value.protocol !== PROTOCOL && value.protocol !== LEGACY_PROTOCOL) return undefined;
234
+ if (
235
+ value.protocol !== PROTOCOL &&
236
+ value.protocol !== PREVIOUS_PROTOCOL &&
237
+ value.protocol !== LEGACY_PROTOCOL
238
+ ) return undefined;
167
239
 
168
240
  if (value.kind === "mode") {
169
241
  if (!isMode(value.mode)) return undefined;
170
242
  return { protocol: PROTOCOL, kind: "mode", mode: value.mode };
171
243
  }
172
244
 
245
+ if (value.kind === "focus") {
246
+ if (value.protocol !== PROTOCOL || typeof value.questionId !== "string") return undefined;
247
+ return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
248
+ }
249
+
173
250
  if (value.kind === "route") {
174
251
  if (
175
252
  typeof value.routeId !== "string" ||
@@ -178,7 +255,9 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
178
255
  typeof value.reason !== "string" ||
179
256
  !isStringArray(value.knownEvidence) ||
180
257
  (value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
181
- (value.methodLocation !== undefined && typeof value.methodLocation !== "string")
258
+ (value.methodLocation !== undefined && typeof value.methodLocation !== "string") ||
259
+ (value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile)) ||
260
+ (value.directStep !== undefined && !parseDirectStep(value.directStep))
182
261
  ) {
183
262
  return undefined;
184
263
  }
@@ -192,6 +271,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
192
271
  knownEvidence: value.knownEvidence,
193
272
  targetQuestionId: value.targetQuestionId,
194
273
  methodLocation: value.methodLocation,
274
+ executionProfile: value.executionProfile,
275
+ directStep: value.directStep === undefined ? undefined : parseDirectStep(value.directStep),
195
276
  };
196
277
  }
197
278
 
@@ -223,9 +304,10 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
223
304
  id: `question:${value.routeId}:legacy:${index + 1}`,
224
305
  question,
225
306
  status: "needs-evidence",
226
- sourceRouteId: value.routeId,
307
+ sourceRouteId: String(value.routeId),
227
308
  })),
228
309
  artifacts: value.artifacts,
310
+ changedArtifacts: false,
229
311
  };
230
312
  }
231
313
 
@@ -243,6 +325,7 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
243
325
  basis: value.basis,
244
326
  openedQuestions: openedQuestions as PendingQuestion[],
245
327
  artifacts: value.artifacts,
328
+ changedArtifacts: typeof value.changedArtifacts === "boolean" ? value.changedArtifacts : false,
246
329
  };
247
330
  }
248
331
 
@@ -253,7 +336,7 @@ interface BranchEntryLike {
253
336
  message?: { role?: string; toolName?: string; details?: unknown };
254
337
  }
255
338
 
256
- const DEVELOPER_TOOL_NAMES = new Set([
339
+ const DEVELOPER_TOOL_NAMES = new Set<string>([
257
340
  ROUTE_TOOL,
258
341
  JUDGMENT_TOOL,
259
342
  LEGACY_ROUTE_TOOL,
@@ -261,7 +344,10 @@ const DEVELOPER_TOOL_NAMES = new Set([
261
344
  ]);
262
345
 
263
346
  export function eventFromBranchEntry(entry: BranchEntryLike): DeveloperEvent | undefined {
264
- if (entry.type === "custom" && entry.customType === MODE_ENTRY) {
347
+ if (
348
+ entry.type === "custom" &&
349
+ (entry.customType === MODE_ENTRY || entry.customType === FOCUS_ENTRY)
350
+ ) {
265
351
  return normalizeDeveloperEvent(entry.data);
266
352
  }
267
353
  if (
package/extensions/tui.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type ThemeColor,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import {
8
+ Box,
8
9
  Container,
9
10
  matchesKey,
10
11
  type SelectItem,
@@ -32,7 +33,7 @@ function modeName(mode: DeveloperMode): string {
32
33
 
33
34
  function protocolColor(value: ProtocolState): ThemeColor {
34
35
  if (value === "blocked") return "error";
35
- if (value === "needs-evidence") return "warning";
36
+ if (value === "needs-evidence" || value === "needs-verification") return "warning";
36
37
  if (value === "needs-judgment") return "accent";
37
38
  return "dim";
38
39
  }
@@ -109,6 +110,51 @@ interface SelectDialogOptions {
109
110
  maxPrimaryColumnWidth?: number;
110
111
  }
111
112
 
113
+ class SelectionPreview {
114
+ private item?: SelectItem;
115
+ private offset = 0;
116
+ private lastWidth = 40;
117
+ private readonly theme: Theme;
118
+ private readonly maxLines: number;
119
+
120
+ constructor(theme: Theme, maxLines = 5) {
121
+ this.theme = theme;
122
+ this.maxLines = maxLines;
123
+ }
124
+
125
+ setItem(item: SelectItem): void {
126
+ this.item = item;
127
+ this.offset = 0;
128
+ }
129
+
130
+ scroll(delta: number): void {
131
+ const total = this.contentLines(this.lastWidth).length;
132
+ this.offset = Math.max(0, Math.min(this.offset + delta, Math.max(0, total - this.maxLines)));
133
+ }
134
+
135
+ render(width: number): string[] {
136
+ this.lastWidth = width;
137
+ const lines = this.contentLines(width);
138
+ const visible = lines.slice(this.offset, this.offset + this.maxLines);
139
+ if (lines.length > this.maxLines) {
140
+ const end = Math.min(lines.length, this.offset + this.maxLines);
141
+ visible.push(this.theme.fg("dim", ` detail ${this.offset + 1}–${end}/${lines.length} · shift+↑↓ scroll`));
142
+ }
143
+ return visible;
144
+ }
145
+
146
+ invalidate(): void {}
147
+
148
+ private contentLines(width: number): string[] {
149
+ if (!this.item) return [];
150
+ const content = [
151
+ this.theme.fg("accent", this.theme.bold(this.item.label)),
152
+ ...(this.item.description ? [this.theme.fg("muted", this.item.description)] : []),
153
+ ].join("\n");
154
+ return new Text(content, 1, 0).render(width);
155
+ }
156
+ }
157
+
112
158
  async function showSelectDialog(
113
159
  ctx: ExtensionCommandContext,
114
160
  options: SelectDialogOptions,
@@ -116,13 +162,17 @@ async function showSelectDialog(
116
162
  const result = await ctx.ui.custom<string | null>(
117
163
  (tui, theme, _keybindings, done) => {
118
164
  const container = new Container();
165
+ const surface = new Box(1, 0, (text) => theme.bg("customMessageBg", text));
166
+ surface.addChild(container);
119
167
  const title = new Text("", 1, 0);
120
168
  const subtitle = new Text("", 1, 0);
121
169
  const hint = new Text("", 1, 0);
170
+ const previewLabel = new Text(theme.fg("dim", "Selected detail"), 1, 0);
171
+ const preview = new SelectionPreview(theme);
122
172
  const updateText = () => {
123
- title.setText(theme.fg("accent", theme.bold(options.title)));
173
+ title.setText(theme.fg("accent", theme.bold(`◆ ${options.title}`)));
124
174
  subtitle.setText(theme.fg("muted", options.subtitle));
125
- hint.setText(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"));
175
+ hint.setText(theme.fg("dim", "↑↓ navigate · shift+↑↓ scroll detail · enter select · esc cancel"));
126
176
  };
127
177
  updateText();
128
178
 
@@ -142,24 +192,32 @@ async function showSelectDialog(
142
192
  );
143
193
  list.onSelect = (item) => done(item.value);
144
194
  list.onCancel = () => done(null);
195
+ list.onSelectionChange = (item) => preview.setItem(item);
196
+ const initialItem = list.getSelectedItem();
197
+ if (initialItem) preview.setItem(initialItem);
145
198
 
146
199
  container.addChild(new DynamicBorder((text) => theme.fg("borderAccent", text)));
147
200
  container.addChild(title);
148
201
  container.addChild(subtitle);
149
202
  container.addChild(list);
203
+ container.addChild(new DynamicBorder((text) => theme.fg("borderMuted", text)));
204
+ container.addChild(previewLabel);
205
+ container.addChild(preview);
150
206
  container.addChild(hint);
151
207
  container.addChild(new DynamicBorder((text) => theme.fg("borderAccent", text)));
152
208
 
153
209
  return {
154
210
  render(width: number) {
155
- return container.render(width);
211
+ return surface.render(width);
156
212
  },
157
213
  invalidate() {
158
214
  updateText();
159
- container.invalidate();
215
+ surface.invalidate();
160
216
  },
161
217
  handleInput(data: string) {
162
- list.handleInput(data);
218
+ if (matchesKey(data, "shift+up")) preview.scroll(-1);
219
+ else if (matchesKey(data, "shift+down")) preview.scroll(1);
220
+ else list.handleInput(data);
163
221
  tui.requestRender();
164
222
  },
165
223
  };
@@ -169,7 +227,7 @@ async function showSelectDialog(
169
227
  overlayOptions: {
170
228
  anchor: "center",
171
229
  width: options.width,
172
- maxHeight: Math.min(options.maxVisible + 7, 20),
230
+ maxHeight: Math.min(options.maxVisible + 15, 26),
173
231
  margin: 1,
174
232
  },
175
233
  },
@@ -241,6 +299,12 @@ export class DeveloperWidget {
241
299
  if (this.state.pendingQuestions.length > 3) {
242
300
  lines.push(this.theme.fg("dim", ` +${this.state.pendingQuestions.length - 3} more open questions`));
243
301
  }
302
+ if (this.state.implementationFramingRequired) {
303
+ lines.push(this.theme.fg("warning", "→ next · sketch feature shape or signal structural movement"));
304
+ }
305
+ if (this.state.verificationRequired) {
306
+ lines.push(this.theme.fg("warning", "→ next · verify changed artifacts before completion"));
307
+ }
244
308
  return lines;
245
309
  }
246
310
 
@@ -259,17 +323,35 @@ export class DeveloperStatusPanel {
259
323
  private readonly view: DeveloperStatusView;
260
324
  private readonly theme: Theme;
261
325
  private readonly onClose: () => void;
262
-
263
- constructor(view: DeveloperStatusView, theme: Theme, onClose: () => void) {
326
+ private readonly requestRender: () => void;
327
+ private readonly viewportHeight: number;
328
+ private scrollOffset = 0;
329
+ private maxScrollOffset = 0;
330
+
331
+ constructor(
332
+ view: DeveloperStatusView,
333
+ theme: Theme,
334
+ onClose: () => void,
335
+ options: { viewportHeight?: number; requestRender?: () => void } = {},
336
+ ) {
264
337
  this.view = view;
265
338
  this.theme = theme;
266
339
  this.onClose = onClose;
340
+ this.viewportHeight = options.viewportHeight ?? 24;
341
+ this.requestRender = options.requestRender ?? (() => {});
267
342
  }
268
343
 
269
344
  handleInput(data: string): void {
270
345
  if (matchesKey(data, "escape") || matchesKey(data, "enter") || matchesKey(data, "ctrl+c")) {
271
346
  this.onClose();
347
+ return;
272
348
  }
349
+ if (matchesKey(data, "up")) this.scrollOffset = Math.max(0, this.scrollOffset - 1);
350
+ else if (matchesKey(data, "down")) {
351
+ this.scrollOffset = Math.min(this.maxScrollOffset, this.scrollOffset + 1);
352
+ } else return;
353
+ this.invalidate();
354
+ this.requestRender();
273
355
  }
274
356
 
275
357
  render(width: number): string[] {
@@ -278,17 +360,22 @@ export class DeveloperStatusPanel {
278
360
  const panelWidth = Math.max(20, width);
279
361
  const innerWidth = Math.max(18, panelWidth - 2);
280
362
  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) => {
363
+ const background = (text: string) => this.theme.bg("customMessageBg", text);
364
+ const border = (text: string) => this.theme.fg("borderAccent", text);
365
+ const row = (content = "") => {
366
+ const clipped = truncateToWidth(content, innerWidth, "…", true);
367
+ const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)));
368
+ return background(`${border("│")}${clipped}${padding}${border("│")}`);
369
+ };
370
+ const addWrapped = (label: string, value: string, color: ThemeColor = "muted", _maxLines?: number) => {
284
371
  const prefix = ` ${this.theme.fg("dim", `${label} ·`)} `;
285
- const wrapped = wrapTextWithAnsi(prefix + this.theme.fg(color, value), innerWidth).slice(0, maxLines);
372
+ const wrapped = wrapTextWithAnsi(prefix + this.theme.fg(color, value), innerWidth);
286
373
  for (const line of wrapped) rows.push(row(line));
287
374
  };
288
375
  const section = (title: string) => rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
289
376
 
290
- rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
291
- rows.push(row(` ${this.theme.fg("accent", this.theme.bold("Developer status"))}`));
377
+ rows.push(background(border(`╭${"─".repeat(innerWidth)}╮`)));
378
+ rows.push(row(` ${this.theme.fg("accent", this.theme.bold("Developer status"))}`));
292
379
  rows.push(row());
293
380
 
294
381
  const state = this.view.state;
@@ -320,7 +407,7 @@ export class DeveloperStatusPanel {
320
407
  if (state.pendingQuestions.length === 0) {
321
408
  addWrapped("state", "No unresolved Developer questions.", "dim", 1);
322
409
  } else {
323
- for (const question of state.pendingQuestions.slice(0, 4)) {
410
+ for (const question of state.pendingQuestions) {
324
411
  addWrapped(
325
412
  question.status === "blocked" ? "blocked" : "needs evidence",
326
413
  question.question,
@@ -328,9 +415,6 @@ export class DeveloperStatusPanel {
328
415
  1,
329
416
  );
330
417
  }
331
- if (state.pendingQuestions.length > 4) {
332
- addWrapped("more", String(state.pendingQuestions.length - 4), "dim", 1);
333
- }
334
418
  }
335
419
 
336
420
  rows.push(row());
@@ -363,11 +447,27 @@ export class DeveloperStatusPanel {
363
447
  );
364
448
  rows.push(row());
365
449
  rows.push(row(` ${this.theme.fg("dim", "enter/esc close · /develop questions revisits open work")}`));
366
- rows.push(border(`╰${"─".repeat(innerWidth)}╯`));
450
+ rows.push(background(border(`╰${"─".repeat(innerWidth)}╯`)));
451
+
452
+ const header = rows.slice(0, 2);
453
+ const body = rows.slice(2, -2);
454
+ const bodyCapacity = Math.max(1, this.viewportHeight - 4);
455
+ this.maxScrollOffset = Math.max(0, body.length - bodyCapacity);
456
+ this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset);
457
+ const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + bodyCapacity);
458
+ const position = body.length > bodyCapacity
459
+ ? `↑↓ scroll · ${this.scrollOffset + 1}–${Math.min(body.length, this.scrollOffset + bodyCapacity)}/${body.length} · enter/esc close`
460
+ : "enter/esc close · /develop questions revisits open work";
461
+ const visibleRows = [
462
+ ...header,
463
+ ...visibleBody,
464
+ row(` ${this.theme.fg("dim", position)}`),
465
+ background(border(`╰${"─".repeat(innerWidth)}╯`)),
466
+ ];
367
467
 
368
468
  this.cachedWidth = width;
369
- this.cachedLines = rows;
370
- return rows;
469
+ this.cachedLines = visibleRows;
470
+ return visibleRows;
371
471
  }
372
472
 
373
473
  invalidate(): void {
@@ -380,12 +480,27 @@ export async function showDeveloperStatus(
380
480
  ctx: ExtensionCommandContext,
381
481
  view: DeveloperStatusView,
382
482
  ): Promise<void> {
383
- await ctx.ui.custom<void>((_tui, theme, _keybindings, done) =>
384
- new DeveloperStatusPanel(view, theme, () => done()));
483
+ await ctx.ui.custom<void>(
484
+ (tui, theme, _keybindings, done) =>
485
+ new DeveloperStatusPanel(view, theme, () => done(), {
486
+ viewportHeight: Math.max(12, Math.floor(tui.terminal.rows * 0.8)),
487
+ requestRender: () => tui.requestRender(),
488
+ }),
489
+ {
490
+ overlay: true,
491
+ overlayOptions: {
492
+ anchor: "center",
493
+ width: "82%",
494
+ minWidth: 56,
495
+ maxHeight: "85%",
496
+ margin: 1,
497
+ },
498
+ },
499
+ );
385
500
  }
386
501
 
387
502
  export function prepareQuestionPrompt(ctx: ExtensionCommandContext, question: PendingQuestion): void {
388
- const prompt = `Revisit Developer question ${question.id}: ${question.question}`;
503
+ const prompt = `Revisit this Developer question: ${question.question}`;
389
504
  const current = ctx.ui.getEditorText();
390
505
  ctx.ui.setEditorText(current.trim() ? `${current.trimEnd()}\n\n${prompt}` : prompt);
391
506
  }
package/package.json CHANGED
@@ -1,14 +1,9 @@
1
1
  {
2
2
  "name": "@hobin/developer",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Adaptive product-development reasoning and evidence for Pi.",
5
5
  "type": "module",
6
- "keywords": [
7
- "pi-package",
8
- "pi",
9
- "developer",
10
- "skills"
11
- ],
6
+ "keywords": ["pi-package", "pi", "developer", "skills"],
12
7
  "license": "MIT",
13
8
  "author": {
14
9
  "name": "dev-hobin",
@@ -26,32 +21,23 @@
26
21
  "publishConfig": {
27
22
  "access": "public"
28
23
  },
29
- "files": [
30
- "extensions",
31
- "skills",
32
- "README.md",
33
- "LICENSE"
34
- ],
24
+ "files": ["extensions", "skills", "README.md", "SOURCES.md", "LICENSE"],
35
25
  "engines": {
36
26
  "node": ">=22.19.0"
37
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
+ },
38
33
  "pi": {
39
- "extensions": [
40
- "./extensions/developer.ts"
41
- ],
42
- "skills": [
43
- "./skills"
44
- ]
34
+ "extensions": ["./extensions/developer.ts"],
35
+ "skills": ["./skills"]
45
36
  },
46
37
  "peerDependencies": {
47
38
  "@earendil-works/pi-ai": "*",
48
39
  "@earendil-works/pi-coding-agent": "*",
49
40
  "@earendil-works/pi-tui": "*",
50
41
  "typebox": "*"
51
- },
52
- "scripts": {
53
- "check": "node scripts/check-package.mjs && node --test tests/*.test.ts",
54
- "eval": "node scripts/eval-rpc.mjs",
55
- "eval:json": "node scripts/eval-json.mjs"
56
42
  }
57
- }
43
+ }
@@ -30,6 +30,12 @@ The wish opens freedom; the stop check brings the review back to evidence.
30
30
  Failure is not handled by a better name. Failure exposes a broken layer,
31
31
  contract, or missing artifact.
32
32
 
33
+ Use [the recipe cards](recipe-cards.md) when the derivation rule is missing,
34
+ [the repair table](repair-table.md) when the stop check fails, and
35
+ [the worked examples](worked-examples.md) when a completed calibration case is
36
+ needed. The field card remains the auditable summary; these files supply focused
37
+ construction, repair, and examples.
38
+
33
39
  ## Field Card
34
40
 
35
41
  Fill this card for serious reviews.
@@ -153,6 +159,7 @@ If the sentence cannot be filled:
153
159
  | --- | --- | --- |
154
160
  | The problem vocabulary is missing or misleading | Language | Notation As Data |
155
161
  | Similar functions, workflows, or tests move together | Language/Unit | Movement Pattern Extraction |
162
+ | Data, methods, messages, or change reasons suggest a misplaced owner | Unit/Boundary | Responsibility Boundary |
156
163
  | Operation results do not remain composable | Unit | Closure Composition Unit |
157
164
  | A loop, state transition, or contract has no preserved meaning | Law | Invariant Iteration |
158
165
  | Callers must know raw representation or hidden policy | Boundary | Data Abstraction Boundary |