@hobin/developer 0.1.2 → 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 +32 -11
- package/extensions/developer.ts +136 -21
- package/extensions/references/behavior-preserving-structural-change.md +14 -10
- package/extensions/state.ts +88 -10
- package/extensions/tui.ts +140 -25
- package/package.json +11 -26
package/README.md
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
Adaptive product-development judgment for [Pi](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
Developer combines a small, branch-aware coordination extension with ten
|
|
6
|
-
independent skills. It routes one concrete question
|
|
7
|
-
resulting evidence, and preserves unresolved questions
|
|
8
|
-
|
|
6
|
+
independent skills. It routes one concrete question or green-to-green movement
|
|
7
|
+
at a time, records the resulting evidence, and preserves unresolved questions.
|
|
8
|
+
Its default topology gives feature work a useful backbone without turning every
|
|
9
|
+
task into a fixed lifecycle.
|
|
9
10
|
|
|
10
11
|
## Install
|
|
11
12
|
|
|
@@ -86,11 +87,26 @@ Developer adds two model-facing protocol tools:
|
|
|
86
87
|
1. `developer_route_question` opens one route for one concrete question.
|
|
87
88
|
2. The route target is `direct` or one currently available Developer skill.
|
|
88
89
|
3. A skill route returns the exact Pi-discovered `SKILL.md` instructions and canonical
|
|
89
|
-
path; a direct route
|
|
90
|
-
|
|
91
|
-
execution profile
|
|
90
|
+
path; a direct route declares one movement, its stable landing, and its narrow
|
|
91
|
+
verification. A behavior-preserving structural route also loads the focused
|
|
92
|
+
execution profile based on flocking-style movement and small tidyings.
|
|
92
93
|
4. `developer_record_judgment` closes the route with a status, result, evidence,
|
|
93
|
-
artifacts, and any newly opened questions.
|
|
94
|
+
artifacts, and any newly opened questions. Developer then routes again from
|
|
95
|
+
the stable landing before another movement.
|
|
96
|
+
|
|
97
|
+
The conditional default topology is:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
clarify when needed → model consequential cases
|
|
101
|
+
→ sketch the first feature implementation surface OR signal existing-code movement
|
|
102
|
+
→ one direct green-to-green movement → re-route from evidence
|
|
103
|
+
→ verify before completion
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This is not a mandatory phase sequence. A stage may be not applicable and new
|
|
107
|
+
evidence may route backward or sideways. One guard is deliberate: a resolved
|
|
108
|
+
model cannot flow straight into mutation. New feature work receives an initial
|
|
109
|
+
`sketch`; existing-code structural work receives a `signal` first.
|
|
94
110
|
|
|
95
111
|
Product code is still read, edited, executed, and tested with Pi's normal tools.
|
|
96
112
|
Developer's tools only route and record judgment; they do not implement product
|
|
@@ -130,8 +146,12 @@ These are routing states, not product-completion claims. In particular:
|
|
|
130
146
|
- A resolved `specify` judgment is not user acceptance.
|
|
131
147
|
- A resolved `verify` judgment is not timeless proof after later changes.
|
|
132
148
|
|
|
133
|
-
Pending questions receive stable IDs
|
|
134
|
-
|
|
149
|
+
Pending questions receive stable internal IDs, but users do not type them.
|
|
150
|
+
Selecting `/develop questions` focuses the question in branch state; the next
|
|
151
|
+
route automatically associates that focus. A sole pending question or an exact
|
|
152
|
+
question match is also associated automatically. Resolved and not-applicable
|
|
153
|
+
judgments remove the associated question immediately; needs-evidence and blocked
|
|
154
|
+
judgments retain the same identity.
|
|
135
155
|
|
|
136
156
|
## Skills
|
|
137
157
|
|
|
@@ -184,8 +204,9 @@ stored in tool-result details. Developer reconstructs state from the current
|
|
|
184
204
|
session branch on startup and tree navigation, so a fork inherits only the
|
|
185
205
|
events on its branch.
|
|
186
206
|
|
|
187
|
-
The current event contract is `developer/
|
|
188
|
-
history can be replayed, but
|
|
207
|
+
The current event contract is `developer/v3`. Legacy `developer/v1` and
|
|
208
|
+
`developer/v2` routing history can be replayed, but new framing and verification
|
|
209
|
+
obligations are not retroactively inferred.
|
|
189
210
|
|
|
190
211
|
Developer uses Pi's normal compaction. Each new agent turn receives current
|
|
191
212
|
protocol state, and route results place identity and recovery metadata before
|
package/extensions/developer.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { Type } from "typebox";
|
|
|
17
17
|
|
|
18
18
|
import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
|
|
19
19
|
import {
|
|
20
|
+
FOCUS_ENTRY,
|
|
20
21
|
JUDGMENT_TOOL,
|
|
21
22
|
MODE_ENTRY,
|
|
22
23
|
PROTOCOL,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
type DeveloperMode,
|
|
30
31
|
type DeveloperState,
|
|
31
32
|
type DirectExecutionProfile,
|
|
33
|
+
type FocusEvent,
|
|
32
34
|
type JudgmentEvent,
|
|
33
35
|
type ModeEvent,
|
|
34
36
|
type RouteEvent,
|
|
@@ -107,6 +109,25 @@ function compactLine(value: string, maxChars = 160): string {
|
|
|
107
109
|
return line.length <= maxChars ? line : `${line.slice(0, maxChars - 1)}…`;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
function normalizedQuestion(value: string): string {
|
|
113
|
+
return value.toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function inferredQuestionId(
|
|
117
|
+
state: DeveloperState,
|
|
118
|
+
question: string,
|
|
119
|
+
explicitQuestionId?: string,
|
|
120
|
+
): string | undefined {
|
|
121
|
+
if (explicitQuestionId) return explicitQuestionId;
|
|
122
|
+
if (state.focusedQuestionId) return state.focusedQuestionId;
|
|
123
|
+
const normalized = normalizedQuestion(question);
|
|
124
|
+
const exact = state.pendingQuestions.filter(
|
|
125
|
+
(pending) => normalizedQuestion(pending.question) === normalized,
|
|
126
|
+
);
|
|
127
|
+
if (exact.length === 1) return exact[0]?.id;
|
|
128
|
+
return state.pendingQuestions.length === 1 ? state.pendingQuestions[0]?.id : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
110
131
|
function summarizeState(state: DeveloperState): string {
|
|
111
132
|
if (state.mode === "off") return "developer: off";
|
|
112
133
|
const target = state.activeRoute ? state.activeRoute.owner : "none";
|
|
@@ -117,12 +138,14 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
117
138
|
const lines = [
|
|
118
139
|
"",
|
|
119
140
|
`Developer protocol (${state.mode}) is active.`,
|
|
120
|
-
"-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
"- Use the default topology as a conditional backbone, not a rigid lifecycle: clarify meaning when needed -> model consequential cases -> sketch the first implementation surface for new behavior or signal the smallest structural movement in existing code -> execute one direct step -> verify current claims.",
|
|
142
|
+
"- Adapt away from that topology when evidence makes a stage not applicable, but never jump directly from a resolved model to mutation: use sketch for feature implementation or signal for existing-code structural movement first.",
|
|
143
|
+
`- Call ${ROUTE_TOOL} for exactly one concrete judgment or one green-to-green direct movement.`,
|
|
144
|
+
"- Use owner=direct only when the next local movement, stable landing, and narrow verification are already justified; otherwise choose the focused skill whose scope fits the current question.",
|
|
145
|
+
"- A direct step has one observable difference and one structural or behavioral purpose. Stop when its failure is locally explainable and the repository is green, pausable, and reviewable.",
|
|
123
146
|
"- For direct structural work intended to preserve behavior, set execution_profile=behavior-preserving-structure; omit the field for other direct actions and every skill route.",
|
|
124
|
-
`- Follow the
|
|
125
|
-
"-
|
|
147
|
+
`- Follow the routed method, then close the route with ${JUDGMENT_TOOL}. After every direct stable landing, route again from the new evidence before selecting another movement.`,
|
|
148
|
+
"- Do not carry a predetermined implementation queue through multiple direct steps. Re-observe after each stable landing and reroute to a skill whenever meaning, cases, design, structural direction, timing, naming, or evidence becomes uncertain.",
|
|
126
149
|
"- Protocol state is routing bookkeeping. Idle never proves product completion, user acceptance, or current verification.",
|
|
127
150
|
"- Product files are changed with Pi implementation tools. Developer protocol tools only route and record judgments.",
|
|
128
151
|
];
|
|
@@ -133,6 +156,17 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
133
156
|
);
|
|
134
157
|
}
|
|
135
158
|
|
|
159
|
+
if (state.implementationFramingRequired) {
|
|
160
|
+
lines.push(
|
|
161
|
+
"Required next framing: the latest resolved model exposed implementation work. Route sketch before new feature mutation, or signal before an existing-code structural movement; direct is not ready yet.",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (state.verificationRequired) {
|
|
165
|
+
lines.push(
|
|
166
|
+
"Verification debt: a direct route changed artifacts after the last resolved verify judgment. Route verify before claiming completion or handing off as done.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
136
170
|
if (state.activeRoute) {
|
|
137
171
|
lines.push(
|
|
138
172
|
`Active route: ${state.activeRoute.routeId} · ${state.activeRoute.owner} · ${state.activeRoute.question}`,
|
|
@@ -149,7 +183,7 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
149
183
|
lines.push(`- ${question.id} · ${question.status} · ${question.question}`);
|
|
150
184
|
}
|
|
151
185
|
lines.push(
|
|
152
|
-
|
|
186
|
+
"- Revisit questions naturally. Developer automatically associates a focused, sole, or exactly matching pending question; open_question_id is an internal disambiguator only when several questions remain.",
|
|
153
187
|
);
|
|
154
188
|
}
|
|
155
189
|
lines.push(
|
|
@@ -174,6 +208,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
174
208
|
let availableSkills = new Map<string, Skill>();
|
|
175
209
|
let state = initialState();
|
|
176
210
|
let routeOpening = false;
|
|
211
|
+
const routesWithMutation = new Set<string>();
|
|
177
212
|
let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
|
|
178
213
|
|
|
179
214
|
pi.registerFlag("develop-mode", {
|
|
@@ -206,7 +241,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
206
241
|
"developer",
|
|
207
242
|
ctx.mode === "tui" ? renderDeveloperFooter(state, ctx.ui.theme) : summarizeState(state),
|
|
208
243
|
);
|
|
209
|
-
if (
|
|
244
|
+
if (
|
|
245
|
+
!state.activeRoute &&
|
|
246
|
+
state.pendingQuestions.length === 0 &&
|
|
247
|
+
!state.implementationFramingRequired &&
|
|
248
|
+
!state.verificationRequired
|
|
249
|
+
) {
|
|
210
250
|
ctx.ui.setWidget("developer", undefined);
|
|
211
251
|
return;
|
|
212
252
|
}
|
|
@@ -227,6 +267,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
227
267
|
.slice(0, 3)
|
|
228
268
|
.map((question) => `open · ${question.id} · ${compactLine(question.question)}`),
|
|
229
269
|
...(state.pendingQuestions.length > 3 ? [`open · +${state.pendingQuestions.length - 3} more`] : []),
|
|
270
|
+
...(state.implementationFramingRequired ? ["next · sketch feature shape or signal structural movement"] : []),
|
|
271
|
+
...(state.verificationRequired ? ["next · verify changed artifacts before completion"] : []),
|
|
230
272
|
];
|
|
231
273
|
ctx.ui.setWidget("developer", lines, { placement: "belowEditor" });
|
|
232
274
|
};
|
|
@@ -283,6 +325,21 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
283
325
|
{
|
|
284
326
|
...SharedRouteParams,
|
|
285
327
|
owner: Type.Literal("direct", { description: "Use Pi implementation tools for an already-justified action" }),
|
|
328
|
+
movement: Type.String({
|
|
329
|
+
minLength: 1,
|
|
330
|
+
maxLength: MAX_QUESTION_CHARS,
|
|
331
|
+
description: "One locally explainable behavioral or structural movement; not a multi-step implementation queue",
|
|
332
|
+
}),
|
|
333
|
+
stop_condition: Type.String({
|
|
334
|
+
minLength: 1,
|
|
335
|
+
maxLength: MAX_QUESTION_CHARS,
|
|
336
|
+
description: "The green, pausable, reviewable stable landing that ends this direct route",
|
|
337
|
+
}),
|
|
338
|
+
verification: Type.String({
|
|
339
|
+
minLength: 1,
|
|
340
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
341
|
+
description: "The narrowest check that can catch the likely break in this movement",
|
|
342
|
+
}),
|
|
286
343
|
execution_profile: Type.Optional(
|
|
287
344
|
Type.Literal("behavior-preserving-structure", {
|
|
288
345
|
description: "Load the focused structural-mutation protocol; omit for ordinary direct action",
|
|
@@ -297,11 +354,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
297
354
|
name: ROUTE_TOOL,
|
|
298
355
|
label: "Developer Route Question",
|
|
299
356
|
description:
|
|
300
|
-
"Route one concrete
|
|
357
|
+
"Route one concrete judgment or one green-to-green direct movement. Uses an adaptive default topology: model, then sketch for feature shape or signal for structural movement, direct stable landings, and verify before completion.",
|
|
301
358
|
promptSnippet: "Choose how to handle one development question",
|
|
302
359
|
promptGuidelines: [
|
|
303
360
|
`Call ${ROUTE_TOOL} only when there is no active Developer route.`,
|
|
304
|
-
`Use ${ROUTE_TOOL} with the most focused skill supported by current evidence;
|
|
361
|
+
`Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; owner=direct requires one movement, one stable landing, and one narrow verification.`,
|
|
362
|
+
`After a resolved model, use sketch for first feature implementation framing or signal for existing-code structural movement before direct mutation.`,
|
|
305
363
|
],
|
|
306
364
|
parameters: RouteParams,
|
|
307
365
|
executionMode: "sequential",
|
|
@@ -318,12 +376,22 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
318
376
|
const reason = params.reason.trim();
|
|
319
377
|
if (!question || !reason) fail("Question and reason must contain non-whitespace text.");
|
|
320
378
|
|
|
321
|
-
const
|
|
379
|
+
const explicitQuestionId = params.open_question_id?.trim() || undefined;
|
|
380
|
+
const targetQuestionId = inferredQuestionId(state, question, explicitQuestionId);
|
|
322
381
|
if (targetQuestionId && !state.pendingQuestions.some((item) => item.id === targetQuestionId)) {
|
|
323
382
|
fail(`Unknown pending question ID: ${targetQuestionId}`);
|
|
324
383
|
}
|
|
325
384
|
|
|
326
385
|
const owner = params.owner;
|
|
386
|
+
if (
|
|
387
|
+
owner === "direct" &&
|
|
388
|
+
state.implementationFramingRequired &&
|
|
389
|
+
(availableSkills.has("sketch") || availableSkills.has("signal"))
|
|
390
|
+
) {
|
|
391
|
+
fail(
|
|
392
|
+
"The latest resolved model requires implementation framing before direct work. Route sketch for new feature shape or signal for existing-code structural movement.",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
327
395
|
const skill = owner === "direct" ? undefined : availableSkills.get(owner);
|
|
328
396
|
if (owner !== "direct" && !skill) {
|
|
329
397
|
fail(`Developer skill ${owner} is unavailable or disabled in the current Pi resource configuration.`);
|
|
@@ -351,6 +419,23 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
351
419
|
"The next local action is already justified. Keep this route open while using Pi implementation tools and collecting evidence.",
|
|
352
420
|
].join("\n");
|
|
353
421
|
|
|
422
|
+
const directStep =
|
|
423
|
+
owner === "direct"
|
|
424
|
+
? {
|
|
425
|
+
movement: ("movement" in params ? params.movement : question).trim(),
|
|
426
|
+
stopCondition: (
|
|
427
|
+
"stop_condition" in params
|
|
428
|
+
? params.stop_condition
|
|
429
|
+
: "Reach a green, pausable, reviewable stable landing."
|
|
430
|
+
).trim(),
|
|
431
|
+
verification: (
|
|
432
|
+
"verification" in params
|
|
433
|
+
? params.verification
|
|
434
|
+
: "Run the narrowest relevant check and inspect the resulting diff or output."
|
|
435
|
+
).trim(),
|
|
436
|
+
}
|
|
437
|
+
: undefined;
|
|
438
|
+
|
|
354
439
|
const event: RouteEvent = {
|
|
355
440
|
protocol: PROTOCOL,
|
|
356
441
|
kind: "route",
|
|
@@ -362,6 +447,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
362
447
|
targetQuestionId,
|
|
363
448
|
methodLocation: skill?.filePath,
|
|
364
449
|
executionProfile,
|
|
450
|
+
directStep,
|
|
365
451
|
};
|
|
366
452
|
const response = [
|
|
367
453
|
`Route ID: ${event.routeId}`,
|
|
@@ -369,6 +455,14 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
369
455
|
`Target: ${event.owner}`,
|
|
370
456
|
skill ? `Skill location: ${skill.filePath}` : "Skill location: direct action; no skill file",
|
|
371
457
|
executionProfile ? `Execution profile: ${executionProfile}` : "Execution profile: skill judgment",
|
|
458
|
+
...(directStep
|
|
459
|
+
? [
|
|
460
|
+
`Movement: ${directStep.movement}`,
|
|
461
|
+
`Stable landing: ${directStep.stopCondition}`,
|
|
462
|
+
`Narrow verification: ${directStep.verification}`,
|
|
463
|
+
"Stop this direct route at that landing, record the evidence, and route again before another movement.",
|
|
464
|
+
]
|
|
465
|
+
: []),
|
|
372
466
|
`Reason: ${event.reason}`,
|
|
373
467
|
`Known evidence: ${event.knownEvidence.length > 0 ? event.knownEvidence.join(" | ") : "none"}`,
|
|
374
468
|
targetQuestionId ? `Revisits pending question: ${targetQuestionId}` : "Revisits pending question: none",
|
|
@@ -397,11 +491,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
397
491
|
context.lastComponent,
|
|
398
492
|
);
|
|
399
493
|
},
|
|
400
|
-
renderResult(result, { expanded, isPartial
|
|
494
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
401
495
|
if (isPartial) {
|
|
402
496
|
return reusableText(theme.fg("warning", "routing development question…"), context.lastComponent);
|
|
403
497
|
}
|
|
404
|
-
if (isError) {
|
|
498
|
+
if (context.isError) {
|
|
405
499
|
return reusableText(
|
|
406
500
|
theme.fg("error", resultText(result) || "Developer route failed"),
|
|
407
501
|
context.lastComponent,
|
|
@@ -425,6 +519,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
425
519
|
if (event.executionProfile) {
|
|
426
520
|
text += `\n${theme.fg("dim", `profile · ${event.executionProfile}`)}`;
|
|
427
521
|
}
|
|
522
|
+
if (event.directStep) {
|
|
523
|
+
text += `\n${theme.fg("dim", `movement · ${event.directStep.movement}`)}`;
|
|
524
|
+
text += `\n${theme.fg("dim", `landing · ${event.directStep.stopCondition}`)}`;
|
|
525
|
+
text += `\n${theme.fg("dim", `verify · ${event.directStep.verification}`)}`;
|
|
526
|
+
}
|
|
428
527
|
}
|
|
429
528
|
if (!expanded && event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
430
529
|
return reusableText(text, context.lastComponent);
|
|
@@ -503,6 +602,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
503
602
|
basis,
|
|
504
603
|
openedQuestions,
|
|
505
604
|
artifacts: (params.artifacts ?? []).map((item) => item.trim()).filter(Boolean),
|
|
605
|
+
changedArtifacts: routesWithMutation.has(params.route_id),
|
|
506
606
|
};
|
|
507
607
|
const nextState = applyDeveloperEvent(state, event);
|
|
508
608
|
if (nextState.pendingQuestions.length > MAX_PENDING_QUESTIONS) {
|
|
@@ -513,12 +613,17 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
513
613
|
|
|
514
614
|
const next = protocolState(nextState);
|
|
515
615
|
const nextMessage =
|
|
516
|
-
|
|
517
|
-
?
|
|
518
|
-
|
|
616
|
+
event.owner === "direct"
|
|
617
|
+
? nextState.verificationRequired
|
|
618
|
+
? "Stable landing recorded. Route again from the new evidence; verify is required before claiming completion."
|
|
619
|
+
: "Stable landing recorded. Route again from the new evidence before selecting another movement."
|
|
620
|
+
: next === "idle"
|
|
621
|
+
? "Developer protocol is idle. This is routing state only and does not prove task completion."
|
|
622
|
+
: `Developer protocol is ${next}. Address the current routing obligation before handoff.`;
|
|
519
623
|
const response = `Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\n${nextMessage}`;
|
|
520
624
|
ensureSafeToolText(response, "Developer judgment result");
|
|
521
625
|
|
|
626
|
+
routesWithMutation.delete(params.route_id);
|
|
522
627
|
state = nextState;
|
|
523
628
|
syncProtocolTools();
|
|
524
629
|
refreshUI(ctx);
|
|
@@ -541,11 +646,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
541
646
|
context.lastComponent,
|
|
542
647
|
);
|
|
543
648
|
},
|
|
544
|
-
renderResult(result, { expanded, isPartial
|
|
649
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
545
650
|
if (isPartial) {
|
|
546
651
|
return reusableText(theme.fg("warning", "recording development judgment…"), context.lastComponent);
|
|
547
652
|
}
|
|
548
|
-
if (isError) {
|
|
653
|
+
if (context.isError) {
|
|
549
654
|
return reusableText(
|
|
550
655
|
theme.fg("error", resultText(result) || "Developer judgment failed"),
|
|
551
656
|
context.lastComponent,
|
|
@@ -668,8 +773,16 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
668
773
|
if (!questionId) return;
|
|
669
774
|
const question = state.pendingQuestions.find((item) => item.id === questionId);
|
|
670
775
|
if (!question) return;
|
|
776
|
+
const focusEvent: FocusEvent = {
|
|
777
|
+
protocol: PROTOCOL,
|
|
778
|
+
kind: "focus",
|
|
779
|
+
questionId: question.id,
|
|
780
|
+
};
|
|
781
|
+
pi.appendEntry(FOCUS_ENTRY, focusEvent);
|
|
782
|
+
state = applyDeveloperEvent(state, focusEvent);
|
|
783
|
+
refreshUI(ctx);
|
|
671
784
|
prepareQuestionPrompt(ctx, question);
|
|
672
|
-
ctx.ui.notify("Open question loaded into the editor for review.", "info");
|
|
785
|
+
ctx.ui.notify("Open question focused and loaded into the editor for review.", "info");
|
|
673
786
|
return;
|
|
674
787
|
}
|
|
675
788
|
ctx.ui.notify(
|
|
@@ -717,10 +830,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
717
830
|
});
|
|
718
831
|
|
|
719
832
|
pi.on("tool_call", (event) => {
|
|
720
|
-
if (state.mode !== "strict") return;
|
|
721
833
|
const mutationBuiltins = builtinMutationToolNames(pi.getAllTools());
|
|
722
|
-
if (
|
|
723
|
-
|
|
834
|
+
if (mutationBuiltins.has(event.toolName) && state.activeRoute?.owner === "direct") {
|
|
835
|
+
routesWithMutation.add(state.activeRoute.routeId);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (state.mode !== "strict" || !mutationBuiltins.has(event.toolName)) return;
|
|
724
839
|
return {
|
|
725
840
|
block: true,
|
|
726
841
|
reason: `Developer strict mode requires an active ${ROUTE_TOOL} targeting direct action (owner=direct) before Pi built-in edit, write, or bash.`,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Behavior-Preserving Structural Change
|
|
2
2
|
|
|
3
|
-
Use this protocol only after a `direct` route has justified
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use this protocol only after a `direct` route has justified one concrete structural
|
|
4
|
+
movement and its stable landing. It governs that single green-to-green mutation;
|
|
5
|
+
it does not discover the move, approve an abstraction, decide its timing, or carry
|
|
6
|
+
a multi-step implementation queue across landings.
|
|
6
7
|
|
|
7
8
|
## Entry Contract
|
|
8
9
|
|
|
@@ -42,13 +43,14 @@ valid. Do not rely on a final large diff to explain which step changed behavior.
|
|
|
42
43
|
|
|
43
44
|
## Evidence Rhythm
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Within the routed movement:
|
|
46
47
|
|
|
47
48
|
1. run the narrowest check that can catch the likely break;
|
|
48
49
|
2. inspect the diff for mixed behavior and structural changes;
|
|
49
50
|
3. reduce the step when the failure cannot be explained locally;
|
|
50
|
-
4. return to
|
|
51
|
-
5.
|
|
51
|
+
4. return to the declared green, deployable stable landing;
|
|
52
|
+
5. close the direct route there and let Developer re-observe and route the next
|
|
53
|
+
question instead of following a predetermined final design.
|
|
52
54
|
|
|
53
55
|
A passing test is useful only when it exercises the preserved behavior. When no
|
|
54
56
|
cheap verifier exists, keep the movement smaller and record the residual risk.
|
|
@@ -63,13 +65,15 @@ A stable landing is not merely a green command. It is a state where:
|
|
|
63
65
|
- the diff has one explainable structural purpose;
|
|
64
66
|
- the next decision can be made from the new evidence.
|
|
65
67
|
|
|
66
|
-
Stop there when the accepted
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
Stop and close the direct route there even when the wider accepted task still has
|
|
69
|
+
work remaining. Developer must select the next movement or focused judgment from
|
|
70
|
+
the new evidence. Further caller movement, cleanup, polymorphism, factory design,
|
|
71
|
+
or public abstraction requires another route and its own current pressure.
|
|
69
72
|
|
|
70
73
|
## Reroute Conditions
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
Close direct execution at the latest stable landing and open a focused judgment
|
|
76
|
+
when the movement reveals:
|
|
73
77
|
|
|
74
78
|
- an unexpected product behavior or policy choice: `specify`;
|
|
75
79
|
- missing cases, transition rules, or replacement obligations: `model`;
|
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;
|
|
@@ -17,6 +19,18 @@ export interface ModeEvent {
|
|
|
17
19
|
mode: DeveloperMode;
|
|
18
20
|
}
|
|
19
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
|
+
|
|
20
34
|
export interface RouteEvent {
|
|
21
35
|
protocol: typeof PROTOCOL;
|
|
22
36
|
kind: "route";
|
|
@@ -28,6 +42,7 @@ export interface RouteEvent {
|
|
|
28
42
|
targetQuestionId?: string;
|
|
29
43
|
methodLocation?: string;
|
|
30
44
|
executionProfile?: DirectExecutionProfile;
|
|
45
|
+
directStep?: DirectStepContract;
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
export interface PendingQuestion {
|
|
@@ -48,9 +63,10 @@ export interface JudgmentEvent {
|
|
|
48
63
|
basis: string[];
|
|
49
64
|
openedQuestions: PendingQuestion[];
|
|
50
65
|
artifacts: string[];
|
|
66
|
+
changedArtifacts: boolean;
|
|
51
67
|
}
|
|
52
68
|
|
|
53
|
-
export type DeveloperEvent = ModeEvent | RouteEvent | JudgmentEvent;
|
|
69
|
+
export type DeveloperEvent = ModeEvent | FocusEvent | RouteEvent | JudgmentEvent;
|
|
54
70
|
|
|
55
71
|
export interface DeveloperState {
|
|
56
72
|
mode: DeveloperMode;
|
|
@@ -58,19 +74,25 @@ export interface DeveloperState {
|
|
|
58
74
|
lastRoute?: RouteEvent;
|
|
59
75
|
lastJudgment?: JudgmentEvent;
|
|
60
76
|
pendingQuestions: PendingQuestion[];
|
|
77
|
+
focusedQuestionId?: string;
|
|
78
|
+
implementationFramingRequired: boolean;
|
|
79
|
+
verificationRequired: boolean;
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
export const initialState = (): DeveloperState => ({
|
|
64
83
|
mode: "off",
|
|
65
84
|
pendingQuestions: [],
|
|
85
|
+
implementationFramingRequired: false,
|
|
86
|
+
verificationRequired: false,
|
|
66
87
|
});
|
|
67
88
|
|
|
68
|
-
export type ProtocolState = "idle" | "needs-judgment" | "needs-evidence" | "blocked";
|
|
89
|
+
export type ProtocolState = "idle" | "needs-judgment" | "needs-evidence" | "needs-verification" | "blocked";
|
|
69
90
|
|
|
70
91
|
export function protocolState(state: DeveloperState): ProtocolState {
|
|
71
92
|
if (state.activeRoute) return "needs-judgment";
|
|
72
93
|
if (state.pendingQuestions.some((question) => question.status === "blocked")) return "blocked";
|
|
73
94
|
if (state.pendingQuestions.length > 0) return "needs-evidence";
|
|
95
|
+
if (state.verificationRequired) return "needs-verification";
|
|
74
96
|
return "idle";
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -84,12 +106,19 @@ export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent
|
|
|
84
106
|
return { ...state, mode: event.mode };
|
|
85
107
|
}
|
|
86
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
|
+
|
|
87
114
|
if (event.kind === "route") {
|
|
88
115
|
if (state.activeRoute) return state;
|
|
89
116
|
return {
|
|
90
117
|
...state,
|
|
91
118
|
activeRoute: event,
|
|
92
119
|
lastRoute: event,
|
|
120
|
+
focusedQuestionId:
|
|
121
|
+
event.targetQuestionId === state.focusedQuestionId ? undefined : state.focusedQuestionId,
|
|
93
122
|
};
|
|
94
123
|
}
|
|
95
124
|
|
|
@@ -106,7 +135,7 @@ export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent
|
|
|
106
135
|
pending = upsertQuestion(pending, {
|
|
107
136
|
id: route.targetQuestionId,
|
|
108
137
|
question: route.question,
|
|
109
|
-
status: event.status,
|
|
138
|
+
status: event.status === "blocked" ? "blocked" : "needs-evidence",
|
|
110
139
|
sourceRouteId: route.routeId,
|
|
111
140
|
});
|
|
112
141
|
}
|
|
@@ -114,18 +143,35 @@ export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent
|
|
|
114
143
|
pending = upsertQuestion(pending, {
|
|
115
144
|
id: `question:${route.routeId}`,
|
|
116
145
|
question: route.question,
|
|
117
|
-
status: event.status,
|
|
146
|
+
status: event.status === "blocked" ? "blocked" : "needs-evidence",
|
|
118
147
|
sourceRouteId: route.routeId,
|
|
119
148
|
});
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
for (const question of event.openedQuestions) pending = upsertQuestion(pending, question);
|
|
123
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
|
+
|
|
124
165
|
return {
|
|
125
166
|
...state,
|
|
126
167
|
activeRoute: undefined,
|
|
127
168
|
lastJudgment: judgment,
|
|
128
169
|
pendingQuestions: pending,
|
|
170
|
+
focusedQuestionId: pending.some((question) => question.id === state.focusedQuestionId)
|
|
171
|
+
? state.focusedQuestionId
|
|
172
|
+
: undefined,
|
|
173
|
+
implementationFramingRequired,
|
|
174
|
+
verificationRequired,
|
|
129
175
|
};
|
|
130
176
|
}
|
|
131
177
|
|
|
@@ -149,6 +195,22 @@ function isDirectExecutionProfile(value: unknown): value is DirectExecutionProfi
|
|
|
149
195
|
return value === "ordinary" || value === "behavior-preserving-structure";
|
|
150
196
|
}
|
|
151
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
|
+
|
|
152
214
|
function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
|
|
153
215
|
if (!isObject(value)) return undefined;
|
|
154
216
|
if (
|
|
@@ -169,13 +231,22 @@ function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
|
|
|
169
231
|
|
|
170
232
|
export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefined {
|
|
171
233
|
if (!isObject(value)) return undefined;
|
|
172
|
-
if (
|
|
234
|
+
if (
|
|
235
|
+
value.protocol !== PROTOCOL &&
|
|
236
|
+
value.protocol !== PREVIOUS_PROTOCOL &&
|
|
237
|
+
value.protocol !== LEGACY_PROTOCOL
|
|
238
|
+
) return undefined;
|
|
173
239
|
|
|
174
240
|
if (value.kind === "mode") {
|
|
175
241
|
if (!isMode(value.mode)) return undefined;
|
|
176
242
|
return { protocol: PROTOCOL, kind: "mode", mode: value.mode };
|
|
177
243
|
}
|
|
178
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
|
+
|
|
179
250
|
if (value.kind === "route") {
|
|
180
251
|
if (
|
|
181
252
|
typeof value.routeId !== "string" ||
|
|
@@ -185,7 +256,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
185
256
|
!isStringArray(value.knownEvidence) ||
|
|
186
257
|
(value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
|
|
187
258
|
(value.methodLocation !== undefined && typeof value.methodLocation !== "string") ||
|
|
188
|
-
(value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile))
|
|
259
|
+
(value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile)) ||
|
|
260
|
+
(value.directStep !== undefined && !parseDirectStep(value.directStep))
|
|
189
261
|
) {
|
|
190
262
|
return undefined;
|
|
191
263
|
}
|
|
@@ -200,6 +272,7 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
200
272
|
targetQuestionId: value.targetQuestionId,
|
|
201
273
|
methodLocation: value.methodLocation,
|
|
202
274
|
executionProfile: value.executionProfile,
|
|
275
|
+
directStep: value.directStep === undefined ? undefined : parseDirectStep(value.directStep),
|
|
203
276
|
};
|
|
204
277
|
}
|
|
205
278
|
|
|
@@ -231,9 +304,10 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
231
304
|
id: `question:${value.routeId}:legacy:${index + 1}`,
|
|
232
305
|
question,
|
|
233
306
|
status: "needs-evidence",
|
|
234
|
-
sourceRouteId: value.routeId,
|
|
307
|
+
sourceRouteId: String(value.routeId),
|
|
235
308
|
})),
|
|
236
309
|
artifacts: value.artifacts,
|
|
310
|
+
changedArtifacts: false,
|
|
237
311
|
};
|
|
238
312
|
}
|
|
239
313
|
|
|
@@ -251,6 +325,7 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
251
325
|
basis: value.basis,
|
|
252
326
|
openedQuestions: openedQuestions as PendingQuestion[],
|
|
253
327
|
artifacts: value.artifacts,
|
|
328
|
+
changedArtifacts: typeof value.changedArtifacts === "boolean" ? value.changedArtifacts : false,
|
|
254
329
|
};
|
|
255
330
|
}
|
|
256
331
|
|
|
@@ -261,7 +336,7 @@ interface BranchEntryLike {
|
|
|
261
336
|
message?: { role?: string; toolName?: string; details?: unknown };
|
|
262
337
|
}
|
|
263
338
|
|
|
264
|
-
const DEVELOPER_TOOL_NAMES = new Set([
|
|
339
|
+
const DEVELOPER_TOOL_NAMES = new Set<string>([
|
|
265
340
|
ROUTE_TOOL,
|
|
266
341
|
JUDGMENT_TOOL,
|
|
267
342
|
LEGACY_ROUTE_TOOL,
|
|
@@ -269,7 +344,10 @@ const DEVELOPER_TOOL_NAMES = new Set([
|
|
|
269
344
|
]);
|
|
270
345
|
|
|
271
346
|
export function eventFromBranchEntry(entry: BranchEntryLike): DeveloperEvent | undefined {
|
|
272
|
-
if (
|
|
347
|
+
if (
|
|
348
|
+
entry.type === "custom" &&
|
|
349
|
+
(entry.customType === MODE_ENTRY || entry.customType === FOCUS_ENTRY)
|
|
350
|
+
) {
|
|
273
351
|
return normalizeDeveloperEvent(entry.data);
|
|
274
352
|
}
|
|
275
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,33 +21,23 @@
|
|
|
26
21
|
"publishConfig": {
|
|
27
22
|
"access": "public"
|
|
28
23
|
},
|
|
29
|
-
"files": [
|
|
30
|
-
"extensions",
|
|
31
|
-
"skills",
|
|
32
|
-
"README.md",
|
|
33
|
-
"SOURCES.md",
|
|
34
|
-
"LICENSE"
|
|
35
|
-
],
|
|
24
|
+
"files": ["extensions", "skills", "README.md", "SOURCES.md", "LICENSE"],
|
|
36
25
|
"engines": {
|
|
37
26
|
"node": ">=22.19.0"
|
|
38
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
|
+
},
|
|
39
33
|
"pi": {
|
|
40
|
-
"extensions": [
|
|
41
|
-
|
|
42
|
-
],
|
|
43
|
-
"skills": [
|
|
44
|
-
"./skills"
|
|
45
|
-
]
|
|
34
|
+
"extensions": ["./extensions/developer.ts"],
|
|
35
|
+
"skills": ["./skills"]
|
|
46
36
|
},
|
|
47
37
|
"peerDependencies": {
|
|
48
38
|
"@earendil-works/pi-ai": "*",
|
|
49
39
|
"@earendil-works/pi-coding-agent": "*",
|
|
50
40
|
"@earendil-works/pi-tui": "*",
|
|
51
41
|
"typebox": "*"
|
|
52
|
-
},
|
|
53
|
-
"scripts": {
|
|
54
|
-
"check": "node scripts/check-package.mjs && node --test tests/*.test.ts",
|
|
55
|
-
"eval": "node scripts/eval-rpc.mjs",
|
|
56
|
-
"eval:json": "node scripts/eval-json.mjs"
|
|
57
42
|
}
|
|
58
|
-
}
|
|
43
|
+
}
|