@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.
- package/README.md +61 -20
- package/SOURCES.md +104 -0
- package/extensions/developer.ts +202 -44
- package/extensions/references/behavior-preserving-structural-change.md +149 -0
- package/extensions/skills.ts +1 -16
- package/extensions/state.ts +96 -10
- package/extensions/tui.ts +140 -25
- package/package.json +11 -25
- package/skills/abstraction-review/references/field-card.md +7 -0
- package/skills/abstraction-review/references/recipe-cards.md +67 -0
- package/skills/abstraction-review/references/repair-table.md +5 -0
- package/skills/model/SKILL.md +6 -1
- package/skills/model/references/problem-modeling.md +294 -235
- package/skills/model/references/worked-models-and-specialized-techniques.md +218 -0
- package/skills/naming-judgment/SKILL.md +5 -3
- package/skills/naming-judgment/references/domain-naming.md +202 -0
- package/skills/schedule/SKILL.md +1 -1
- package/skills/schedule/references/structural-change-timing.md +80 -13
- package/skills/signal/SKILL.md +2 -2
- package/skills/signal/references/structural-movement.md +202 -0
- package/skills/sketch/SKILL.md +35 -8
- package/skills/sketch/references/abstraction-barriers-and-closure.md +160 -0
- package/skills/sketch/references/abstraction-composition-and-state.md +184 -0
- package/skills/sketch/references/composition-generative-recursion-and-accumulators.md +196 -0
- package/skills/sketch/references/data-driven-design.md +177 -0
- package/skills/sketch/references/data-shape-template-catalog.md +241 -0
- package/skills/sketch/references/generic-operations-and-languages.md +134 -0
- package/skills/sketch/references/processes-state-and-time.md +171 -0
- package/skills/sketch/references/responsibility-and-variation.md +245 -0
- package/skills/verify/references/verifier-selection-and-pass-but-wrong.md +61 -3
- package/skills/naming-judgment/references/elements-of-clojure-naming.md +0 -74
- package/skills/signal/references/flocking-and-structural-movement.md +0 -157
- package/skills/sketch/references/design-recipe-and-abstraction-barriers.md +0 -169
package/extensions/state.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export const PROTOCOL = "developer/
|
|
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 (
|
|
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 (
|
|
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
|
|
211
|
+
return surface.render(width);
|
|
156
212
|
},
|
|
157
213
|
invalidate() {
|
|
158
214
|
updateText();
|
|
159
|
-
|
|
215
|
+
surface.invalidate();
|
|
160
216
|
},
|
|
161
217
|
handleInput(data: string) {
|
|
162
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
|
282
|
-
const
|
|
283
|
-
const
|
|
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)
|
|
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
|
|
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 =
|
|
370
|
-
return
|
|
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>(
|
|
384
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 |
|