pi-matt-pocock 0.2.2 → 0.3.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/ARCHITECTURE.zh-CN.md +37 -19
- package/README.md +80 -13
- package/package.json +12 -7
- package/procedures/code-review.md +25 -17
- package/procedures/codebase-design.md +11 -10
- package/procedures/deslop.md +4 -2
- package/procedures/diagnosing-bugs.md +25 -25
- package/procedures/domain-modeling.md +18 -20
- package/procedures/grill-me.md +1 -1
- package/procedures/grill-with-docs.md +1 -1
- package/procedures/grilling.md +29 -5
- package/procedures/handoff.md +1 -1
- package/procedures/implement.md +7 -1
- package/procedures/improve-codebase-architecture.md +22 -22
- package/procedures/pr.md +169 -0
- package/procedures/prototype.md +7 -6
- package/procedures/research.md +2 -2
- package/procedures/retro.md +42 -0
- package/procedures/setup-matt-pocock-skills.md +26 -22
- package/procedures/tdd.md +17 -15
- package/procedures/teach.md +11 -7
- package/procedures/to-questionnaire.md +7 -5
- package/procedures/to-spec.md +2 -2
- package/procedures/to-tickets.md +12 -12
- package/procedures/triage.md +22 -22
- package/procedures/wait-what.md +1 -1
- package/procedures/wayfinder.md +24 -24
- package/procedures/wizard.md +7 -7
- package/procedures/writing-for-agents.md +14 -14
- package/{procedures → references}/ADR-FORMAT.md +11 -11
- package/{procedures → references}/AGENT-BRIEF.md +4 -4
- package/{procedures → references}/DEEPENING.md +3 -3
- package/{procedures → references}/DESIGN-IT-TWICE.md +7 -7
- package/{procedures → references}/GLOSSARY-FORMAT.md +9 -2
- package/{procedures → references}/HTML-REPORT.md +15 -15
- package/{procedures → references}/LEARNING-RECORD-FORMAT.md +10 -10
- package/{procedures → references}/LOGIC.md +5 -5
- package/{procedures → references}/MISSION-FORMAT.md +2 -2
- package/{procedures → references}/OUT-OF-SCOPE.md +11 -11
- package/{procedures → references}/RESOURCES-FORMAT.md +1 -1
- package/{procedures → references}/SKILL-MECHANICS.md +3 -3
- package/{procedures → references}/UI.md +14 -14
- package/{procedures → references}/bdd-best-practices.md +3 -3
- package/{procedures → references}/bdd.md +9 -9
- package/{procedures → references}/domain.md +12 -12
- package/references/issue-tracker-github.md +50 -0
- package/{procedures → references}/issue-tracker-gitlab.md +9 -9
- package/{procedures → references}/issue-tracker-local.md +3 -3
- package/{procedures → references}/phase-boundaries.md +8 -8
- package/src/catalog.json +49 -28
- package/src/catalog.ts +58 -2
- package/src/index.ts +521 -383
- package/src/native-dialog.ts +149 -213
- package/src/resolver.ts +30 -14
- package/src/workflow.ts +55 -16
- package/upstream/UPSTREAM.md +116 -0
- package/{scripts → upstream}/check-upstream-sync.mjs +12 -12
- package/{upstream-selection.json → upstream/upstream-selection.json} +9 -7
- package/UPSTREAM.md +0 -63
- package/procedures/CONTEXT-FORMAT.md +0 -60
- package/procedures/issue-tracker-github.md +0 -45
- /package/{procedures → assets}/hitl-loop.template.sh +0 -0
- /package/{procedures → assets}/template.sh +0 -0
- /package/{procedures → references}/GLOSSARY.md +0 -0
- /package/{procedures → references}/gherkin-guide.md +0 -0
- /package/{procedures → references}/mocking.md +0 -0
- /package/{procedures → references}/testing-anti-patterns.md +0 -0
- /package/{procedures → references}/tests.md +0 -0
- /package/{procedures → references}/triage-labels.md +0 -0
- /package/{procedures → references}/writing-great-skills.md +0 -0
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
fieldLine,
|
|
14
14
|
notifyPi,
|
|
15
15
|
safeDisplayText,
|
|
16
|
+
setOwnedTools,
|
|
16
17
|
startedToolLifecycle,
|
|
17
18
|
} from "@fradser/pi-kit";
|
|
18
19
|
import {
|
|
@@ -20,12 +21,15 @@ import {
|
|
|
20
21
|
loadConfig,
|
|
21
22
|
macosChooseFromList,
|
|
22
23
|
macosInputDialog,
|
|
24
|
+
type MacosChooseFromListResult,
|
|
23
25
|
} from "./native-dialog.ts";
|
|
24
|
-
import { Type } from "typebox";
|
|
26
|
+
import { Type, type Static } from "typebox";
|
|
25
27
|
import {
|
|
26
28
|
findProcedure,
|
|
27
29
|
modelStandaloneCapabilities,
|
|
28
30
|
standaloneCapabilities,
|
|
31
|
+
userStandaloneCapabilities,
|
|
32
|
+
validateProcedureCatalog,
|
|
29
33
|
workflowDefinitions,
|
|
30
34
|
workflowPlacement,
|
|
31
35
|
} from "./catalog.ts";
|
|
@@ -56,18 +60,26 @@ import {
|
|
|
56
60
|
type WorkflowState,
|
|
57
61
|
} from "./workflow.ts";
|
|
58
62
|
|
|
63
|
+
/** Tools activated only while a workflow is active. */
|
|
59
64
|
const ACTIVE_TOOLS = ["matt_pocock_active", "matt_pocock_ask"] as const;
|
|
65
|
+
/** Tool ids this extension registers. The gateway is unconditional: every routing
|
|
66
|
+
* prompt names it, so a session must never be left without it. */
|
|
67
|
+
const OWNED_TOOLS = ["matt_pocock_workflow", ...ACTIVE_TOOLS] as const;
|
|
60
68
|
const PROCEDURE_ENTRY = "matt-pocock-procedure";
|
|
61
69
|
|
|
62
70
|
/** Session details for one delivered procedure: a workflow state, a standalone capability, and the user's own task. */
|
|
63
71
|
type ProcedureDetails = Partial<WorkflowState> & { mode?: string; capability?: string; request?: string };
|
|
64
72
|
|
|
73
|
+
function readableProcedureTitle(id: string): string {
|
|
74
|
+
return findProcedure(id)?.label ?? id;
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
/** The block body: the user's own task when given, otherwise what they started. */
|
|
66
78
|
function procedureSubject(details: ProcedureDetails): string {
|
|
67
79
|
const request = details.request?.trim();
|
|
68
80
|
if (request) return request;
|
|
69
81
|
if (details.phase) return readablePhaseTitle(details.phase);
|
|
70
|
-
return details.capability
|
|
82
|
+
return details.capability ? readableProcedureTitle(details.capability) : "";
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
function stringEnum(values: string[], options?: Record<string, unknown>) {
|
|
@@ -77,25 +89,46 @@ function stringEnum(values: string[], options?: Record<string, unknown>) {
|
|
|
77
89
|
function workflowGatewayParameters() {
|
|
78
90
|
const routes = workflowRoutes().map((route) => route.route);
|
|
79
91
|
const capabilities = modelStandaloneCapabilities().map((capability) => capability.id);
|
|
80
|
-
|
|
92
|
+
const userCapabilities = userStandaloneCapabilities().map((capability) => capability.id);
|
|
93
|
+
const branches = Type.Union([
|
|
81
94
|
Type.Object({
|
|
82
95
|
mode: Type.Literal("workflow", { description: "Start a persisted multi-step engineering workflow." }),
|
|
83
96
|
route: stringEnum(routes, { description: "Workflow route to start." }),
|
|
84
|
-
}),
|
|
97
|
+
}, { additionalProperties: false }),
|
|
85
98
|
Type.Object({
|
|
86
99
|
mode: Type.Literal("capability", { description: "Run a curated standalone capability without persistent workflow state." }),
|
|
87
100
|
capability: stringEnum(capabilities, { description: "Standalone capability to run." }),
|
|
88
|
-
}),
|
|
101
|
+
}, { additionalProperties: false }),
|
|
102
|
+
Type.Object({
|
|
103
|
+
mode: Type.Literal("user-capability", { description: "Run a user-invoked standalone capability the user explicitly asked for." }),
|
|
104
|
+
capability: stringEnum(userCapabilities, { description: "User-invoked capability the user requested by name." }),
|
|
105
|
+
}, { additionalProperties: false }),
|
|
89
106
|
Type.Object({
|
|
90
107
|
mode: Type.Literal("reference", { description: "Load a reference disclosed by a standalone capability." }),
|
|
91
108
|
capability: Type.String({ description: "Standalone capability id from the injected capability context." }),
|
|
92
109
|
reference: Type.String({ description: "Disclosed reference id returned by the capability." }),
|
|
93
|
-
}),
|
|
110
|
+
}, { additionalProperties: false }),
|
|
94
111
|
]);
|
|
112
|
+
// Keep exact branch validation and static narrowing while exposing every
|
|
113
|
+
// operation to providers that inspect root properties before union branches.
|
|
114
|
+
// Nullable hints prevent Pi v1 from dropping optional nulls before the original
|
|
115
|
+
// branches perform their existing coercion/validation. They add no valid calls.
|
|
116
|
+
return Type.Unsafe<Static<typeof branches>>({
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
mode: stringEnum(["workflow", "capability", "user-capability", "reference"]),
|
|
120
|
+
route: Type.Union([stringEnum(routes), Type.Null()]),
|
|
121
|
+
capability: Type.Union([Type.String(), Type.Null()]),
|
|
122
|
+
reference: Type.Union([Type.String(), Type.Null()]),
|
|
123
|
+
},
|
|
124
|
+
required: ["mode"],
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
anyOf: branches.anyOf,
|
|
127
|
+
});
|
|
95
128
|
}
|
|
96
129
|
|
|
97
130
|
function activeWorkflowParameters() {
|
|
98
|
-
|
|
131
|
+
const branches = Type.Union([
|
|
99
132
|
Type.Object({
|
|
100
133
|
action: Type.Literal("transition"),
|
|
101
134
|
target: Type.String({ description: "One procedure id from the current state's allowedNext list." }),
|
|
@@ -110,6 +143,18 @@ function activeWorkflowParameters() {
|
|
|
110
143
|
reason: Type.String({ description: "Why this workflow is being cancelled." }),
|
|
111
144
|
}, { additionalProperties: false }),
|
|
112
145
|
]);
|
|
146
|
+
return Type.Unsafe<Static<typeof branches>>({
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
action: stringEnum(["transition", "load", "complete", "cancel"]),
|
|
150
|
+
target: Type.Union([Type.String(), Type.Null()]),
|
|
151
|
+
reference: Type.Union([Type.String(), Type.Null()]),
|
|
152
|
+
reason: Type.Union([Type.String(), Type.Null()]),
|
|
153
|
+
},
|
|
154
|
+
required: ["action"],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
anyOf: branches.anyOf,
|
|
157
|
+
});
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
function workflowEventSubject(action: "transition" | "complete" | "cancel", phase: string): string {
|
|
@@ -117,251 +162,357 @@ function workflowEventSubject(action: "transition" | "complete" | "cancel", phas
|
|
|
117
162
|
return action === "transition" ? title : `${title} ${action === "complete" ? "completed" : "cancelled"}`;
|
|
118
163
|
}
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
pi.setActiveTools(active ? [...withoutActive, ...ACTIVE_TOOLS] : withoutActive);
|
|
165
|
+
/** Keep the selected decision explicit in model context without echoing the offered choices. */
|
|
166
|
+
function formatDecisionAnswer(question: string, answer: string, recommendation?: string): string {
|
|
167
|
+
const selected = answer === recommendation
|
|
168
|
+
? answer.replace(/^\s*\(Recommended\)\s*|\s*\(Recommended\)\s*$/i, "")
|
|
169
|
+
: answer;
|
|
170
|
+
const quote = selected.split(/\r?\n/).map((line) => `> ${line}`).join("\n");
|
|
171
|
+
return `${question}\n\n${quote}`;
|
|
128
172
|
}
|
|
129
173
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
/**
|
|
175
|
+
* A deadline dismisses one dialog. Its own timer sets the expiry flag, never a clock
|
|
176
|
+
* reading, so a cancel that arrives before the deadline stays a cancel.
|
|
177
|
+
*/
|
|
178
|
+
async function askUntilDeadline<T>(
|
|
179
|
+
deadline: number, signal: AbortSignal | undefined, open: (dialogSignal: AbortSignal) => Promise<T>,
|
|
180
|
+
): Promise<{ value: T; expired: boolean }> {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const forward = () => controller.abort();
|
|
183
|
+
signal?.addEventListener("abort", forward, { once: true });
|
|
184
|
+
if (signal?.aborted) forward();
|
|
185
|
+
let expired = false;
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
if (!controller.signal.aborted) {
|
|
188
|
+
expired = true;
|
|
189
|
+
controller.abort();
|
|
190
|
+
}
|
|
191
|
+
}, Math.max(0, deadline - Date.now()));
|
|
192
|
+
try {
|
|
193
|
+
return { value: await open(controller.signal), expired };
|
|
194
|
+
} finally {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
signal?.removeEventListener("abort", forward);
|
|
197
|
+
}
|
|
133
198
|
}
|
|
134
199
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
/** The deadline timer, not a clock reading, dismisses the selector and reports the expiry. */
|
|
201
|
+
async function selectDecision(
|
|
202
|
+
ctx: ExtensionContext, question: string, choices: string[], deadline: number, budget: number, signal?: AbortSignal,
|
|
203
|
+
): Promise<MacosChooseFromListResult> {
|
|
204
|
+
const { value, expired } = await askUntilDeadline(deadline, signal, (dialogSignal) =>
|
|
205
|
+
ctx.ui.select(question, choices, { signal: dialogSignal, timeout: budget }));
|
|
206
|
+
if (value !== undefined) return { action: "selected", item: value };
|
|
207
|
+
return { action: expired ? "timed_out" : "cancelled" };
|
|
139
208
|
}
|
|
140
209
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
210
|
+
export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
211
|
+
const errors = validateProcedureCatalog();
|
|
212
|
+
if (errors.length > 0) throw new Error(`Invalid Matt Pocock catalog: ${errors.join("; ")}`);
|
|
213
|
+
const pi = extensionApi;
|
|
214
|
+
const standaloneReferences = new Map<string, string[]>();
|
|
215
|
+
const pendingUI = new Set<AbortController>();
|
|
216
|
+
let activeWorkflow: WorkflowState | undefined;
|
|
217
|
+
|
|
218
|
+
function interruptPendingUI(): void {
|
|
219
|
+
const controllers = [...pendingUI];
|
|
220
|
+
pendingUI.clear();
|
|
221
|
+
for (const controller of controllers) controller.abort();
|
|
222
|
+
}
|
|
153
223
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
224
|
+
async function withWorkflowSignal<T>(signal: AbortSignal | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
const abort = () => controller.abort();
|
|
227
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
228
|
+
if (signal?.aborted) abort();
|
|
229
|
+
pendingUI.add(controller);
|
|
230
|
+
try {
|
|
231
|
+
return await run(controller.signal);
|
|
232
|
+
} finally {
|
|
233
|
+
pendingUI.delete(controller);
|
|
234
|
+
signal?.removeEventListener("abort", abort);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
157
237
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
238
|
+
async function selectWorkflowOption(
|
|
239
|
+
ctx: ExtensionCommandContext,
|
|
240
|
+
title: string,
|
|
241
|
+
choices: string[],
|
|
242
|
+
apply: (choice: string) => void | Promise<void>,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
const workflow = activeWorkflow;
|
|
245
|
+
await withWorkflowSignal(undefined, async (signal) => {
|
|
246
|
+
const choice = await ctx.ui.select(title, choices, { signal });
|
|
247
|
+
if (!choice || signal.aborted || activeWorkflow !== workflow) return;
|
|
248
|
+
// Invoke the action before yielding: another await would let a replacement
|
|
249
|
+
// workflow start between this identity check and the selected action.
|
|
250
|
+
await apply(choice);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
162
253
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
254
|
+
function refreshActiveTools(active: boolean): void {
|
|
255
|
+
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
256
|
+
// The gateway is re-asserted on every write, never read back from the snapshot:
|
|
257
|
+
// /matt-pocock delivers a routing prompt that names it, so a session that lost
|
|
258
|
+
// it would instruct the model to call a tool it cannot see. See setOwnedTools.
|
|
259
|
+
setOwnedTools(pi, { owned: OWNED_TOOLS, always: ["matt_pocock_workflow"], toggled: ACTIVE_TOOLS, enabled: active });
|
|
260
|
+
}
|
|
167
261
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
262
|
+
/** Deliver a procedure as one lifecycle row: users see their own task, the model sees the whole body. */
|
|
263
|
+
function deliverProcedure(content: string, details: ProcedureDetails): void {
|
|
264
|
+
pi.sendMessage({ customType: PROCEDURE_ENTRY, content, display: true, details }, { deliverAs: "followUp", triggerTurn: true });
|
|
265
|
+
}
|
|
170
266
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* for want of room rather than for want of work.
|
|
178
|
-
*/
|
|
179
|
-
function deliveryStep(state: WorkflowState, action: string, request = "", full = false): { text: string; state: WorkflowState } {
|
|
180
|
-
const delta = resolveProcedureDelta(
|
|
181
|
-
state.procedure,
|
|
182
|
-
state.loadedReferences,
|
|
183
|
-
full ? [] : state.deliveredProcedures,
|
|
184
|
-
);
|
|
185
|
-
const next: WorkflowState = { ...state, deliveredProcedures: delta.delivered };
|
|
186
|
-
const body = delta.content ? delta.content : ALREADY_DELIVERED;
|
|
187
|
-
return {
|
|
188
|
-
text: `# Matt Pocock workflow procedure\n\nRoute: ${next.route}\nPhase: ${next.phase}\nWork item: ${next.workItemId}\n\n${body}${userRequestSection(request)}${stateContract(next, action)}`,
|
|
189
|
-
state: next,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
267
|
+
function persistWorkflow(state: WorkflowState | TerminalWorkflowState): void {
|
|
268
|
+
pi.appendEntry(WORKFLOW_STATE_ENTRY, state);
|
|
269
|
+
activeWorkflow = state.status === "active" ? state : undefined;
|
|
270
|
+
interruptPendingUI();
|
|
271
|
+
refreshActiveTools(state.status === "active");
|
|
272
|
+
}
|
|
192
273
|
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
274
|
+
function terminalState(status: "completed" | "cancelled", reason?: string): TerminalWorkflowState {
|
|
275
|
+
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
276
|
+
return {
|
|
277
|
+
version: WORKFLOW_STATE_VERSION,
|
|
278
|
+
workItemId: activeWorkflow.workItemId,
|
|
279
|
+
route: activeWorkflow.route,
|
|
280
|
+
procedure: activeWorkflow.procedure,
|
|
281
|
+
phase: activeWorkflow.phase,
|
|
282
|
+
status,
|
|
283
|
+
reason,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
199
286
|
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
if (!definition?.standalone) {
|
|
203
|
-
throw new Error(`Capability ${capability} is not a standalone Matt Pocock capability.`);
|
|
287
|
+
function availableReferences(state: WorkflowState): string[] {
|
|
288
|
+
return resolveWorkflowContext(state.procedure, state.loadedReferences).availableReferences;
|
|
204
289
|
}
|
|
205
|
-
const bundle = resolveAccessibleReference(definition.id, [], reference);
|
|
206
|
-
return `# Matt Pocock standalone reference\n\nCapability: ${definition.id}\nReference: ${bundle.root}\nPersistent workflow state: none\nAvailable references: ${bundle.availableReferences.join(", ") || "none"}\n\n${bundle.content}`;
|
|
207
|
-
}
|
|
208
290
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const step = deliveryStep(created, "started workflow", request);
|
|
220
|
-
persistWorkflow(step.state);
|
|
221
|
-
return step;
|
|
222
|
-
}
|
|
291
|
+
function stateContract(state: WorkflowState, action: string, references: string[]): string {
|
|
292
|
+
const placement = workflowPlacement(state.route, state.procedure);
|
|
293
|
+
const calls = [
|
|
294
|
+
...(placement?.allowedNext ?? []).map((target) => JSON.stringify({ action: "transition", target })),
|
|
295
|
+
...references.map((reference) => JSON.stringify({ action: "load", reference })),
|
|
296
|
+
JSON.stringify({ action: "complete" }),
|
|
297
|
+
JSON.stringify({ action: "cancel", reason: "<why>" }),
|
|
298
|
+
];
|
|
299
|
+
return `\n\n## Workflow state contract\n- Addressed: ${state.workItemId} · ${state.route} · ${state.procedure}\n- Synchronous action: ${action}\n- Allowed next: ${placement?.allowedNext.join(", ") || "none"}\n- Available references: ${references.join(", ") || "none"}\n- Pending: workflow remains active\n- Next actor: agent\n- Exact matt_pocock_active calls: ${calls.join("; ") || "none"}\n- Failed controls do not change the current procedure. Follow its returned state; do not act as if a failed transition succeeded.`;
|
|
300
|
+
}
|
|
223
301
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// The reference's own closure is deduplicated too: a reference whose body is already in
|
|
229
|
-
// the session does not need to arrive twice.
|
|
230
|
-
const delta = resolveProcedureDelta(reference, [], activeWorkflow.deliveredProcedures);
|
|
231
|
-
const state: WorkflowState = { ...activeWorkflow, loadedReferences, deliveredProcedures: delta.delivered };
|
|
232
|
-
persistWorkflow(state);
|
|
233
|
-
return {
|
|
234
|
-
state,
|
|
235
|
-
content: `# Matt Pocock workflow reference\n\nWork item: ${state.workItemId}\nReference: ${bundle.root}\n\n${delta.content || ALREADY_DELIVERED}${stateContract(state, `loaded reference ${bundle.root}`)}`,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
302
|
+
/** Delivered to the model, never rendered: the user's own words belong in the prompt. */
|
|
303
|
+
function userRequestSection(request: string): string {
|
|
304
|
+
return request ? `\n\nUser target/request:\n${request}` : "";
|
|
305
|
+
}
|
|
238
306
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
307
|
+
/** Delivered when a procedure's body is already in the session, so the prompt is never empty. */
|
|
308
|
+
const ALREADY_DELIVERED = "Its instructions were delivered earlier in this session and are unchanged. Continue against them; do not re-request them.";
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The prompt for a workflow step, plus the state that records what it delivered.
|
|
312
|
+
*
|
|
313
|
+
* Only procedures this session has not been shown are embedded. A transition re-delivers
|
|
314
|
+
* the active context every time, and a bundle carries its whole dependency closure, so
|
|
315
|
+
* re-sending that closure would spend the context the work needs and end a long task early
|
|
316
|
+
* for want of room rather than for want of work.
|
|
317
|
+
*/
|
|
318
|
+
function deliveryStep(state: WorkflowState, action: string, request = "", full = false): { text: string; state: WorkflowState } {
|
|
319
|
+
const delta = resolveProcedureDelta(
|
|
320
|
+
state.procedure,
|
|
321
|
+
state.loadedReferences,
|
|
322
|
+
full ? [] : state.deliveredProcedures,
|
|
323
|
+
);
|
|
324
|
+
const next: WorkflowState = { ...state, deliveredProcedures: delta.delivered };
|
|
325
|
+
const body = delta.content ? delta.content : ALREADY_DELIVERED;
|
|
326
|
+
return {
|
|
327
|
+
text: `# Matt Pocock workflow procedure\n\nRoute: ${next.route}\nPhase: ${next.phase}\nWork item: ${next.workItemId}\n\n${body}${userRequestSection(request)}${stateContract(next, action, delta.availableReferences)}`,
|
|
328
|
+
state: next,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
242
331
|
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
}
|
|
332
|
+
function standalonePrompt(capability: string, request = ""): string {
|
|
333
|
+
const definition = findProcedure(capability);
|
|
334
|
+
if (!definition?.standalone) throw new Error(`Unknown standalone Matt Pocock capability: ${capability}`);
|
|
335
|
+
const bundle = resolveProcedureBundle(definition.id);
|
|
336
|
+
standaloneReferences.set(definition.id, []);
|
|
337
|
+
return `# Matt Pocock standalone capability\n\nCapability: ${definition.id}\nPersistent workflow state: none\nAvailable references: ${bundle.availableReferences.join(", ") || "none"}\nStandalone decisions: ask the user in conversation; workflow-only tools do not belong to this capability.\n\n${bundle.content}${userRequestSection(request)}`;
|
|
338
|
+
}
|
|
246
339
|
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
340
|
+
function standaloneReferencePrompt(capability: string, reference: string): string {
|
|
341
|
+
const definition = findProcedure(capability);
|
|
342
|
+
if (!definition?.standalone) {
|
|
343
|
+
throw new Error(`Capability ${capability} is not a standalone Matt Pocock capability.`);
|
|
344
|
+
}
|
|
345
|
+
const previous = standaloneReferences.get(definition.id) ?? [];
|
|
346
|
+
const bundle = resolveAccessibleReference(definition.id, previous, reference);
|
|
347
|
+
const loaded = [...new Set([...previous, ...bundle.loaded])];
|
|
348
|
+
const context = resolveWorkflowContext(definition.id, loaded);
|
|
349
|
+
standaloneReferences.set(definition.id, loaded);
|
|
350
|
+
return `# Matt Pocock standalone reference\n\nCapability: ${definition.id}\nReference: ${bundle.root}\nPersistent workflow state: none\nAvailable references: ${context.availableReferences.join(", ") || "none"}\n\n${bundle.content}`;
|
|
351
|
+
}
|
|
250
352
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Begin a workflow and persist its state exactly once, carrying the delivery set
|
|
355
|
+
* this first step shows. A second entry would read as two workflows begun, and a
|
|
356
|
+
* persisted state without the set would re-deliver the whole closure on restore.
|
|
357
|
+
*/
|
|
358
|
+
function startWorkflow(route: string, request = ""): { state: WorkflowState; text: string } {
|
|
359
|
+
if (activeWorkflow) {
|
|
360
|
+
throw new Error(`Workflow ${activeWorkflow.workItemId} is already active. Complete or cancel it before starting another.`);
|
|
361
|
+
}
|
|
362
|
+
if (!findWorkflowRoute(route)) throw new Error(`Unknown Matt Pocock route: ${route}`);
|
|
363
|
+
const created = routeEntryState(route, randomUUID());
|
|
364
|
+
if (!created) throw new Error(`Workflow ${route} has no catalog entry procedure.`);
|
|
365
|
+
const step = deliveryStep(created, "started workflow", request);
|
|
366
|
+
persistWorkflow(step.state);
|
|
367
|
+
return step;
|
|
368
|
+
}
|
|
254
369
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
370
|
+
function loadActiveReference(reference: string): { state: WorkflowState; content: string } {
|
|
371
|
+
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
372
|
+
const bundle = resolveAccessibleReference(activeWorkflow.procedure, activeWorkflow.loadedReferences, reference);
|
|
373
|
+
const loadedReferences = [...new Set([...activeWorkflow.loadedReferences, ...bundle.loaded])];
|
|
374
|
+
// The reference's own closure is deduplicated too: a reference whose body is already in
|
|
375
|
+
// the session does not need to arrive twice.
|
|
376
|
+
const delta = resolveProcedureDelta(activeWorkflow.procedure, loadedReferences, activeWorkflow.deliveredProcedures);
|
|
377
|
+
const state: WorkflowState = { ...activeWorkflow, loadedReferences, deliveredProcedures: delta.delivered };
|
|
378
|
+
persistWorkflow(state);
|
|
379
|
+
return {
|
|
380
|
+
state,
|
|
381
|
+
content: `# Matt Pocock workflow reference\n\nWork item: ${state.workItemId}\nReference: ${bundle.root}\n\n${delta.content || ALREADY_DELIVERED}${stateContract(state, `loaded reference ${bundle.root}`, delta.availableReferences)}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
264
384
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const capability = capabilityFromChoice(choice);
|
|
269
|
-
if (capability) deliverProcedure(standalonePrompt(capability), { mode: "capability", capability, request: "" });
|
|
270
|
-
}
|
|
385
|
+
function routeChoices(): string[] {
|
|
386
|
+
return workflowRoutes().map((route) => `${route.label} — ${route.description}`);
|
|
387
|
+
}
|
|
271
388
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!activeWorkflow) {
|
|
275
|
-
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
276
|
-
return;
|
|
389
|
+
function routeFromChoice(choice: string): string | undefined {
|
|
390
|
+
return workflowRoutes().find((route) => choice.startsWith(route.label))?.route;
|
|
277
391
|
}
|
|
278
|
-
const state = transitionState(activeWorkflow, target);
|
|
279
|
-
resolveProcedureBundle(state.procedure);
|
|
280
|
-
const step = deliveryStep(state, `transitioned to ${state.procedure}`);
|
|
281
|
-
persistWorkflow(step.state);
|
|
282
|
-
deliverProcedure(step.text, { ...step.state, request: "" });
|
|
283
|
-
}
|
|
284
392
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
288
|
-
return;
|
|
393
|
+
function capabilityChoices(): string[] {
|
|
394
|
+
return standaloneCapabilities().map((capability) => `${capability.label} — ${capability.description ?? capability.id}`);
|
|
289
395
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return;
|
|
396
|
+
|
|
397
|
+
function capabilityFromChoice(choice: string): string | undefined {
|
|
398
|
+
return standaloneCapabilities().find((capability) => choice.startsWith(`${capability.label} —`))?.id;
|
|
294
399
|
}
|
|
295
|
-
const choice = await ctx.ui.select(`Transition ${activeWorkflow.route} from ${activeWorkflow.procedure}`, next);
|
|
296
|
-
if (!choice) return;
|
|
297
|
-
applyTransition(ctx, choice);
|
|
298
|
-
}
|
|
299
400
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
401
|
+
async function chooseRoute(ctx: ExtensionCommandContext): Promise<void> {
|
|
402
|
+
await selectWorkflowOption(ctx, "Start Matt Pocock workflow", routeChoices(), (choice) => {
|
|
403
|
+
const route = routeFromChoice(choice);
|
|
404
|
+
if (!route) return;
|
|
405
|
+
const step = startWorkflow(route);
|
|
406
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
407
|
+
deliverProcedure(step.text, { ...step.state, request: "" });
|
|
408
|
+
});
|
|
409
|
+
}
|
|
306
410
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
411
|
+
async function chooseCapability(ctx: ExtensionCommandContext): Promise<void> {
|
|
412
|
+
await selectWorkflowOption(ctx, "Run Matt Pocock capability", capabilityChoices(), (choice) => {
|
|
413
|
+
const capability = capabilityFromChoice(choice);
|
|
414
|
+
if (capability) deliverProcedure(standalonePrompt(capability), { mode: "capability", capability, request: "" });
|
|
415
|
+
});
|
|
416
|
+
}
|
|
313
417
|
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
418
|
+
function transitionWorkflow(target: string): { state: WorkflowState; text: string } {
|
|
419
|
+
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
420
|
+
const state = transitionState(activeWorkflow, target);
|
|
421
|
+
const step = deliveryStep(state, `transitioned to ${state.procedure}`);
|
|
422
|
+
persistWorkflow(step.state);
|
|
423
|
+
return step;
|
|
318
424
|
}
|
|
319
|
-
notifyPi(
|
|
320
|
-
ctx.ui,
|
|
321
|
-
`Matt Pocock workflow: ${formatReadableWorkflowSubject(activeWorkflow.route, activeWorkflow.phase)} · ${activeWorkflow.workItemId}`,
|
|
322
|
-
"info",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
425
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (choice === "View current workflow") return showStatus(ctx);
|
|
336
|
-
if (choice === "Transition current workflow") return chooseTransition(ctx);
|
|
337
|
-
if (choice === "Complete current workflow") return completeWorkflow(ctx);
|
|
338
|
-
cancelWorkflow(ctx, "Cancelled by user from the Matt Pocock menu.");
|
|
339
|
-
}
|
|
426
|
+
/** Both command and tool transitions use the same validated state change. */
|
|
427
|
+
function applyTransition(ctx: ExtensionCommandContext, target: string): void {
|
|
428
|
+
if (!activeWorkflow) {
|
|
429
|
+
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const step = transitionWorkflow(target);
|
|
433
|
+
deliverProcedure(step.text, { ...step.state, request: "" });
|
|
434
|
+
}
|
|
340
435
|
|
|
341
|
-
|
|
436
|
+
async function chooseTransition(ctx: ExtensionCommandContext): Promise<void> {
|
|
437
|
+
if (!activeWorkflow) {
|
|
438
|
+
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const { route, procedure } = activeWorkflow;
|
|
442
|
+
const next = workflowPlacement(route, procedure)?.allowedNext ?? [];
|
|
443
|
+
if (next.length === 0) {
|
|
444
|
+
notifyPi(ctx.ui, "This workflow has no next procedure. Complete or cancel it.", "info");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const choices = next.map((id) => ({ id, label: readablePhaseTitle(workflowPlacement(route, id)!.phase) }));
|
|
448
|
+
await selectWorkflowOption(ctx, `Transition ${readableRouteTitle(activeWorkflow.route)} from ${readablePhaseTitle(activeWorkflow.phase)}`, choices.map((choice) => choice.label),
|
|
449
|
+
(label) => {
|
|
450
|
+
const target = choices.find((choice) => choice.label === label)?.id;
|
|
451
|
+
if (target) applyTransition(ctx, target);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
342
454
|
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
455
|
+
function endWorkflow(ctx: ExtensionContext, status: "completed" | "cancelled", reason?: string): void {
|
|
456
|
+
const terminal = terminalState(status, reason);
|
|
457
|
+
persistWorkflow(terminal);
|
|
458
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
459
|
+
notifyPi(ctx.ui, `Matt Pocock workflow ${status}: ${terminal.workItemId}`, "info");
|
|
460
|
+
}
|
|
346
461
|
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
462
|
+
function showStatus(ctx: ExtensionContext): void {
|
|
463
|
+
if (!activeWorkflow) {
|
|
464
|
+
notifyPi(ctx.ui, "Matt Pocock workflow: inactive", "info");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
notifyPi(
|
|
468
|
+
ctx.ui,
|
|
469
|
+
`Matt Pocock workflow: ${formatReadableWorkflowSubject(activeWorkflow.route, activeWorkflow.phase)} · ${activeWorkflow.workItemId}`,
|
|
470
|
+
"info",
|
|
471
|
+
);
|
|
351
472
|
}
|
|
352
|
-
clearPiStatus(ctx.ui, "matt-pocock");
|
|
353
|
-
deliverProcedure(contextRoutingPrompt(cancelledWorkflow), { request: "" });
|
|
354
|
-
}
|
|
355
473
|
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
474
|
+
async function showMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
475
|
+
const choices = activeWorkflow
|
|
476
|
+
? [START_TASK_CHOICE, "View current workflow", "Transition current workflow", "Complete current workflow", "Cancel current workflow", "Run a standalone capability"]
|
|
477
|
+
: [START_TASK_CHOICE, "Start a workflow", "Run a standalone capability", "View current workflow"];
|
|
478
|
+
await selectWorkflowOption(ctx, "Matt Pocock", choices, (choice) => {
|
|
479
|
+
if (choice === START_TASK_CHOICE) return startFromContext(ctx);
|
|
480
|
+
if (choice === "Start a workflow") return chooseRoute(ctx);
|
|
481
|
+
if (choice === "Run a standalone capability") return chooseCapability(ctx);
|
|
482
|
+
if (choice === "View current workflow") return showStatus(ctx);
|
|
483
|
+
if (choice === "Transition current workflow") return chooseTransition(ctx);
|
|
484
|
+
if (choice === "Complete current workflow") return endWorkflow(ctx, "completed");
|
|
485
|
+
endWorkflow(ctx, "cancelled", "Cancelled by user from the Matt Pocock menu.");
|
|
486
|
+
});
|
|
487
|
+
}
|
|
359
488
|
|
|
360
|
-
|
|
361
|
-
|
|
489
|
+
const START_TASK_CHOICE = "Start a task";
|
|
490
|
+
|
|
491
|
+
function startFromContext(ctx: ExtensionCommandContext): void {
|
|
492
|
+
const cancelledWorkflow = Boolean(activeWorkflow);
|
|
493
|
+
if (activeWorkflow) {
|
|
494
|
+
persistWorkflow(terminalState("cancelled", "Superseded by a new /matt-pocock context routing request."));
|
|
495
|
+
}
|
|
496
|
+
clearPiStatus(ctx.ui, "matt-pocock");
|
|
497
|
+
deliverProcedure(workflowRoutingPrompt("", cancelledWorkflow), { request: "" });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function workflowRoutingPrompt(prompt: string, cancelledWorkflow: boolean): string {
|
|
501
|
+
const task = prompt
|
|
502
|
+
? `Route and execute this request through the relevant Matt Pocock workflow or standalone capability: ${prompt}`
|
|
503
|
+
: "Infer the user's current task from recent conversation context and route and execute it through the relevant Matt Pocock workflow or standalone capability.";
|
|
504
|
+
return `${task}\n\n${cancelledWorkflow ? "The previous active workflow has been cancelled because this is a new request. " : ""}Use matt_pocock_workflow with mode workflow for a structured multi-step engineering task, or mode capability for a matching standalone capability. If neither applies, handle the request normally. Begin immediately once the route is clear.`;
|
|
505
|
+
}
|
|
362
506
|
|
|
363
|
-
|
|
364
|
-
|
|
507
|
+
const restoreWorkflow = async (ctx: ExtensionContext): Promise<void> => {
|
|
508
|
+
interruptPendingUI();
|
|
509
|
+
standaloneReferences.clear();
|
|
510
|
+
const branch = ctx.sessionManager.getBranch();
|
|
511
|
+
const record = latestWorkflowRecord(branch);
|
|
512
|
+
const newest = [...branch].reverse().find((entry) => entry.type === "custom" && entry.customType === WORKFLOW_STATE_ENTRY);
|
|
513
|
+
if (newest && !record) {
|
|
514
|
+
notifyPi(ctx.ui, "Could not restore Matt Pocock workflow: latest record is malformed or incompatible.", "warning");
|
|
515
|
+
}
|
|
365
516
|
if (!record || record.status !== "active") {
|
|
366
517
|
activeWorkflow = undefined;
|
|
367
518
|
refreshActiveTools(false);
|
|
@@ -370,7 +521,8 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
370
521
|
}
|
|
371
522
|
|
|
372
523
|
try {
|
|
373
|
-
|
|
524
|
+
const placement = workflowPlacement(record.route, record.procedure);
|
|
525
|
+
if (!placement || placement.phase !== record.phase) {
|
|
374
526
|
throw new Error(`Procedure ${record.procedure} is not part of workflow ${record.route}.`);
|
|
375
527
|
}
|
|
376
528
|
// A restore is a context boundary: this session may have been compacted or restarted,
|
|
@@ -378,8 +530,8 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
378
530
|
// tracking only suppresses repeats inside one continuous session.
|
|
379
531
|
const restored = withDeliveryDefaults(record);
|
|
380
532
|
const step = deliveryStep(restored, "restored workflow", "", true);
|
|
381
|
-
//
|
|
382
|
-
//
|
|
533
|
+
// Restore the in-memory delivery set without adding workflow state entries.
|
|
534
|
+
// The silent custom message must enter context before automatic compaction retries.
|
|
383
535
|
activeWorkflow = step.state;
|
|
384
536
|
refreshActiveTools(true);
|
|
385
537
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
@@ -388,7 +540,7 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
388
540
|
content: step.text,
|
|
389
541
|
display: false,
|
|
390
542
|
details: step.state,
|
|
391
|
-
}, {
|
|
543
|
+
}, { triggerTurn: false });
|
|
392
544
|
} catch (error) {
|
|
393
545
|
activeWorkflow = record;
|
|
394
546
|
const cancelled = terminalState("cancelled", `Restore validation failed: ${String(error)}`);
|
|
@@ -401,7 +553,10 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
401
553
|
"warning",
|
|
402
554
|
);
|
|
403
555
|
}
|
|
404
|
-
}
|
|
556
|
+
};
|
|
557
|
+
pi.on("session_start", async (_event, ctx) => restoreWorkflow(ctx));
|
|
558
|
+
pi.on("session_tree", async (_event, ctx) => restoreWorkflow(ctx));
|
|
559
|
+
pi.on("session_compact", async (_event, ctx) => restoreWorkflow(ctx));
|
|
405
560
|
|
|
406
561
|
if (typeof pi.registerMessageRenderer === "function") {
|
|
407
562
|
pi.registerMessageRenderer(PROCEDURE_ENTRY, (message, options, theme) => {
|
|
@@ -416,21 +571,28 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
416
571
|
});
|
|
417
572
|
}
|
|
418
573
|
|
|
419
|
-
pi.on("before_agent_start", async (event) =>
|
|
420
|
-
|
|
574
|
+
pi.on("before_agent_start", async (event) => {
|
|
575
|
+
const options = event.systemPromptOptions;
|
|
576
|
+
if (!("sections" in options) || !options.sections || typeof options.sections !== "object" || Array.isArray(options.sections)) {
|
|
577
|
+
throw new Error("This extension requires Pi structured prompt sections.");
|
|
578
|
+
}
|
|
579
|
+
const sections = options.sections;
|
|
580
|
+
Reflect.deleteProperty(sections, "matt_pocock_workflow");
|
|
581
|
+
Object.assign(sections, { matt_pocock_workflow: activeWorkflow
|
|
421
582
|
? workflowGuidance(activeWorkflow, availableReferences(activeWorkflow))
|
|
422
|
-
: availableWorkflowsGuidance()}
|
|
423
|
-
})
|
|
583
|
+
: availableWorkflowsGuidance() });
|
|
584
|
+
});
|
|
424
585
|
|
|
425
586
|
pi.registerTool({
|
|
426
587
|
name: "matt_pocock_workflow",
|
|
427
588
|
label: "Matt Pocock Gateway",
|
|
428
|
-
description: "Start a Matt Pocock engineering workflow, run a curated standalone capability, or load a reference disclosed by that capability. Workflows cover structured multi-step engineering; standalone capabilities cover focused methods and are listed in the injected capability catalog.",
|
|
589
|
+
description: "Start a Matt Pocock engineering workflow, run a curated standalone capability, run a user-invoked capability the user explicitly asked for, or load a reference disclosed by that capability. Workflows cover structured multi-step engineering; standalone capabilities cover focused methods and are listed in the injected capability catalog.",
|
|
429
590
|
promptSnippet: "Start a Matt Pocock workflow or run a standalone capability",
|
|
430
591
|
promptGuidelines: [
|
|
431
|
-
"mode workflow: structured multi-step engineering
|
|
432
|
-
"mode capability: a
|
|
433
|
-
"mode
|
|
592
|
+
'mode workflow: {"mode": "workflow", "route": "<route id>"} for structured multi-step engineering (idea-to-ship for features, hard-bug for non-trivial bugs, architecture for deepening/refactoring, wayfinding for ambiguous maps, triage for issues).',
|
|
593
|
+
'mode capability: {"mode": "capability", "capability": "<capability id>"} for a focused method. A capability id is not a workflow route; use mode capability even when its subject is engineering.',
|
|
594
|
+
'mode user-capability: {"mode": "user-capability", "capability": "<capability id>"} only when the user explicitly asked for that named capability, such as "grill me". Never select one on your own initiative.',
|
|
595
|
+
'mode reference: {"mode": "reference", "capability": "<capability id>", "reference": "<disclosed reference id>"}.',
|
|
434
596
|
"Start with matt_pocock_workflow; the started workflow carries its procedure and next-step guidance.",
|
|
435
597
|
],
|
|
436
598
|
parameters: workflowGatewayParameters(),
|
|
@@ -441,18 +603,17 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
441
603
|
const subject = details.mode === "workflow"
|
|
442
604
|
? readablePhaseTitle(details.phase ?? "active")
|
|
443
605
|
: details.mode === "reference"
|
|
444
|
-
? `${details.capability ?? "capability"} · ${details.reference ?? "reference"}`
|
|
445
|
-
: details.capability ?? "standalone capability";
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
606
|
+
? `${readableProcedureTitle(details.capability ?? "capability")} · ${readableProcedureTitle(details.reference ?? "reference")}`
|
|
607
|
+
: readableProcedureTitle(details.capability ?? "standalone capability");
|
|
608
|
+
// A start row is the whole message: the phase title alone, with nothing held
|
|
609
|
+
// back for Ctrl+O. Entry phase titles are unique per route, so the row needs no
|
|
610
|
+
// route field. The route id stays in the result details for model context.
|
|
611
|
+
return mattPocockRows.result(() => eventToolLifecycle("matt pocock", subject, { label: "started" }))(result, options, theme, context);
|
|
449
612
|
},
|
|
450
|
-
async execute(_toolCallId, params,
|
|
613
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
614
|
+
signal?.throwIfAborted();
|
|
451
615
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
452
616
|
if (params.mode === "workflow") {
|
|
453
|
-
if (activeWorkflow) {
|
|
454
|
-
throw new Error(`Workflow ${activeWorkflow.workItemId} is already active. Complete or cancel it before starting another.`);
|
|
455
|
-
}
|
|
456
617
|
const step = startWorkflow(params.route);
|
|
457
618
|
return {
|
|
458
619
|
content: [{ type: "text", text: step.text }],
|
|
@@ -466,6 +627,16 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
466
627
|
details: { mode: "reference", capability: params.capability, reference: params.reference },
|
|
467
628
|
};
|
|
468
629
|
}
|
|
630
|
+
if (params.mode === "user-capability") {
|
|
631
|
+
const definition = findProcedure(params.capability);
|
|
632
|
+
if (!definition?.standalone || definition.invocation !== "user") {
|
|
633
|
+
throw new Error(`Capability ${params.capability} is not a user-invocation capability.`);
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
content: [{ type: "text", text: standalonePrompt(definition.id) }],
|
|
637
|
+
details: { mode: "user-capability", capability: definition.id },
|
|
638
|
+
};
|
|
639
|
+
}
|
|
469
640
|
const definition = findProcedure(params.capability);
|
|
470
641
|
if (!definition?.standalone || definition.invocation !== "model") {
|
|
471
642
|
throw new Error(`Capability ${params.capability} is not available to the model gateway.`);
|
|
@@ -480,7 +651,7 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
480
651
|
pi.registerTool({
|
|
481
652
|
name: "matt_pocock_active",
|
|
482
653
|
label: "Matt Pocock Active Workflow",
|
|
483
|
-
description:
|
|
654
|
+
description: 'Operate on the active workflow using exactly one shape: {"action":"transition","target":"<allowedNext id>"}, {"action":"load","reference":"<availableReferences id>"}, {"action":"complete"}, or {"action":"cancel","reason":"<why>"}. Transition requires target; a reference uses action load. Copy a call from the latest workflow state contract.',
|
|
484
655
|
promptSnippet: "Transition, load a reference, complete, or cancel the active Matt Pocock workflow",
|
|
485
656
|
promptGuidelines: [
|
|
486
657
|
'action transition: {"action": "transition", "target": "<one allowedNext id>"} — target is required.',
|
|
@@ -492,35 +663,37 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
492
663
|
renderShell: "self",
|
|
493
664
|
renderCall: () => mattPocockRows.emptyCall(),
|
|
494
665
|
renderResult(result, options, theme, context) {
|
|
495
|
-
const details = (result.details ?? {}) as { action?: string; subject?: string; state?: { phase?: string; reason?: string } };
|
|
666
|
+
const details = (result.details ?? {}) as { action?: string; subject?: string; reference?: string; state?: { phase?: string; reason?: string } };
|
|
496
667
|
// Saved results may still have route-based subjects; render from that event's phase, not active state.
|
|
497
668
|
const subject = details.state?.phase && (details.action === "transition" || details.action === "complete" || details.action === "cancel")
|
|
498
669
|
? workflowEventSubject(details.action, details.state.phase)
|
|
499
|
-
: details.
|
|
670
|
+
: details.action === "load"
|
|
671
|
+
? `loaded ${readableProcedureTitle(details.reference ?? details.subject?.replace(/^loaded /, "") ?? "reference")}`
|
|
672
|
+
: details.subject ?? details.action ?? "workflow updated";
|
|
500
673
|
const reason = details.action === "cancel" ? safeDisplayText(details.state?.reason ?? "").trim() : undefined;
|
|
674
|
+
// Every Matt Pocock row shows its facts by default, so a cancellation reason is
|
|
675
|
+
// an inline body line, never a body that waits behind Ctrl+O.
|
|
501
676
|
return mattPocockRows.result(() => eventToolLifecycle("matt pocock", subject, {
|
|
502
677
|
label: "event",
|
|
503
|
-
|
|
678
|
+
summary: reason ? [fieldLine("reason", reason)] : [],
|
|
504
679
|
}))(result, options, theme, context);
|
|
505
680
|
},
|
|
506
|
-
async execute(_toolCallId, params,
|
|
681
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
682
|
+
signal?.throwIfAborted();
|
|
507
683
|
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
508
684
|
if (params.action === "transition") {
|
|
509
|
-
const
|
|
510
|
-
resolveProcedureBundle(state.procedure);
|
|
511
|
-
const step = deliveryStep(state, `transitioned to ${state.procedure}`);
|
|
512
|
-
persistWorkflow(step.state);
|
|
685
|
+
const step = transitionWorkflow(params.target);
|
|
513
686
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
514
687
|
return {
|
|
515
688
|
content: [{ type: "text", text: step.text }],
|
|
516
|
-
details: { action: "transition", subject: workflowEventSubject("transition", state.phase), state: step.state },
|
|
689
|
+
details: { action: "transition", subject: workflowEventSubject("transition", step.state.phase), state: step.state },
|
|
517
690
|
};
|
|
518
691
|
}
|
|
519
692
|
if (params.action === "load") {
|
|
520
693
|
const loaded = loadActiveReference(params.reference);
|
|
521
694
|
return {
|
|
522
695
|
content: [{ type: "text", text: loaded.content }],
|
|
523
|
-
details: { action: "load", subject: `loaded ${params.reference}`, state: loaded.state },
|
|
696
|
+
details: { action: "load", reference: params.reference, subject: `loaded ${readableProcedureTitle(params.reference)}`, state: loaded.state },
|
|
524
697
|
};
|
|
525
698
|
}
|
|
526
699
|
const terminal = params.action === "complete"
|
|
@@ -539,39 +712,23 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
539
712
|
},
|
|
540
713
|
});
|
|
541
714
|
|
|
542
|
-
function isChineseText(text: string): boolean {
|
|
543
|
-
return /[\u4e00-\u9fa5]/.test(text);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function deriveDialogTitle(explicitTitle?: string): string {
|
|
547
|
-
if (explicitTitle && explicitTitle.trim()) return explicitTitle.trim();
|
|
548
|
-
if (activeWorkflow) {
|
|
549
|
-
return formatReadableWorkflowSubject(activeWorkflow.route, activeWorkflow.phase);
|
|
550
|
-
}
|
|
551
|
-
return "Decision";
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function deriveDialogButtonNames(question: string): { ok: string; cancel: string } {
|
|
555
|
-
const isZh = isChineseText(question);
|
|
556
|
-
return {
|
|
557
|
-
ok: isZh ? "确认" : "Select",
|
|
558
|
-
cancel: isZh ? "取消" : "Cancel",
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
715
|
pi.registerTool({
|
|
563
716
|
name: "matt_pocock_ask",
|
|
564
717
|
label: "Matt Pocock Ask",
|
|
565
718
|
description: "Ask a structured workflow decision question via Pi UI selection.",
|
|
566
719
|
promptSnippet: "Ask a structured question during the active Matt Pocock workflow",
|
|
720
|
+
promptGuidelines: [
|
|
721
|
+
"Every call passes recommended as an exact copy of one offered option; the deadline adopts it and no question waits indefinitely. For an approval-required external or destructive action, recommend the option that does not act.",
|
|
722
|
+
"Escape, cancellation, abort, and no UI leave a decision pending; do not interpret them as consent.",
|
|
723
|
+
],
|
|
567
724
|
parameters: Type.Object({
|
|
568
725
|
title: Type.Optional(Type.String({ description: "Short title summarizing the decision context; defaults to workflow route and phase." })),
|
|
569
726
|
question: Type.String({ description: "The interview or decision question to ask the user." }),
|
|
570
|
-
options: Type.Array(Type.String(), { description: "2 to 4 suggested options, recommended option first." }),
|
|
571
|
-
recommended: Type.
|
|
572
|
-
timeout_seconds: Type.Optional(Type.Number({ description: "Seconds to wait
|
|
727
|
+
options: Type.Array(Type.String({ minLength: 1 }), { minItems: 2, maxItems: 4, uniqueItems: true, description: "2 to 4 suggested options, recommended option first." }),
|
|
728
|
+
recommended: Type.String({ description: "Required. An exact copy of one options entry, including annotations such as (Recommended) and whitespace. The selection deadline automatically adopts this option." }),
|
|
729
|
+
timeout_seconds: Type.Optional(Type.Number({ exclusiveMinimum: 0, maximum: 3600, description: "Seconds to wait before adopting the recommended option; default 60." })),
|
|
573
730
|
allow_custom: Type.Optional(Type.Boolean({ description: "Allow a custom typed answer; default true." })),
|
|
574
|
-
}),
|
|
731
|
+
}, { additionalProperties: false }),
|
|
575
732
|
renderShell: "self",
|
|
576
733
|
renderCall: () => mattPocockRows.emptyCall(),
|
|
577
734
|
renderResult(result, options, theme, context) {
|
|
@@ -585,140 +742,118 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
585
742
|
: answerLines.map((line, index) => index === 0 ? `Answer: ${line}` : ` ${line}`);
|
|
586
743
|
const metadata = [
|
|
587
744
|
details.timed_out && details.source === "timeout_recommended" ? fieldLine("reason", "selection timed out (used recommendation)") : undefined,
|
|
745
|
+
// A deadline always adopts a recommendation now. This line only renders a
|
|
746
|
+
// pending timeout persisted by an older session, so history keeps its reason.
|
|
588
747
|
details.timed_out && details.pending ? fieldLine("reason", "selection timed out") : undefined,
|
|
589
748
|
details.source === "no_ui" ? fieldLine("reason", "no UI available") : undefined,
|
|
590
749
|
details.is_custom ? fieldLine("source", "custom input") : undefined,
|
|
591
750
|
].filter((line): line is string => Boolean(line));
|
|
592
751
|
return mattPocockRows.result(() => eventToolLifecycle("matt pocock", safeDisplayText(params.question ?? "question"), {
|
|
593
752
|
label: "ask",
|
|
594
|
-
|
|
595
|
-
|
|
753
|
+
// Decision metadata joins the answer body: one collapsed row carries the
|
|
754
|
+
// question, the answer, and how that answer was obtained.
|
|
755
|
+
summary: [...(summary.length > 0 ? summary : ["Answer: (none)"]), ...metadata],
|
|
596
756
|
}))(result, options, theme, context);
|
|
597
757
|
},
|
|
598
|
-
async execute(_toolCallId, params,
|
|
758
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
599
759
|
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (nativeChoice.action === "cancelled" || !nativeChoice.item) {
|
|
760
|
+
const askedWorkflow = activeWorkflow;
|
|
761
|
+
return withWorkflowSignal(signal, async (decisionSignal) => {
|
|
762
|
+
const interrupted = () => decisionSignal.aborted || activeWorkflow !== askedWorkflow;
|
|
763
|
+
const pendingAbort = () => ({
|
|
764
|
+
content: [{ type: "text" as const, text: "[Pending user decision] Question interrupted. Do not proceed until the user responds." }],
|
|
765
|
+
details: { pending: true, source: "aborted" },
|
|
766
|
+
});
|
|
767
|
+
if (interrupted()) return pendingAbort();
|
|
768
|
+
const offered = typeof params.recommended === "string" ? params.recommended : "";
|
|
769
|
+
const recommended = params.options.includes(offered) ? offered : offered.trim();
|
|
770
|
+
if (!recommended || !params.options.includes(recommended)) {
|
|
771
|
+
throw new Error("A recommended answer is required and must match an offered option.");
|
|
772
|
+
}
|
|
773
|
+
if (!ctx.hasUI) {
|
|
774
|
+
return {
|
|
775
|
+
content: [{ type: "text", text: "[Pending user decision] No UI is available. Do not proceed until the user responds." }],
|
|
776
|
+
details: { pending: true, source: "no_ui" },
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
// One deadline governs the whole ask: it dismisses the selector and the custom
|
|
780
|
+
// answer input, then adopts the recommendation so no question waits indefinitely.
|
|
781
|
+
// The first dialog receives the requested budget, and later phases the remainder.
|
|
782
|
+
const timeoutSeconds = params.timeout_seconds ?? 60;
|
|
783
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
784
|
+
const deadline = Date.now() + timeoutMs;
|
|
785
|
+
const remainingMs = () => Math.max(1, deadline - Date.now());
|
|
786
|
+
const adoptRecommendation = () => ({
|
|
787
|
+
content: [{ type: "text" as const, text: formatDecisionAnswer(params.question, recommended, recommended) }],
|
|
788
|
+
details: { answer: recommended, is_custom: false, timed_out: true, pending: false, source: "timeout_recommended" },
|
|
789
|
+
});
|
|
790
|
+
let customOption = "Type custom answer...";
|
|
791
|
+
while (params.options.includes(customOption)) customOption += " (custom)";
|
|
792
|
+
const choices = [...params.options];
|
|
793
|
+
const allowCustom = params.allow_custom ?? true;
|
|
794
|
+
if (allowCustom) choices.push(customOption);
|
|
795
|
+
const title = params.title?.trim() || formatReadableWorkflowSubject(askedWorkflow.route, askedWorkflow.phase);
|
|
796
|
+
const isChinese = /[\u4e00-\u9fa5]/.test(params.question);
|
|
797
|
+
const buttons = { ok: isChinese ? "确认" : "Select", cancel: isChinese ? "取消" : "Cancel" };
|
|
798
|
+
|
|
799
|
+
// Each UI adapter returns the same selection outcome; decision handling is shared.
|
|
800
|
+
async function ask(native: boolean) {
|
|
801
|
+
if (interrupted()) return pendingAbort();
|
|
802
|
+
const choice = native
|
|
803
|
+
? await macosChooseFromList({
|
|
804
|
+
title, prompt: params.question, items: choices, defaultItem: recommended,
|
|
805
|
+
okButtonName: buttons.ok, cancelButtonName: buttons.cancel,
|
|
806
|
+
timeoutSeconds, signal: decisionSignal,
|
|
807
|
+
})
|
|
808
|
+
: await selectDecision(ctx, params.question, choices, deadline, timeoutMs, decisionSignal);
|
|
809
|
+
if (interrupted()) return pendingAbort();
|
|
810
|
+
if (choice.action === "timed_out") return adoptRecommendation();
|
|
811
|
+
if (choice.action !== "selected" || !choice.item) {
|
|
654
812
|
return {
|
|
655
|
-
content: [{ type: "text", text: "[Pending user decision] No answer was selected. Do not proceed until the user responds." }],
|
|
813
|
+
content: [{ type: "text" as const, text: "[Pending user decision] No answer was selected. Do not proceed until the user responds." }],
|
|
656
814
|
details: { pending: true, timed_out: false, source: "cancelled" },
|
|
657
815
|
};
|
|
658
816
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
817
|
+
const isCustom = allowCustom && choice.item === customOption;
|
|
818
|
+
let answer = choice.item;
|
|
819
|
+
if (isCustom) {
|
|
820
|
+
if (native) {
|
|
821
|
+
const input = await macosInputDialog({
|
|
822
|
+
title, prompt: params.question, defaultAnswer: "", buttons: [buttons.cancel, buttons.ok],
|
|
823
|
+
defaultButton: buttons.ok, cancelButton: buttons.cancel,
|
|
824
|
+
timeoutSeconds: remainingMs() / 1000, signal: decisionSignal,
|
|
825
|
+
});
|
|
826
|
+
if (interrupted()) return pendingAbort();
|
|
827
|
+
if (input.action === "timed_out") return adoptRecommendation();
|
|
828
|
+
answer = input.action === "confirmed" ? input.text?.trim() ?? "" : "";
|
|
829
|
+
} else {
|
|
830
|
+
const remaining = remainingMs();
|
|
831
|
+
const { value, expired } = await askUntilDeadline(deadline, decisionSignal, (dialogSignal) =>
|
|
832
|
+
ctx.ui.input(params.question, "Enter your answer...", { signal: dialogSignal, timeout: remaining }));
|
|
833
|
+
if (interrupted()) return pendingAbort();
|
|
834
|
+
if (expired) return adoptRecommendation();
|
|
835
|
+
answer = value?.trim() ?? "";
|
|
836
|
+
}
|
|
670
837
|
if (!answer) {
|
|
671
838
|
return {
|
|
672
|
-
content: [{ type: "text", text: "[Pending user decision] No custom answer was provided. Do not proceed until the user responds." }],
|
|
839
|
+
content: [{ type: "text" as const, text: "[Pending user decision] No custom answer was provided. Do not proceed until the user responds." }],
|
|
673
840
|
details: { pending: true, source: "custom_input_cancelled" },
|
|
674
841
|
};
|
|
675
842
|
}
|
|
676
|
-
return {
|
|
677
|
-
content: [{ type: "text", text: `User answered (custom): ${answer}` }],
|
|
678
|
-
details: { answer, is_custom: true, source: "custom_input" },
|
|
679
|
-
};
|
|
680
843
|
}
|
|
681
|
-
|
|
682
844
|
return {
|
|
683
|
-
content: [{ type: "text", text:
|
|
684
|
-
details: { answer
|
|
845
|
+
content: [{ type: "text" as const, text: formatDecisionAnswer(params.question, answer, isCustom ? undefined : recommended) }],
|
|
846
|
+
details: { answer, is_custom: isCustom, source: isCustom ? "custom_input" : "choice_selected" },
|
|
685
847
|
};
|
|
686
|
-
} catch {
|
|
687
|
-
// Graceful fallback to TUI on any native dialog error
|
|
688
848
|
}
|
|
689
|
-
}
|
|
690
849
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
content: [{ type: "text", text: `User selected (timeout default): ${answer}` }],
|
|
697
|
-
details: { answer, is_custom: false, timed_out: true, pending: false, source: "timeout_recommended" },
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
return {
|
|
701
|
-
content: [{ type: "text", text: "[Pending user decision] No answer was selected. Do not proceed until the user responds." }],
|
|
702
|
-
details: { pending: true, timed_out: false, source: "cancelled" },
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
if (selected === customOption) {
|
|
706
|
-
const answer = (await ctx.ui.input(params.question, "Enter your answer..."))?.trim();
|
|
707
|
-
if (!answer) {
|
|
708
|
-
return {
|
|
709
|
-
content: [{ type: "text", text: "[Pending user decision] No custom answer was provided. Do not proceed until the user responds." }],
|
|
710
|
-
details: { pending: true, source: "custom_input_cancelled" },
|
|
711
|
-
};
|
|
850
|
+
if (loadConfig().useNativeDialog && isNativeDialogSupported()) {
|
|
851
|
+
try { return await ask(true); } catch {
|
|
852
|
+
// Process and parsing failures fall back to Pi UI; user cancellation does not.
|
|
853
|
+
}
|
|
712
854
|
}
|
|
713
|
-
return
|
|
714
|
-
|
|
715
|
-
details: { answer, is_custom: true, source: "custom_input" },
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
return {
|
|
719
|
-
content: [{ type: "text", text: `User selected: ${selected}` }],
|
|
720
|
-
details: { answer: selected, is_custom: false, source: "choice_selected" },
|
|
721
|
-
};
|
|
855
|
+
return await ask(false);
|
|
856
|
+
});
|
|
722
857
|
},
|
|
723
858
|
});
|
|
724
859
|
|
|
@@ -738,6 +873,10 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
738
873
|
const token = command.match(/^\S+/)?.[0] ?? command;
|
|
739
874
|
const task = command.slice(token.length).trim();
|
|
740
875
|
if (token === "status" && !task) return showStatus(ctx);
|
|
876
|
+
if ((token === "status" || token === "complete") && task) {
|
|
877
|
+
notifyPi(ctx.ui, `Usage: /matt-pocock ${token}`, "error");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
741
880
|
if (token === "transition" && !task) {
|
|
742
881
|
if (!ctx.hasUI) {
|
|
743
882
|
notifyPi(ctx.ui, "Name a target: /matt-pocock transition <target>, or use the interactive menu.", "error");
|
|
@@ -746,13 +885,12 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
746
885
|
return chooseTransition(ctx);
|
|
747
886
|
}
|
|
748
887
|
if (token === "transition") return applyTransition(ctx, task);
|
|
749
|
-
if (token === "complete" && !task) return
|
|
888
|
+
if (token === "complete" && !task) return endWorkflow(ctx, "completed");
|
|
750
889
|
if (token === "cancel" || token === "end") {
|
|
751
|
-
return
|
|
890
|
+
return endWorkflow(ctx, "cancelled", task || `Cancelled by user with /matt-pocock ${token}.`);
|
|
752
891
|
}
|
|
753
892
|
|
|
754
893
|
if (findWorkflowRoute(token)) {
|
|
755
|
-
if (activeWorkflow) throw new Error(`Workflow ${activeWorkflow.workItemId} is already active.`);
|
|
756
894
|
const step = startWorkflow(token, task);
|
|
757
895
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
758
896
|
deliverProcedure(step.text, { ...step.state, request: task });
|