pi-matt-pocock 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +38 -0
- package/TODO.md +11 -0
- package/index.ts +1 -0
- package/package.json +44 -0
- package/procedures/ADR-FORMAT.md +47 -0
- package/procedures/AGENT-BRIEF.md +207 -0
- package/procedures/CONTEXT-FORMAT.md +60 -0
- package/procedures/DEEPENING.md +37 -0
- package/procedures/DESIGN-IT-TWICE.md +44 -0
- package/procedures/GLOSSARY-FORMAT.md +35 -0
- package/procedures/GLOSSARY.md +201 -0
- package/procedures/HTML-REPORT.md +123 -0
- package/procedures/LEARNING-RECORD-FORMAT.md +46 -0
- package/procedures/LOGIC.md +79 -0
- package/procedures/MISSION-FORMAT.md +31 -0
- package/procedures/OUT-OF-SCOPE.md +105 -0
- package/procedures/RESOURCES-FORMAT.md +32 -0
- package/procedures/SKILL-MECHANICS.md +22 -0
- package/procedures/UI.md +112 -0
- package/procedures/bdd-best-practices.md +68 -0
- package/procedures/bdd.md +98 -0
- package/procedures/code-review.md +111 -0
- package/procedures/codebase-design.md +113 -0
- package/procedures/diagnosing-bugs.md +139 -0
- package/procedures/domain-modeling.md +73 -0
- package/procedures/domain.md +51 -0
- package/procedures/gherkin-guide.md +109 -0
- package/procedures/grill-me.md +5 -0
- package/procedures/grill-with-docs.md +5 -0
- package/procedures/grilling.md +11 -0
- package/procedures/handoff.md +13 -0
- package/procedures/hitl-loop.template.sh +44 -0
- package/procedures/implement.md +13 -0
- package/procedures/improve-codebase-architecture.md +69 -0
- package/procedures/issue-tracker-github.md +45 -0
- package/procedures/issue-tracker-gitlab.md +46 -0
- package/procedures/issue-tracker-local.md +30 -0
- package/procedures/mocking.md +59 -0
- package/procedures/phase-boundaries.md +55 -0
- package/procedures/prototype.md +25 -0
- package/procedures/research.md +11 -0
- package/procedures/resolving-merge-conflicts.md +13 -0
- package/procedures/setup-matt-pocock-skills.md +114 -0
- package/procedures/tdd.md +60 -0
- package/procedures/teach.md +137 -0
- package/procedures/template.sh +204 -0
- package/procedures/testing-anti-patterns.md +37 -0
- package/procedures/tests.md +77 -0
- package/procedures/to-questionnaire.md +47 -0
- package/procedures/to-spec.md +77 -0
- package/procedures/to-tickets.md +97 -0
- package/procedures/triage-labels.md +15 -0
- package/procedures/triage.md +110 -0
- package/procedures/wait-what.md +1 -0
- package/procedures/wayfinder.md +127 -0
- package/procedures/wizard.md +39 -0
- package/procedures/writing-for-agents.md +76 -0
- package/procedures/writing-great-skills.md +81 -0
- package/src/index.ts +485 -0
- package/src/procedures.ts +33 -0
- package/src/workflow.ts +192 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import {
|
|
2
|
+
keyHint,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import {
|
|
9
|
+
clearPiStatus,
|
|
10
|
+
createStaticToolLifecycleResultRenderer,
|
|
11
|
+
eventToolLifecycle,
|
|
12
|
+
formatToolErrorLine,
|
|
13
|
+
notifyPi,
|
|
14
|
+
safeDisplayText,
|
|
15
|
+
} from "@fradser/pi-kit";
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
import { procedurePrompt } from "./procedures.ts";
|
|
18
|
+
import {
|
|
19
|
+
availableWorkflowsGuidance,
|
|
20
|
+
findWorkflowRoute,
|
|
21
|
+
formatReadableWorkflowSubject,
|
|
22
|
+
latestWorkflowState,
|
|
23
|
+
normalizeProcedureName,
|
|
24
|
+
phaseForProcedure,
|
|
25
|
+
transitionProcedureOptions,
|
|
26
|
+
transitionProcedures,
|
|
27
|
+
WORKFLOW_STATE_ENTRY,
|
|
28
|
+
workflowGuidance,
|
|
29
|
+
workflowRoutes,
|
|
30
|
+
type WorkflowState,
|
|
31
|
+
} from "./workflow.ts";
|
|
32
|
+
|
|
33
|
+
function stringEnum<T extends readonly string[]>(values: T, options?: Record<string, unknown>) {
|
|
34
|
+
return Type.Unsafe<T[number]>({
|
|
35
|
+
type: "string",
|
|
36
|
+
enum: [...values],
|
|
37
|
+
...options,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function workflowRouteParameters<T extends string>(route: T) {
|
|
42
|
+
return Type.Object({
|
|
43
|
+
route: Type.Literal(route, { description: "Target engineering workflow route." }),
|
|
44
|
+
procedure: Type.Optional(stringEnum(transitionProcedureOptions(route), {
|
|
45
|
+
description: `Specific ${route} procedure. Omit it to use the route default.`,
|
|
46
|
+
})),
|
|
47
|
+
phase: Type.Optional(Type.String({ description: "Target workflow phase." })),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function workflowToolParameters() {
|
|
52
|
+
return Type.Union([
|
|
53
|
+
workflowRouteParameters("idea-to-ship"),
|
|
54
|
+
workflowRouteParameters("hard-bug"),
|
|
55
|
+
workflowRouteParameters("triage"),
|
|
56
|
+
workflowRouteParameters("wayfinding"),
|
|
57
|
+
workflowRouteParameters("architecture"),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeExpandHint(): string {
|
|
62
|
+
try {
|
|
63
|
+
const hint = keyHint("app.tools.expand", "to expand");
|
|
64
|
+
return String(hint);
|
|
65
|
+
} catch {
|
|
66
|
+
return "to expand";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let activeWorkflow: WorkflowState | undefined;
|
|
71
|
+
|
|
72
|
+
function setInterviewToolActive(active: boolean): void {
|
|
73
|
+
const tools = pi.getActiveTools();
|
|
74
|
+
const nextTools = active
|
|
75
|
+
? [...new Set([...tools, "matt_pocock_ask"])]
|
|
76
|
+
: tools.filter((tool) => tool !== "matt_pocock_ask");
|
|
77
|
+
pi.setActiveTools(nextTools);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function endWorkflow(ctx: ExtensionContext): void {
|
|
81
|
+
activeWorkflow = undefined;
|
|
82
|
+
pi.appendEntry(WORKFLOW_STATE_ENTRY, { active: false });
|
|
83
|
+
setInterviewToolActive(false);
|
|
84
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function persistWorkflow(state: WorkflowState): void {
|
|
88
|
+
activeWorkflow = state;
|
|
89
|
+
pi.appendEntry(WORKFLOW_STATE_ENTRY, state);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function unavailableProcedureMessage(route: string, procedure: string): string {
|
|
93
|
+
const allowedProcedures = transitionProcedures(route);
|
|
94
|
+
const defaultProcedure = findWorkflowRoute(route)?.procedure ?? allowedProcedures[0];
|
|
95
|
+
return `Procedure ${procedure} is not available for route ${route}. Valid procedures: ${allowedProcedures.join(", ")}. Omit procedure to use the route default (${defaultProcedure}). Do not switch routes to work around a procedure error.`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loadWorkflowProcedure(state: WorkflowState): string {
|
|
99
|
+
const normalized = normalizeProcedureName(state.procedure);
|
|
100
|
+
if (!transitionProcedures(state.route).includes(normalized)) {
|
|
101
|
+
throw new Error(unavailableProcedureMessage(state.route, state.procedure));
|
|
102
|
+
}
|
|
103
|
+
return procedurePrompt(state.route, normalized, state.phase);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function injectProcedure(content: string): void {
|
|
107
|
+
pi.sendUserMessage(content, { deliverAs: "followUp" });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function activateWorkflow(state: WorkflowState, ctx: ExtensionContext): void {
|
|
111
|
+
const content = loadWorkflowProcedure(state);
|
|
112
|
+
persistWorkflow(state);
|
|
113
|
+
setInterviewToolActive(true);
|
|
114
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
115
|
+
injectProcedure(content);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function routeChoices(): string[] {
|
|
119
|
+
return workflowRoutes().map((route) => `${route.label} — ${route.description}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function routeFromChoice(choice: string): WorkflowState | undefined {
|
|
123
|
+
const route = workflowRoutes().find((candidate) => choice.startsWith(candidate.label));
|
|
124
|
+
return route && {
|
|
125
|
+
route: route.route,
|
|
126
|
+
procedure: route.procedure,
|
|
127
|
+
phase: route.phase,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function chooseRoute(ctx: ExtensionCommandContext): Promise<void> {
|
|
132
|
+
const choice = await ctx.ui.select("Start Matt Pocock workflow", routeChoices());
|
|
133
|
+
if (!choice) return;
|
|
134
|
+
const state = routeFromChoice(choice);
|
|
135
|
+
if (state) activateWorkflow(state, ctx);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function showMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
139
|
+
const choices = ["Start a workflow", "View current workflow", "Transition current workflow", "End current workflow"];
|
|
140
|
+
const choice = await ctx.ui.select("Matt Pocock workflow", choices);
|
|
141
|
+
if (!choice) return;
|
|
142
|
+
if (choice === "Start a workflow") {
|
|
143
|
+
await chooseRoute(ctx);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (choice === "View current workflow") {
|
|
147
|
+
showStatus(ctx);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (choice === "Transition current workflow") {
|
|
151
|
+
await chooseTransition(ctx);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
endWorkflow(ctx);
|
|
155
|
+
notifyPi(ctx.ui, "Matt Pocock workflow ended.", "info");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function chooseTransition(ctx: ExtensionCommandContext): Promise<void> {
|
|
159
|
+
if (!activeWorkflow) {
|
|
160
|
+
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const procedures = transitionProcedures(activeWorkflow.route);
|
|
165
|
+
const choice = await ctx.ui.select(
|
|
166
|
+
`Transition ${activeWorkflow.route} from ${activeWorkflow.phase}`,
|
|
167
|
+
procedures,
|
|
168
|
+
);
|
|
169
|
+
if (!choice) return;
|
|
170
|
+
|
|
171
|
+
activateWorkflow({
|
|
172
|
+
route: activeWorkflow.route,
|
|
173
|
+
procedure: choice,
|
|
174
|
+
phase: phaseForProcedure(choice),
|
|
175
|
+
}, ctx);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function showStatus(ctx: ExtensionContext): void {
|
|
179
|
+
if (!activeWorkflow) {
|
|
180
|
+
notifyPi(ctx.ui, "Matt Pocock workflow: inactive", "info");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
notifyPi(ctx.ui, `Matt Pocock workflow: ${formatReadableWorkflowSubject(activeWorkflow.route, activeWorkflow.phase)}`, "info");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseRoute(args: string): WorkflowState | undefined {
|
|
187
|
+
const route = findWorkflowRoute(args);
|
|
188
|
+
return route && { route: route.route, procedure: route.procedure, phase: route.phase };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function workflowRoutingPrompt(prompt: string, endedWorkflow: boolean): string {
|
|
192
|
+
return `Route and execute this engineering request through the relevant Matt Pocock workflow: ${prompt}
|
|
193
|
+
|
|
194
|
+
${endedWorkflow ? "End any active Matt Pocock workflow first; that workflow has now been ended. " : ""}Determine whether the request needs a structured workflow. If it does, call matt_pocock_workflow with the matching route and procedure, then begin that procedure immediately. If it does not, explain briefly that no Matt Pocock workflow applies and handle the request normally. Do not ask whether to continue once the next action is clear.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let pi: ExtensionAPI;
|
|
198
|
+
|
|
199
|
+
export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
200
|
+
pi = extensionApi;
|
|
201
|
+
|
|
202
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
203
|
+
const restoredWorkflow = latestWorkflowState(ctx.sessionManager.getBranch());
|
|
204
|
+
if (!restoredWorkflow) {
|
|
205
|
+
activeWorkflow = undefined;
|
|
206
|
+
setInterviewToolActive(false);
|
|
207
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const content = loadWorkflowProcedure(restoredWorkflow);
|
|
213
|
+
activeWorkflow = restoredWorkflow;
|
|
214
|
+
setInterviewToolActive(true);
|
|
215
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
216
|
+
pi.sendMessage({
|
|
217
|
+
customType: "matt-pocock-procedure",
|
|
218
|
+
content,
|
|
219
|
+
display: false,
|
|
220
|
+
details: activeWorkflow,
|
|
221
|
+
}, { deliverAs: "nextTurn" });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
endWorkflow(ctx);
|
|
224
|
+
notifyPi(ctx.ui, `Could not restore Matt Pocock workflow: ${String(error)}`, "warning");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (typeof pi.registerMessageRenderer === "function") {
|
|
229
|
+
pi.registerMessageRenderer("matt-pocock-procedure", (message, _options, theme) => {
|
|
230
|
+
const details = (message.details ?? {}) as Partial<WorkflowState>;
|
|
231
|
+
const route = details.route ?? (activeWorkflow?.route || "workflow");
|
|
232
|
+
const phase = details.phase ?? (activeWorkflow?.phase || "active");
|
|
233
|
+
const subject = formatReadableWorkflowSubject(route, phase);
|
|
234
|
+
const prefix = theme.fg("customMessageLabel", theme.bold("[matt pocock] started ·"));
|
|
235
|
+
return new Text(`${prefix} ${safeDisplayText(subject)}`, 0, 0);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
pi.on("before_agent_start", async (event) => {
|
|
240
|
+
if (activeWorkflow) {
|
|
241
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${workflowGuidance(activeWorkflow)}` };
|
|
242
|
+
}
|
|
243
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${availableWorkflowsGuidance()}` };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
pi.registerTool({
|
|
247
|
+
name: "matt_pocock_workflow",
|
|
248
|
+
label: "Matt Pocock Workflow",
|
|
249
|
+
description: "Activate or transition a Matt Pocock engineering workflow procedure (idea-to-ship, hard-bug, triage, wayfinding, architecture). Use when a structured workflow is appropriate for the task.",
|
|
250
|
+
promptSnippet: "Activate or transition a Matt Pocock engineering workflow procedure",
|
|
251
|
+
promptGuidelines: [
|
|
252
|
+
"Use matt_pocock_workflow when the task matches a structured engineering workflow: idea-to-ship for features, hard-bug for difficult bugs, triage for raw issues, wayfinding for large ambiguous goals, or architecture for refactoring.",
|
|
253
|
+
],
|
|
254
|
+
parameters: workflowToolParameters(),
|
|
255
|
+
renderShell: "self",
|
|
256
|
+
renderCall: () => new Text("", 0, 0),
|
|
257
|
+
renderResult(result, _options, theme, context) {
|
|
258
|
+
const text = result.content.find((part) => part.type === "text")?.text ?? "";
|
|
259
|
+
if (context.isError) return new Text(theme.fg("error", formatToolErrorLine(text)), 0, 0);
|
|
260
|
+
const details = (result.details ?? {}) as WorkflowState;
|
|
261
|
+
const route = details.route ?? (context.args as { route?: string })?.route ?? "workflow";
|
|
262
|
+
const phase = details.phase ?? "active";
|
|
263
|
+
const prefix = theme.fg("customMessageLabel", theme.bold("[matt pocock] started ·"));
|
|
264
|
+
const subject = formatReadableWorkflowSubject(route, phase);
|
|
265
|
+
return new Text(`${prefix} ${safeDisplayText(subject)}`, 0, 0);
|
|
266
|
+
},
|
|
267
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
268
|
+
const route = findWorkflowRoute(params.route);
|
|
269
|
+
if (!route) {
|
|
270
|
+
throw new Error(`Unknown Matt Pocock route: ${params.route}`);
|
|
271
|
+
}
|
|
272
|
+
const requestedProcedure = params.procedure ? normalizeProcedureName(params.procedure) : route.procedure;
|
|
273
|
+
const allowedProcedures = transitionProcedures(route.route);
|
|
274
|
+
const fallsBackToRouteDefault = !allowedProcedures.includes(requestedProcedure);
|
|
275
|
+
const state: WorkflowState = fallsBackToRouteDefault
|
|
276
|
+
? { route: route.route, procedure: route.procedure, phase: route.phase }
|
|
277
|
+
: {
|
|
278
|
+
route: route.route,
|
|
279
|
+
procedure: requestedProcedure,
|
|
280
|
+
phase: params.phase ?? (params.procedure ? phaseForProcedure(requestedProcedure) : route.phase),
|
|
281
|
+
};
|
|
282
|
+
const correction = fallsBackToRouteDefault && params.procedure
|
|
283
|
+
? `Note: requested procedure "${params.procedure}" is not available for route ${route.route}; activated the route default "${route.procedure}" at phase "${route.phase}" instead. Valid procedures for ${route.route}: ${allowedProcedures.join(", ")}. Do not switch routes to work around a procedure error.\n\n`
|
|
284
|
+
: "";
|
|
285
|
+
const content = `${correction}${loadWorkflowProcedure(state)}`;
|
|
286
|
+
persistWorkflow(state);
|
|
287
|
+
setInterviewToolActive(true);
|
|
288
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text", text: content }],
|
|
292
|
+
details: state,
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
pi.registerTool({
|
|
298
|
+
name: "matt_pocock_ask",
|
|
299
|
+
label: "Matt Pocock Ask",
|
|
300
|
+
description: "Ask a structured interview or workflow decision question during the current Matt Pocock workflow using Pi UI selection.",
|
|
301
|
+
promptSnippet: "Ask a structured question during the active Matt Pocock workflow",
|
|
302
|
+
promptGuidelines: [
|
|
303
|
+
"Use matt_pocock_ask only during the active Matt Pocock workflow for interview or workflow decisions (e.g. grilling/shaping/scoping). Provide 2-4 options, put the recommended option first or mark it '(Recommended)', and specify a timeout.",
|
|
304
|
+
],
|
|
305
|
+
parameters: Type.Object({
|
|
306
|
+
question: Type.String({ description: "The interview or decision question to ask the user." }),
|
|
307
|
+
options: Type.Array(Type.String(), {
|
|
308
|
+
description: "2 to 4 suggested options. Put the recommended option first or mark it '(Recommended)'.",
|
|
309
|
+
}),
|
|
310
|
+
recommended: Type.Optional(Type.String({
|
|
311
|
+
description: "The recommended option shown to the user; it is never selected without an answer.",
|
|
312
|
+
})),
|
|
313
|
+
timeout_seconds: Type.Optional(Type.Number({
|
|
314
|
+
description: "Seconds to wait for a user response (default: 60; timeout leaves the decision pending; set <=0 for no timeout).",
|
|
315
|
+
})),
|
|
316
|
+
allow_custom: Type.Optional(Type.Boolean({
|
|
317
|
+
description: "Whether to allow the user to type a custom answer (default: true).",
|
|
318
|
+
})),
|
|
319
|
+
}),
|
|
320
|
+
renderShell: "self",
|
|
321
|
+
renderCall: () => new Text("", 0, 0),
|
|
322
|
+
renderResult(result, options, theme, context) {
|
|
323
|
+
const text = result.content.find((part) => part.type === "text")?.text ?? "";
|
|
324
|
+
if (context.isError) {
|
|
325
|
+
return new Text(theme.fg("error", formatToolErrorLine(text)), 0, 0);
|
|
326
|
+
}
|
|
327
|
+
const params = (context.args ?? {}) as { question?: string; options?: string[] };
|
|
328
|
+
const detailsObj = (result.details ?? {}) as {
|
|
329
|
+
answer?: string;
|
|
330
|
+
is_custom?: boolean;
|
|
331
|
+
pending?: boolean;
|
|
332
|
+
timed_out?: boolean;
|
|
333
|
+
source?: string;
|
|
334
|
+
};
|
|
335
|
+
const answer = detailsObj.answer ?? text;
|
|
336
|
+
const question = params.question ? safeDisplayText(params.question) : "question";
|
|
337
|
+
const subject = question;
|
|
338
|
+
const summary = [detailsObj.pending
|
|
339
|
+
? "Status: pending user decision"
|
|
340
|
+
: `Answer: ${safeDisplayText(answer || "(none)")}`];
|
|
341
|
+
const effectiveDetails = [
|
|
342
|
+
detailsObj.timed_out && detailsObj.pending ? "Reason: selection timed out" : undefined,
|
|
343
|
+
detailsObj.source === "no_ui" ? "Reason: no UI available" : undefined,
|
|
344
|
+
detailsObj.is_custom ? "Source: custom input" : undefined,
|
|
345
|
+
].filter((line): line is string => Boolean(line));
|
|
346
|
+
|
|
347
|
+
return createStaticToolLifecycleResultRenderer({
|
|
348
|
+
createSpec: () => eventToolLifecycle("matt pocock", subject, {
|
|
349
|
+
label: "ask",
|
|
350
|
+
summary,
|
|
351
|
+
details: effectiveDetails,
|
|
352
|
+
}),
|
|
353
|
+
expandHint: safeExpandHint(),
|
|
354
|
+
fit: truncateToWidth,
|
|
355
|
+
visibleWidth,
|
|
356
|
+
renderError: (line, currentTheme) => new Text(currentTheme.fg("error", line), 0, 0),
|
|
357
|
+
})(result, options, theme, context);
|
|
358
|
+
},
|
|
359
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
360
|
+
const allowCustom = params.allow_custom ?? true;
|
|
361
|
+
const timeoutSec = params.timeout_seconds !== undefined ? params.timeout_seconds : 60;
|
|
362
|
+
const timeoutMs = timeoutSec > 0 ? timeoutSec * 1000 : undefined;
|
|
363
|
+
if (!ctx.hasUI) {
|
|
364
|
+
return {
|
|
365
|
+
content: [{
|
|
366
|
+
type: "text",
|
|
367
|
+
text: "[Pending user decision] No UI is available to collect an answer. Do not proceed until the user responds.",
|
|
368
|
+
}],
|
|
369
|
+
details: {
|
|
370
|
+
pending: true,
|
|
371
|
+
source: "no_ui",
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const customOptionLabel = "Type custom answer...";
|
|
377
|
+
const choices = [...params.options];
|
|
378
|
+
if (allowCustom && !choices.includes(customOptionLabel)) {
|
|
379
|
+
choices.push(customOptionLabel);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const selected = await ctx.ui.select(
|
|
383
|
+
params.question,
|
|
384
|
+
choices,
|
|
385
|
+
timeoutMs !== undefined ? { timeout: timeoutMs } : undefined,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (selected === undefined) {
|
|
389
|
+
return {
|
|
390
|
+
content: [{
|
|
391
|
+
type: "text",
|
|
392
|
+
text: "[Pending user decision] No answer was selected. Do not proceed until the user responds.",
|
|
393
|
+
}],
|
|
394
|
+
details: {
|
|
395
|
+
pending: true,
|
|
396
|
+
timed_out: true,
|
|
397
|
+
source: "timeout",
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (selected === customOptionLabel) {
|
|
403
|
+
const finalAnswer = (await ctx.ui.input(params.question, "Enter your answer..."))?.trim();
|
|
404
|
+
if (!finalAnswer) {
|
|
405
|
+
return {
|
|
406
|
+
content: [{
|
|
407
|
+
type: "text",
|
|
408
|
+
text: "[Pending user decision] No custom answer was provided. Do not proceed until the user responds.",
|
|
409
|
+
}],
|
|
410
|
+
details: {
|
|
411
|
+
pending: true,
|
|
412
|
+
source: "custom_input_cancelled",
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
content: [{
|
|
418
|
+
type: "text",
|
|
419
|
+
text: `User answered (custom): ${finalAnswer}`,
|
|
420
|
+
}],
|
|
421
|
+
details: {
|
|
422
|
+
answer: finalAnswer,
|
|
423
|
+
is_custom: true,
|
|
424
|
+
source: "custom_input",
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
content: [{
|
|
431
|
+
type: "text",
|
|
432
|
+
text: `User selected: ${selected}`,
|
|
433
|
+
}],
|
|
434
|
+
details: {
|
|
435
|
+
answer: selected,
|
|
436
|
+
is_custom: false,
|
|
437
|
+
source: "choice_selected",
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
pi.registerCommand("matt-pocock", {
|
|
444
|
+
description: "Route and manage Matt Pocock engineering workflows",
|
|
445
|
+
handler: async (args, ctx) => {
|
|
446
|
+
const command = args.trim();
|
|
447
|
+
if (!command) {
|
|
448
|
+
if (!ctx.hasUI) {
|
|
449
|
+
notifyPi(ctx.ui, "Usage: /matt-pocock <route | status | transition | end>", "error");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
await showMenu(ctx);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (command === "status") {
|
|
456
|
+
showStatus(ctx);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (command === "transition") {
|
|
460
|
+
if (!ctx.hasUI) {
|
|
461
|
+
notifyPi(ctx.ui, "Choose a transition from the interactive /matt-pocock menu.", "error");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
await chooseTransition(ctx);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (command === "end") {
|
|
468
|
+
endWorkflow(ctx);
|
|
469
|
+
notifyPi(ctx.ui, "Matt Pocock workflow ended.", "info");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const state = parseRoute(command);
|
|
474
|
+
if (state) {
|
|
475
|
+
activateWorkflow(state, ctx);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const endedWorkflow = activeWorkflow !== undefined;
|
|
480
|
+
if (endedWorkflow) endWorkflow(ctx);
|
|
481
|
+
pi.sendUserMessage(workflowRoutingPrompt(command, endedWorkflow), { deliverAs: "followUp" });
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
const procedureDirectory = fileURLToPath(new URL("../procedures/", import.meta.url));
|
|
5
|
+
|
|
6
|
+
export function procedurePath(procedure: string): string {
|
|
7
|
+
const normalized = procedure.trim().replace(/\.md$/i, "");
|
|
8
|
+
if (!/^[a-zA-Z0-9-]+$/.test(normalized)) {
|
|
9
|
+
throw new Error(`Unknown Matt Pocock procedure: ${procedure}`);
|
|
10
|
+
}
|
|
11
|
+
const exactPath = `${procedureDirectory}${normalized}.md`;
|
|
12
|
+
if (existsSync(exactPath)) {
|
|
13
|
+
return exactPath;
|
|
14
|
+
}
|
|
15
|
+
const lowerPath = `${procedureDirectory}${normalized.toLowerCase()}.md`;
|
|
16
|
+
if (existsSync(lowerPath)) {
|
|
17
|
+
return lowerPath;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Procedure file not found for ${procedure}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadProcedure(procedure: string): string {
|
|
23
|
+
return readFileSync(procedurePath(procedure), "utf8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function procedurePrompt(route: string, procedure: string, phase: string): string {
|
|
27
|
+
return `# Matt Pocock workflow procedure
|
|
28
|
+
|
|
29
|
+
Route: ${route}
|
|
30
|
+
Phase: ${phase}
|
|
31
|
+
|
|
32
|
+
${loadProcedure(procedure)}`;
|
|
33
|
+
}
|