@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/developer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { dirname, resolve } from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
|
|
@@ -14,12 +15,9 @@ import {
|
|
|
14
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
15
16
|
import { Type } from "typebox";
|
|
16
17
|
|
|
18
|
+
import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
|
|
17
19
|
import {
|
|
18
|
-
|
|
19
|
-
loadCandidateSkills,
|
|
20
|
-
renderSkillMethod,
|
|
21
|
-
} from "./skills.ts";
|
|
22
|
-
import {
|
|
20
|
+
FOCUS_ENTRY,
|
|
23
21
|
JUDGMENT_TOOL,
|
|
24
22
|
MODE_ENTRY,
|
|
25
23
|
PROTOCOL,
|
|
@@ -31,6 +29,8 @@ import {
|
|
|
31
29
|
reconstructState,
|
|
32
30
|
type DeveloperMode,
|
|
33
31
|
type DeveloperState,
|
|
32
|
+
type DirectExecutionProfile,
|
|
33
|
+
type FocusEvent,
|
|
34
34
|
type JudgmentEvent,
|
|
35
35
|
type ModeEvent,
|
|
36
36
|
type RouteEvent,
|
|
@@ -50,7 +50,13 @@ import {
|
|
|
50
50
|
} from "./tui.ts";
|
|
51
51
|
|
|
52
52
|
const PROTOCOL_TOOLS = [ROUTE_TOOL, JUDGMENT_TOOL] as const;
|
|
53
|
-
const
|
|
53
|
+
const extensionRoot = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const skillsRoot = resolve(extensionRoot, "..", "skills");
|
|
55
|
+
const structuralChangeMethodPath = resolve(
|
|
56
|
+
extensionRoot,
|
|
57
|
+
"references",
|
|
58
|
+
"behavior-preserving-structural-change.md",
|
|
59
|
+
);
|
|
54
60
|
const MAX_PENDING_QUESTIONS = 20;
|
|
55
61
|
const MAX_QUESTION_CHARS = 2_000;
|
|
56
62
|
const MAX_EVIDENCE_CHARS = 2_000;
|
|
@@ -103,6 +109,25 @@ function compactLine(value: string, maxChars = 160): string {
|
|
|
103
109
|
return line.length <= maxChars ? line : `${line.slice(0, maxChars - 1)}…`;
|
|
104
110
|
}
|
|
105
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
|
+
|
|
106
131
|
function summarizeState(state: DeveloperState): string {
|
|
107
132
|
if (state.mode === "off") return "developer: off";
|
|
108
133
|
const target = state.activeRoute ? state.activeRoute.owner : "none";
|
|
@@ -113,11 +138,14 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
113
138
|
const lines = [
|
|
114
139
|
"",
|
|
115
140
|
`Developer protocol (${state.mode}) is active.`,
|
|
116
|
-
"-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"-
|
|
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.",
|
|
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.",
|
|
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.",
|
|
121
149
|
"- Protocol state is routing bookkeeping. Idle never proves product completion, user acceptance, or current verification.",
|
|
122
150
|
"- Product files are changed with Pi implementation tools. Developer protocol tools only route and record judgments.",
|
|
123
151
|
];
|
|
@@ -128,6 +156,17 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
128
156
|
);
|
|
129
157
|
}
|
|
130
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
|
+
|
|
131
170
|
if (state.activeRoute) {
|
|
132
171
|
lines.push(
|
|
133
172
|
`Active route: ${state.activeRoute.routeId} · ${state.activeRoute.owner} · ${state.activeRoute.question}`,
|
|
@@ -144,7 +183,7 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
144
183
|
lines.push(`- ${question.id} · ${question.status} · ${question.question}`);
|
|
145
184
|
}
|
|
146
185
|
lines.push(
|
|
147
|
-
|
|
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.",
|
|
148
187
|
);
|
|
149
188
|
}
|
|
150
189
|
lines.push(
|
|
@@ -156,7 +195,8 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
156
195
|
function routeRenderText(event: RouteEvent | undefined): string {
|
|
157
196
|
if (!event) return "Route unavailable";
|
|
158
197
|
const target = event.targetQuestionId ? ` · revisits ${event.targetQuestionId}` : "";
|
|
159
|
-
|
|
198
|
+
const profile = event.executionProfile ? `/${event.executionProfile}` : "";
|
|
199
|
+
return `${event.owner}${profile} · ${compactLine(event.question)}${target}`;
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
function judgmentRenderText(event: JudgmentEvent | undefined): string {
|
|
@@ -165,10 +205,10 @@ function judgmentRenderText(event: JudgmentEvent | undefined): string {
|
|
|
165
205
|
}
|
|
166
206
|
|
|
167
207
|
export default async function developer(pi: ExtensionAPI) {
|
|
168
|
-
const candidates = loadCandidateSkills(skillsRoot);
|
|
169
208
|
let availableSkills = new Map<string, Skill>();
|
|
170
209
|
let state = initialState();
|
|
171
210
|
let routeOpening = false;
|
|
211
|
+
const routesWithMutation = new Set<string>();
|
|
172
212
|
let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
|
|
173
213
|
|
|
174
214
|
pi.registerFlag("develop-mode", {
|
|
@@ -201,7 +241,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
201
241
|
"developer",
|
|
202
242
|
ctx.mode === "tui" ? renderDeveloperFooter(state, ctx.ui.theme) : summarizeState(state),
|
|
203
243
|
);
|
|
204
|
-
if (
|
|
244
|
+
if (
|
|
245
|
+
!state.activeRoute &&
|
|
246
|
+
state.pendingQuestions.length === 0 &&
|
|
247
|
+
!state.implementationFramingRequired &&
|
|
248
|
+
!state.verificationRequired
|
|
249
|
+
) {
|
|
205
250
|
ctx.ui.setWidget("developer", undefined);
|
|
206
251
|
return;
|
|
207
252
|
}
|
|
@@ -222,6 +267,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
222
267
|
.slice(0, 3)
|
|
223
268
|
.map((question) => `open · ${question.id} · ${compactLine(question.question)}`),
|
|
224
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"] : []),
|
|
225
272
|
];
|
|
226
273
|
ctx.ui.setWidget("developer", lines, { placement: "belowEditor" });
|
|
227
274
|
};
|
|
@@ -240,17 +287,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
240
287
|
refreshUI(ctx);
|
|
241
288
|
};
|
|
242
289
|
|
|
243
|
-
const
|
|
290
|
+
const SharedRouteParams = {
|
|
244
291
|
question: Type.String({
|
|
245
292
|
minLength: 1,
|
|
246
293
|
maxLength: MAX_QUESTION_CHARS,
|
|
247
294
|
description: "The single concrete judgment or action question to route",
|
|
248
295
|
}),
|
|
249
|
-
owner: Type.String({
|
|
250
|
-
minLength: 1,
|
|
251
|
-
maxLength: 64,
|
|
252
|
-
description: "direct, or an exact skill name from the current Available Developer skills list",
|
|
253
|
-
}),
|
|
254
296
|
reason: Type.String({
|
|
255
297
|
minLength: 1,
|
|
256
298
|
maxLength: MAX_QUESTION_CHARS,
|
|
@@ -265,17 +307,59 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
265
307
|
open_question_id: Type.Optional(
|
|
266
308
|
Type.String({ maxLength: 512, description: "Exact pending question ID when this route revisits one" }),
|
|
267
309
|
),
|
|
268
|
-
}
|
|
310
|
+
};
|
|
311
|
+
const RouteParams = Type.Union([
|
|
312
|
+
Type.Object(
|
|
313
|
+
{
|
|
314
|
+
...SharedRouteParams,
|
|
315
|
+
owner: Type.String({
|
|
316
|
+
minLength: 1,
|
|
317
|
+
maxLength: 64,
|
|
318
|
+
pattern: "^(?!direct$)[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
319
|
+
description: "Exact skill name from the current Available Developer skills list",
|
|
320
|
+
}),
|
|
321
|
+
},
|
|
322
|
+
{ additionalProperties: false, description: "Route one question to a Developer skill" },
|
|
323
|
+
),
|
|
324
|
+
Type.Object(
|
|
325
|
+
{
|
|
326
|
+
...SharedRouteParams,
|
|
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
|
+
}),
|
|
343
|
+
execution_profile: Type.Optional(
|
|
344
|
+
Type.Literal("behavior-preserving-structure", {
|
|
345
|
+
description: "Load the focused structural-mutation protocol; omit for ordinary direct action",
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
348
|
+
},
|
|
349
|
+
{ additionalProperties: false, description: "Route one already-justified direct action" },
|
|
350
|
+
),
|
|
351
|
+
]);
|
|
269
352
|
|
|
270
353
|
pi.registerTool({
|
|
271
354
|
name: ROUTE_TOOL,
|
|
272
355
|
label: "Developer Route Question",
|
|
273
356
|
description:
|
|
274
|
-
"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.",
|
|
275
358
|
promptSnippet: "Choose how to handle one development question",
|
|
276
359
|
promptGuidelines: [
|
|
277
360
|
`Call ${ROUTE_TOOL} only when there is no active Developer route.`,
|
|
278
|
-
`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.`,
|
|
279
363
|
],
|
|
280
364
|
parameters: RouteParams,
|
|
281
365
|
executionMode: "sequential",
|
|
@@ -292,25 +376,65 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
292
376
|
const reason = params.reason.trim();
|
|
293
377
|
if (!question || !reason) fail("Question and reason must contain non-whitespace text.");
|
|
294
378
|
|
|
295
|
-
const
|
|
379
|
+
const explicitQuestionId = params.open_question_id?.trim() || undefined;
|
|
380
|
+
const targetQuestionId = inferredQuestionId(state, question, explicitQuestionId);
|
|
296
381
|
if (targetQuestionId && !state.pendingQuestions.some((item) => item.id === targetQuestionId)) {
|
|
297
382
|
fail(`Unknown pending question ID: ${targetQuestionId}`);
|
|
298
383
|
}
|
|
299
384
|
|
|
300
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
|
+
}
|
|
301
395
|
const skill = owner === "direct" ? undefined : availableSkills.get(owner);
|
|
302
396
|
if (owner !== "direct" && !skill) {
|
|
303
397
|
fail(`Developer skill ${owner} is unavailable or disabled in the current Pi resource configuration.`);
|
|
304
398
|
}
|
|
399
|
+
const requestedExecutionProfile =
|
|
400
|
+
"execution_profile" in params ? params.execution_profile : undefined;
|
|
401
|
+
if (owner !== "direct" && requestedExecutionProfile !== undefined) {
|
|
402
|
+
fail("execution_profile is valid only when owner=direct.");
|
|
403
|
+
}
|
|
404
|
+
const executionProfile: DirectExecutionProfile | undefined =
|
|
405
|
+
owner === "direct" ? (requestedExecutionProfile ?? "ordinary") : undefined;
|
|
305
406
|
|
|
306
407
|
const method =
|
|
408
|
+
owner !== "direct"
|
|
409
|
+
? await renderSkillMethod(skill!)
|
|
410
|
+
: executionProfile === "behavior-preserving-structure"
|
|
411
|
+
? [
|
|
412
|
+
'<developer-direct-profile name="behavior-preserving-structure">',
|
|
413
|
+
(await readFile(structuralChangeMethodPath, "utf8")).trim(),
|
|
414
|
+
"</developer-direct-profile>",
|
|
415
|
+
].join("\n")
|
|
416
|
+
: [
|
|
417
|
+
"# Direct action",
|
|
418
|
+
"",
|
|
419
|
+
"The next local action is already justified. Keep this route open while using Pi implementation tools and collecting evidence.",
|
|
420
|
+
].join("\n");
|
|
421
|
+
|
|
422
|
+
const directStep =
|
|
307
423
|
owner === "direct"
|
|
308
|
-
?
|
|
309
|
-
"
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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;
|
|
314
438
|
|
|
315
439
|
const event: RouteEvent = {
|
|
316
440
|
protocol: PROTOCOL,
|
|
@@ -322,12 +446,23 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
322
446
|
knownEvidence: (params.known_evidence ?? []).map((item) => item.trim()).filter(Boolean),
|
|
323
447
|
targetQuestionId,
|
|
324
448
|
methodLocation: skill?.filePath,
|
|
449
|
+
executionProfile,
|
|
450
|
+
directStep,
|
|
325
451
|
};
|
|
326
452
|
const response = [
|
|
327
453
|
`Route ID: ${event.routeId}`,
|
|
328
454
|
`Question: ${event.question}`,
|
|
329
455
|
`Target: ${event.owner}`,
|
|
330
456
|
skill ? `Skill location: ${skill.filePath}` : "Skill location: direct action; no skill file",
|
|
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
|
+
: []),
|
|
331
466
|
`Reason: ${event.reason}`,
|
|
332
467
|
`Known evidence: ${event.knownEvidence.length > 0 ? event.knownEvidence.join(" | ") : "none"}`,
|
|
333
468
|
targetQuestionId ? `Revisits pending question: ${targetQuestionId}` : "Revisits pending question: none",
|
|
@@ -356,11 +491,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
356
491
|
context.lastComponent,
|
|
357
492
|
);
|
|
358
493
|
},
|
|
359
|
-
renderResult(result, { expanded, isPartial
|
|
494
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
360
495
|
if (isPartial) {
|
|
361
496
|
return reusableText(theme.fg("warning", "routing development question…"), context.lastComponent);
|
|
362
497
|
}
|
|
363
|
-
if (isError) {
|
|
498
|
+
if (context.isError) {
|
|
364
499
|
return reusableText(
|
|
365
500
|
theme.fg("error", resultText(result) || "Developer route failed"),
|
|
366
501
|
context.lastComponent,
|
|
@@ -381,6 +516,14 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
381
516
|
}
|
|
382
517
|
text += `\n${theme.fg("dim", `revisits · ${event.targetQuestionId ?? "none"}`)}`;
|
|
383
518
|
text += `\n${theme.fg("dim", `skill · ${event.methodLocation ?? "direct action"}`)}`;
|
|
519
|
+
if (event.executionProfile) {
|
|
520
|
+
text += `\n${theme.fg("dim", `profile · ${event.executionProfile}`)}`;
|
|
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
|
+
}
|
|
384
527
|
}
|
|
385
528
|
if (!expanded && event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
386
529
|
return reusableText(text, context.lastComponent);
|
|
@@ -459,6 +602,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
459
602
|
basis,
|
|
460
603
|
openedQuestions,
|
|
461
604
|
artifacts: (params.artifacts ?? []).map((item) => item.trim()).filter(Boolean),
|
|
605
|
+
changedArtifacts: routesWithMutation.has(params.route_id),
|
|
462
606
|
};
|
|
463
607
|
const nextState = applyDeveloperEvent(state, event);
|
|
464
608
|
if (nextState.pendingQuestions.length > MAX_PENDING_QUESTIONS) {
|
|
@@ -469,12 +613,17 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
469
613
|
|
|
470
614
|
const next = protocolState(nextState);
|
|
471
615
|
const nextMessage =
|
|
472
|
-
|
|
473
|
-
?
|
|
474
|
-
|
|
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.`;
|
|
475
623
|
const response = `Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\n${nextMessage}`;
|
|
476
624
|
ensureSafeToolText(response, "Developer judgment result");
|
|
477
625
|
|
|
626
|
+
routesWithMutation.delete(params.route_id);
|
|
478
627
|
state = nextState;
|
|
479
628
|
syncProtocolTools();
|
|
480
629
|
refreshUI(ctx);
|
|
@@ -497,11 +646,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
497
646
|
context.lastComponent,
|
|
498
647
|
);
|
|
499
648
|
},
|
|
500
|
-
renderResult(result, { expanded, isPartial
|
|
649
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
501
650
|
if (isPartial) {
|
|
502
651
|
return reusableText(theme.fg("warning", "recording development judgment…"), context.lastComponent);
|
|
503
652
|
}
|
|
504
|
-
if (isError) {
|
|
653
|
+
if (context.isError) {
|
|
505
654
|
return reusableText(
|
|
506
655
|
theme.fg("error", resultText(result) || "Developer judgment failed"),
|
|
507
656
|
context.lastComponent,
|
|
@@ -537,7 +686,6 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
537
686
|
if (typeof ctx.getSystemPromptOptions !== "function") return;
|
|
538
687
|
availableSkills = availablePackageSkills(
|
|
539
688
|
ctx.getSystemPromptOptions().skills ?? [],
|
|
540
|
-
candidates,
|
|
541
689
|
skillsRoot,
|
|
542
690
|
);
|
|
543
691
|
};
|
|
@@ -625,8 +773,16 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
625
773
|
if (!questionId) return;
|
|
626
774
|
const question = state.pendingQuestions.find((item) => item.id === questionId);
|
|
627
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);
|
|
628
784
|
prepareQuestionPrompt(ctx, question);
|
|
629
|
-
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");
|
|
630
786
|
return;
|
|
631
787
|
}
|
|
632
788
|
ctx.ui.notify(
|
|
@@ -668,16 +824,18 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
668
824
|
});
|
|
669
825
|
|
|
670
826
|
pi.on("before_agent_start", (event) => {
|
|
671
|
-
availableSkills = availablePackageSkills(event.systemPromptOptions.skills ?? [],
|
|
827
|
+
availableSkills = availablePackageSkills(event.systemPromptOptions.skills ?? [], skillsRoot);
|
|
672
828
|
if (state.mode === "off") return;
|
|
673
829
|
return { systemPrompt: event.systemPrompt + protocolPrompt(state, [...availableSkills.keys()]) };
|
|
674
830
|
});
|
|
675
831
|
|
|
676
832
|
pi.on("tool_call", (event) => {
|
|
677
|
-
if (state.mode !== "strict") return;
|
|
678
833
|
const mutationBuiltins = builtinMutationToolNames(pi.getAllTools());
|
|
679
|
-
if (
|
|
680
|
-
|
|
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;
|
|
681
839
|
return {
|
|
682
840
|
block: true,
|
|
683
841
|
reason: `Developer strict mode requires an active ${ROUTE_TOOL} targeting direct action (owner=direct) before Pi built-in edit, write, or bash.`,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Behavior-Preserving Structural Change
|
|
2
|
+
|
|
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.
|
|
7
|
+
|
|
8
|
+
## Entry Contract
|
|
9
|
+
|
|
10
|
+
Before editing, state:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
Behavior to preserve: the narrow observable claim
|
|
14
|
+
Structural move: one concrete rearrangement
|
|
15
|
+
Pressure: why the accepted task needs or benefits from it now
|
|
16
|
+
Baseline evidence: the cheapest relevant check that is currently green
|
|
17
|
+
Affected surface: files, callers, persisted forms, or public boundaries
|
|
18
|
+
Stop: the next stable, pausable state
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If the behavior claim, timing, or candidate is still under judgment, close or
|
|
22
|
+
pause the direct route and route that question to the appropriate leaf.
|
|
23
|
+
|
|
24
|
+
## Smallest Green Transformation
|
|
25
|
+
|
|
26
|
+
Keep behavior and structure distinct in reasoning even if they eventually share
|
|
27
|
+
a commit. Prefer the smallest movement whose failure can be localized.
|
|
28
|
+
|
|
29
|
+
For replacement-shaped refactors, use this progression:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
new shape parses
|
|
33
|
+
-> new shape executes
|
|
34
|
+
-> callers use its result
|
|
35
|
+
-> replaced code becomes unreachable
|
|
36
|
+
-> replaced code is deleted
|
|
37
|
+
-> relevant evidence is green
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For movement across a boundary, update one sender or caller at a time when the
|
|
41
|
+
old and new forms can safely coexist. Confirm that each intermediate state is
|
|
42
|
+
valid. Do not rely on a final large diff to explain which step changed behavior.
|
|
43
|
+
|
|
44
|
+
## Evidence Rhythm
|
|
45
|
+
|
|
46
|
+
Within the routed movement:
|
|
47
|
+
|
|
48
|
+
1. run the narrowest check that can catch the likely break;
|
|
49
|
+
2. inspect the diff for mixed behavior and structural changes;
|
|
50
|
+
3. reduce the step when the failure cannot be explained locally;
|
|
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.
|
|
54
|
+
|
|
55
|
+
A passing test is useful only when it exercises the preserved behavior. When no
|
|
56
|
+
cheap verifier exists, keep the movement smaller and record the residual risk.
|
|
57
|
+
|
|
58
|
+
## Stable Landing
|
|
59
|
+
|
|
60
|
+
A stable landing is not merely a green command. It is a state where:
|
|
61
|
+
|
|
62
|
+
- the preserved behavior has relevant evidence;
|
|
63
|
+
- names and intermediate compatibility shapes are honest enough to pause;
|
|
64
|
+
- no dead replacement path or half-moved caller remains accidentally active;
|
|
65
|
+
- the diff has one explainable structural purpose;
|
|
66
|
+
- the next decision can be made from the new evidence.
|
|
67
|
+
|
|
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.
|
|
72
|
+
|
|
73
|
+
## Reroute Conditions
|
|
74
|
+
|
|
75
|
+
Close direct execution at the latest stable landing and open a focused judgment
|
|
76
|
+
when the movement reveals:
|
|
77
|
+
|
|
78
|
+
- an unexpected product behavior or policy choice: `specify`;
|
|
79
|
+
- missing cases, transition rules, or replacement obligations: `model`;
|
|
80
|
+
- unresolved ownership, collaboration, or data flow: `sketch`;
|
|
81
|
+
- a new comparison or uncertain structural direction: `signal`;
|
|
82
|
+
- a responsibility, role, boundary, or abstraction candidate: `abstraction-review`;
|
|
83
|
+
- a disputed name or vocabulary boundary: `naming-judgment`;
|
|
84
|
+
- a question of now, after, or never: `schedule`;
|
|
85
|
+
- weak or irrelevant evidence: `verify`.
|
|
86
|
+
|
|
87
|
+
This is adaptive rerouting, not a required sequence.
|
|
88
|
+
|
|
89
|
+
## Worked Mutation Trace
|
|
90
|
+
|
|
91
|
+
Accepted move: replace duplicated schedule conversion in two callers with the
|
|
92
|
+
already-reviewed pure `toScheduleContent` function.
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
Behavior to preserve:
|
|
96
|
+
both callers produce the same ScheduleContent or domain error for the
|
|
97
|
+
characterized table of variants
|
|
98
|
+
Structural move:
|
|
99
|
+
introduce the function, move one caller, then the second, then delete the
|
|
100
|
+
duplicate expressions
|
|
101
|
+
Baseline evidence:
|
|
102
|
+
focused table test plus both caller integration tests are green
|
|
103
|
+
Affected surface:
|
|
104
|
+
converter module, two callers, no persistence format
|
|
105
|
+
Stop:
|
|
106
|
+
both callers use the converter; old expressions are unreachable and removed
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Mutation sequence:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
1. add the pure function with one characterized caller's exact behavior
|
|
113
|
+
2. run the conversion table
|
|
114
|
+
3. switch caller A; run its integration and inspect the diff
|
|
115
|
+
4. switch caller B; run its integration and inspect the diff
|
|
116
|
+
5. search for the replaced expression and remove dead code
|
|
117
|
+
6. run the focused table, integrations, typecheck, and packed-source check
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If caller B reveals that `null` and missing have different product meanings, do
|
|
121
|
+
not broaden the helper until everything passes. Return that policy to `model`;
|
|
122
|
+
caller A remains at a stable landing. If both callers move cleanly, stop.
|
|
123
|
+
Creating a converter class or registry is a new judgment, not “finishing” this
|
|
124
|
+
refactor.
|
|
125
|
+
|
|
126
|
+
## Failure Checks
|
|
127
|
+
|
|
128
|
+
The protocol is being misused when:
|
|
129
|
+
|
|
130
|
+
- tests are changed merely to preserve green output after behavior drift;
|
|
131
|
+
- several differences are removed before the first can be inspected;
|
|
132
|
+
- a class, strategy, polymorphic hierarchy, or factory was chosen in advance;
|
|
133
|
+
- behavior work is hidden inside a structural label;
|
|
134
|
+
- the route continues after a new human-owned policy question appears;
|
|
135
|
+
- an intermediate state cannot safely run or be reviewed;
|
|
136
|
+
- cleanup continues past the accepted pressure because the code could be nicer.
|
|
137
|
+
|
|
138
|
+
## Source Trace
|
|
139
|
+
|
|
140
|
+
- Sandi Metz, Katrina Owen, and TJ Stankus, *99 Bottles of OOP*, Second
|
|
141
|
+
Edition, version 2.2.2, 2024: chapters 3-5 on methodical transformations,
|
|
142
|
+
flocking, gradual refactoring, stable landings, and sender-by-sender movement.
|
|
143
|
+
- Kent Beck, *Tidy First?: A Personal Exercise in Empirical Software Design*,
|
|
144
|
+
O'Reilly Media, 2023: separating behavior and structure, keeping tidyings
|
|
145
|
+
small, and managing structural change as optionality.
|
|
146
|
+
- Matthias Felleisen, Robert Bruce Findler, Matthew Flatt, and Shriram
|
|
147
|
+
Krishnamurthi, *How to Design Programs, Second Edition*, MIT Press, 2018:
|
|
148
|
+
iterative refinement and propagating a changed data design through dependent
|
|
149
|
+
artifacts.
|
package/extensions/skills.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { isAbsolute, relative, resolve } from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_MAX_BYTES,
|
|
7
7
|
DEFAULT_MAX_LINES,
|
|
8
|
-
loadSkillsFromDir,
|
|
9
8
|
stripFrontmatter,
|
|
10
9
|
type Skill,
|
|
11
10
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -33,30 +32,16 @@ export function isWithinRoot(root: string, path: string): boolean {
|
|
|
33
32
|
return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export function loadCandidateSkills(skillsRoot: string): Map<string, Skill> {
|
|
37
|
-
const loaded = loadSkillsFromDir({ dir: skillsRoot, source: "@hobin/developer" });
|
|
38
|
-
const candidates = new Map<string, Skill>();
|
|
39
|
-
|
|
40
|
-
for (const skill of loaded.skills) {
|
|
41
|
-
if (candidates.has(skill.name)) throw new Error(`Duplicate Developer skill name: ${skill.name}`);
|
|
42
|
-
candidates.set(skill.name, skill);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (candidates.size === 0) throw new Error(`No Developer skills found in ${skillsRoot}`);
|
|
46
|
-
return candidates;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
35
|
export function availablePackageSkills(
|
|
50
36
|
loadedSkills: Skill[],
|
|
51
|
-
candidates: ReadonlyMap<string, Skill>,
|
|
52
37
|
skillsRoot: string,
|
|
53
38
|
): Map<string, Skill> {
|
|
54
39
|
const available = new Map<string, Skill>();
|
|
55
40
|
|
|
56
41
|
for (const skill of loadedSkills) {
|
|
57
42
|
if (skill.disableModelInvocation) continue;
|
|
58
|
-
if (!candidates.has(skill.name)) continue;
|
|
59
43
|
if (!isWithinRoot(skillsRoot, skill.filePath)) continue;
|
|
44
|
+
if (available.has(skill.name)) throw new Error(`Duplicate Pi-loaded Developer skill name: ${skill.name}`);
|
|
60
45
|
available.set(skill.name, skill);
|
|
61
46
|
}
|
|
62
47
|
|