pi-matt-pocock 0.2.1 → 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 +539 -363
- package/src/native-dialog.ts +149 -213
- package/src/resolver.ts +57 -11
- package/src/workflow.ts +76 -18
- 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,16 +21,19 @@ 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";
|
|
32
|
-
import { resolveAccessibleReference, resolveProcedureBundle, resolveWorkflowContext } from "./resolver.ts";
|
|
36
|
+
import { resolveAccessibleReference, resolveProcedureBundle, resolveProcedureDelta, resolveWorkflowContext } from "./resolver.ts";
|
|
33
37
|
/** Geometry bound once: every Matt Pocock row shares hint and wrapping. */
|
|
34
38
|
const mattPocockRows = bindLifecycleRenderers({
|
|
35
39
|
fit: truncateToWidth,
|
|
@@ -49,24 +53,33 @@ import {
|
|
|
49
53
|
transitionState,
|
|
50
54
|
workflowGuidance,
|
|
51
55
|
workflowRoutes,
|
|
56
|
+
withDeliveryDefaults,
|
|
52
57
|
WORKFLOW_STATE_ENTRY,
|
|
53
58
|
WORKFLOW_STATE_VERSION,
|
|
54
59
|
type TerminalWorkflowState,
|
|
55
60
|
type WorkflowState,
|
|
56
61
|
} from "./workflow.ts";
|
|
57
62
|
|
|
63
|
+
/** Tools activated only while a workflow is active. */
|
|
58
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;
|
|
59
68
|
const PROCEDURE_ENTRY = "matt-pocock-procedure";
|
|
60
69
|
|
|
61
70
|
/** Session details for one delivered procedure: a workflow state, a standalone capability, and the user's own task. */
|
|
62
71
|
type ProcedureDetails = Partial<WorkflowState> & { mode?: string; capability?: string; request?: string };
|
|
63
72
|
|
|
73
|
+
function readableProcedureTitle(id: string): string {
|
|
74
|
+
return findProcedure(id)?.label ?? id;
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/** The block body: the user's own task when given, otherwise what they started. */
|
|
65
78
|
function procedureSubject(details: ProcedureDetails): string {
|
|
66
79
|
const request = details.request?.trim();
|
|
67
80
|
if (request) return request;
|
|
68
81
|
if (details.phase) return readablePhaseTitle(details.phase);
|
|
69
|
-
return details.capability
|
|
82
|
+
return details.capability ? readableProcedureTitle(details.capability) : "";
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
function stringEnum(values: string[], options?: Record<string, unknown>) {
|
|
@@ -76,25 +89,46 @@ function stringEnum(values: string[], options?: Record<string, unknown>) {
|
|
|
76
89
|
function workflowGatewayParameters() {
|
|
77
90
|
const routes = workflowRoutes().map((route) => route.route);
|
|
78
91
|
const capabilities = modelStandaloneCapabilities().map((capability) => capability.id);
|
|
79
|
-
|
|
92
|
+
const userCapabilities = userStandaloneCapabilities().map((capability) => capability.id);
|
|
93
|
+
const branches = Type.Union([
|
|
80
94
|
Type.Object({
|
|
81
95
|
mode: Type.Literal("workflow", { description: "Start a persisted multi-step engineering workflow." }),
|
|
82
96
|
route: stringEnum(routes, { description: "Workflow route to start." }),
|
|
83
|
-
}),
|
|
97
|
+
}, { additionalProperties: false }),
|
|
84
98
|
Type.Object({
|
|
85
99
|
mode: Type.Literal("capability", { description: "Run a curated standalone capability without persistent workflow state." }),
|
|
86
100
|
capability: stringEnum(capabilities, { description: "Standalone capability to run." }),
|
|
87
|
-
}),
|
|
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 }),
|
|
88
106
|
Type.Object({
|
|
89
107
|
mode: Type.Literal("reference", { description: "Load a reference disclosed by a standalone capability." }),
|
|
90
108
|
capability: Type.String({ description: "Standalone capability id from the injected capability context." }),
|
|
91
109
|
reference: Type.String({ description: "Disclosed reference id returned by the capability." }),
|
|
92
|
-
}),
|
|
110
|
+
}, { additionalProperties: false }),
|
|
93
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
|
+
});
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
function activeWorkflowParameters() {
|
|
97
|
-
|
|
131
|
+
const branches = Type.Union([
|
|
98
132
|
Type.Object({
|
|
99
133
|
action: Type.Literal("transition"),
|
|
100
134
|
target: Type.String({ description: "One procedure id from the current state's allowedNext list." }),
|
|
@@ -109,6 +143,18 @@ function activeWorkflowParameters() {
|
|
|
109
143
|
reason: Type.String({ description: "Why this workflow is being cancelled." }),
|
|
110
144
|
}, { additionalProperties: false }),
|
|
111
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
|
+
});
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
function workflowEventSubject(action: "transition" | "complete" | "cancel", phase: string): string {
|
|
@@ -116,221 +162,357 @@ function workflowEventSubject(action: "transition" | "complete" | "cancel", phas
|
|
|
116
162
|
return action === "transition" ? title : `${title} ${action === "complete" ? "completed" : "cancelled"}`;
|
|
117
163
|
}
|
|
118
164
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
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}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
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
|
+
}
|
|
127
198
|
}
|
|
128
199
|
|
|
129
|
-
/**
|
|
130
|
-
function
|
|
131
|
-
|
|
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" };
|
|
132
208
|
}
|
|
133
209
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
223
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
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
|
+
}
|
|
152
237
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|
|
156
253
|
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
261
|
|
|
162
|
-
/**
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
}
|
|
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
|
+
}
|
|
166
266
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
273
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|
|
178
286
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
if (!definition?.standalone) {
|
|
182
|
-
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;
|
|
183
289
|
}
|
|
184
|
-
const bundle = resolveAccessibleReference(definition.id, [], reference);
|
|
185
|
-
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}`;
|
|
186
|
-
}
|
|
187
290
|
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
}
|
|
196
301
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const state = { ...activeWorkflow, loadedReferences };
|
|
202
|
-
persistWorkflow(state);
|
|
203
|
-
return {
|
|
204
|
-
state,
|
|
205
|
-
content: `# Matt Pocock workflow reference\n\nWork item: ${state.workItemId}\nReference: ${bundle.root}\n\n${bundle.content}${stateContract(state, `loaded reference ${bundle.root}`)}`,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
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
|
+
}
|
|
208
306
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
331
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
}
|
|
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
|
+
}
|
|
216
339
|
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|
|
220
352
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
369
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
}
|
|
234
384
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const capability = capabilityFromChoice(choice);
|
|
239
|
-
if (capability) deliverProcedure(standalonePrompt(capability), { mode: "capability", capability, request: "" });
|
|
240
|
-
}
|
|
385
|
+
function routeChoices(): string[] {
|
|
386
|
+
return workflowRoutes().map((route) => `${route.label} — ${route.description}`);
|
|
387
|
+
}
|
|
241
388
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!activeWorkflow) {
|
|
245
|
-
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
246
|
-
return;
|
|
389
|
+
function routeFromChoice(choice: string): string | undefined {
|
|
390
|
+
return workflowRoutes().find((route) => choice.startsWith(route.label))?.route;
|
|
247
391
|
}
|
|
248
|
-
const state = transitionState(activeWorkflow, target);
|
|
249
|
-
resolveProcedureBundle(state.procedure);
|
|
250
|
-
persistWorkflow(state);
|
|
251
|
-
deliverProcedure(workflowPrompt(state, `transitioned to ${state.procedure}`), { ...state, request: "" });
|
|
252
|
-
}
|
|
253
392
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
notifyPi(ctx.ui, "No active Matt Pocock workflow.", "warning");
|
|
257
|
-
return;
|
|
393
|
+
function capabilityChoices(): string[] {
|
|
394
|
+
return standaloneCapabilities().map((capability) => `${capability.label} — ${capability.description ?? capability.id}`);
|
|
258
395
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return;
|
|
396
|
+
|
|
397
|
+
function capabilityFromChoice(choice: string): string | undefined {
|
|
398
|
+
return standaloneCapabilities().find((capability) => choice.startsWith(`${capability.label} —`))?.id;
|
|
263
399
|
}
|
|
264
|
-
const choice = await ctx.ui.select(`Transition ${activeWorkflow.route} from ${activeWorkflow.procedure}`, next);
|
|
265
|
-
if (!choice) return;
|
|
266
|
-
applyTransition(ctx, choice);
|
|
267
|
-
}
|
|
268
400
|
|
|
269
|
-
function
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
410
|
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
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
|
+
}
|
|
282
417
|
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
287
424
|
}
|
|
288
|
-
notifyPi(
|
|
289
|
-
ctx.ui,
|
|
290
|
-
`Matt Pocock workflow: ${formatReadableWorkflowSubject(activeWorkflow.route, activeWorkflow.phase)} · ${activeWorkflow.workItemId}`,
|
|
291
|
-
"info",
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
425
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
435
|
+
|
|
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
|
+
}
|
|
309
454
|
|
|
310
|
-
|
|
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
|
+
}
|
|
311
461
|
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
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
|
+
);
|
|
472
|
+
}
|
|
315
473
|
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
});
|
|
320
487
|
}
|
|
321
|
-
clearPiStatus(ctx.ui, "matt-pocock");
|
|
322
|
-
deliverProcedure(contextRoutingPrompt(cancelledWorkflow), { request: "" });
|
|
323
|
-
}
|
|
324
488
|
|
|
325
|
-
|
|
326
|
-
return `Route and execute this request through the relevant Matt Pocock workflow or standalone capability: ${prompt}\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.`;
|
|
327
|
-
}
|
|
489
|
+
const START_TASK_CHOICE = "Start a task";
|
|
328
490
|
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
331
499
|
|
|
332
|
-
|
|
333
|
-
const
|
|
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
|
+
}
|
|
506
|
+
|
|
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
|
+
}
|
|
334
516
|
if (!record || record.status !== "active") {
|
|
335
517
|
activeWorkflow = undefined;
|
|
336
518
|
refreshActiveTools(false);
|
|
@@ -339,19 +521,26 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
339
521
|
}
|
|
340
522
|
|
|
341
523
|
try {
|
|
342
|
-
|
|
524
|
+
const placement = workflowPlacement(record.route, record.procedure);
|
|
525
|
+
if (!placement || placement.phase !== record.phase) {
|
|
343
526
|
throw new Error(`Procedure ${record.procedure} is not part of workflow ${record.route}.`);
|
|
344
527
|
}
|
|
345
|
-
|
|
346
|
-
|
|
528
|
+
// A restore is a context boundary: this session may have been compacted or restarted,
|
|
529
|
+
// so the whole active context is re-delivered rather than deduplicated. Delivery
|
|
530
|
+
// tracking only suppresses repeats inside one continuous session.
|
|
531
|
+
const restored = withDeliveryDefaults(record);
|
|
532
|
+
const step = deliveryStep(restored, "restored workflow", "", true);
|
|
533
|
+
// Restore the in-memory delivery set without adding workflow state entries.
|
|
534
|
+
// The silent custom message must enter context before automatic compaction retries.
|
|
535
|
+
activeWorkflow = step.state;
|
|
347
536
|
refreshActiveTools(true);
|
|
348
537
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
349
538
|
pi.sendMessage({
|
|
350
539
|
customType: PROCEDURE_ENTRY,
|
|
351
|
-
content,
|
|
540
|
+
content: step.text,
|
|
352
541
|
display: false,
|
|
353
|
-
details:
|
|
354
|
-
}, {
|
|
542
|
+
details: step.state,
|
|
543
|
+
}, { triggerTurn: false });
|
|
355
544
|
} catch (error) {
|
|
356
545
|
activeWorkflow = record;
|
|
357
546
|
const cancelled = terminalState("cancelled", `Restore validation failed: ${String(error)}`);
|
|
@@ -364,7 +553,10 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
364
553
|
"warning",
|
|
365
554
|
);
|
|
366
555
|
}
|
|
367
|
-
}
|
|
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));
|
|
368
560
|
|
|
369
561
|
if (typeof pi.registerMessageRenderer === "function") {
|
|
370
562
|
pi.registerMessageRenderer(PROCEDURE_ENTRY, (message, options, theme) => {
|
|
@@ -379,21 +571,28 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
379
571
|
});
|
|
380
572
|
}
|
|
381
573
|
|
|
382
|
-
pi.on("before_agent_start", async (event) =>
|
|
383
|
-
|
|
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
|
|
384
582
|
? workflowGuidance(activeWorkflow, availableReferences(activeWorkflow))
|
|
385
|
-
: availableWorkflowsGuidance()}
|
|
386
|
-
})
|
|
583
|
+
: availableWorkflowsGuidance() });
|
|
584
|
+
});
|
|
387
585
|
|
|
388
586
|
pi.registerTool({
|
|
389
587
|
name: "matt_pocock_workflow",
|
|
390
588
|
label: "Matt Pocock Gateway",
|
|
391
|
-
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.",
|
|
392
590
|
promptSnippet: "Start a Matt Pocock workflow or run a standalone capability",
|
|
393
591
|
promptGuidelines: [
|
|
394
|
-
"mode workflow: structured multi-step engineering
|
|
395
|
-
"mode capability: a
|
|
396
|
-
"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>"}.',
|
|
397
596
|
"Start with matt_pocock_workflow; the started workflow carries its procedure and next-step guidance.",
|
|
398
597
|
],
|
|
399
598
|
parameters: workflowGatewayParameters(),
|
|
@@ -404,22 +603,21 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
404
603
|
const subject = details.mode === "workflow"
|
|
405
604
|
? readablePhaseTitle(details.phase ?? "active")
|
|
406
605
|
: details.mode === "reference"
|
|
407
|
-
? `${details.capability ?? "capability"} · ${details.reference ?? "reference"}`
|
|
408
|
-
: details.capability ?? "standalone capability";
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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);
|
|
412
612
|
},
|
|
413
|
-
async execute(_toolCallId, params,
|
|
613
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
614
|
+
signal?.throwIfAborted();
|
|
414
615
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
415
616
|
if (params.mode === "workflow") {
|
|
416
|
-
|
|
417
|
-
throw new Error(`Workflow ${activeWorkflow.workItemId} is already active. Complete or cancel it before starting another.`);
|
|
418
|
-
}
|
|
419
|
-
const state = startWorkflow(params.route);
|
|
617
|
+
const step = startWorkflow(params.route);
|
|
420
618
|
return {
|
|
421
|
-
content: [{ type: "text", text:
|
|
422
|
-
details: { mode: "workflow", ...state },
|
|
619
|
+
content: [{ type: "text", text: step.text }],
|
|
620
|
+
details: { mode: "workflow", ...step.state },
|
|
423
621
|
};
|
|
424
622
|
}
|
|
425
623
|
if (params.mode === "reference") {
|
|
@@ -429,6 +627,16 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
429
627
|
details: { mode: "reference", capability: params.capability, reference: params.reference },
|
|
430
628
|
};
|
|
431
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
|
+
}
|
|
432
640
|
const definition = findProcedure(params.capability);
|
|
433
641
|
if (!definition?.standalone || definition.invocation !== "model") {
|
|
434
642
|
throw new Error(`Capability ${params.capability} is not available to the model gateway.`);
|
|
@@ -443,7 +651,7 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
443
651
|
pi.registerTool({
|
|
444
652
|
name: "matt_pocock_active",
|
|
445
653
|
label: "Matt Pocock Active Workflow",
|
|
446
|
-
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.',
|
|
447
655
|
promptSnippet: "Transition, load a reference, complete, or cancel the active Matt Pocock workflow",
|
|
448
656
|
promptGuidelines: [
|
|
449
657
|
'action transition: {"action": "transition", "target": "<one allowedNext id>"} — target is required.',
|
|
@@ -455,34 +663,37 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
455
663
|
renderShell: "self",
|
|
456
664
|
renderCall: () => mattPocockRows.emptyCall(),
|
|
457
665
|
renderResult(result, options, theme, context) {
|
|
458
|
-
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 } };
|
|
459
667
|
// Saved results may still have route-based subjects; render from that event's phase, not active state.
|
|
460
668
|
const subject = details.state?.phase && (details.action === "transition" || details.action === "complete" || details.action === "cancel")
|
|
461
669
|
? workflowEventSubject(details.action, details.state.phase)
|
|
462
|
-
: details.
|
|
670
|
+
: details.action === "load"
|
|
671
|
+
? `loaded ${readableProcedureTitle(details.reference ?? details.subject?.replace(/^loaded /, "") ?? "reference")}`
|
|
672
|
+
: details.subject ?? details.action ?? "workflow updated";
|
|
463
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.
|
|
464
676
|
return mattPocockRows.result(() => eventToolLifecycle("matt pocock", subject, {
|
|
465
677
|
label: "event",
|
|
466
|
-
|
|
678
|
+
summary: reason ? [fieldLine("reason", reason)] : [],
|
|
467
679
|
}))(result, options, theme, context);
|
|
468
680
|
},
|
|
469
|
-
async execute(_toolCallId, params,
|
|
681
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
682
|
+
signal?.throwIfAborted();
|
|
470
683
|
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
471
684
|
if (params.action === "transition") {
|
|
472
|
-
const
|
|
473
|
-
resolveProcedureBundle(state.procedure);
|
|
474
|
-
persistWorkflow(state);
|
|
685
|
+
const step = transitionWorkflow(params.target);
|
|
475
686
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
476
687
|
return {
|
|
477
|
-
content: [{ type: "text", text:
|
|
478
|
-
details: { action: "transition", subject: workflowEventSubject("transition", state.phase), state },
|
|
688
|
+
content: [{ type: "text", text: step.text }],
|
|
689
|
+
details: { action: "transition", subject: workflowEventSubject("transition", step.state.phase), state: step.state },
|
|
479
690
|
};
|
|
480
691
|
}
|
|
481
692
|
if (params.action === "load") {
|
|
482
693
|
const loaded = loadActiveReference(params.reference);
|
|
483
694
|
return {
|
|
484
695
|
content: [{ type: "text", text: loaded.content }],
|
|
485
|
-
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 },
|
|
486
697
|
};
|
|
487
698
|
}
|
|
488
699
|
const terminal = params.action === "complete"
|
|
@@ -501,39 +712,23 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
501
712
|
},
|
|
502
713
|
});
|
|
503
714
|
|
|
504
|
-
function isChineseText(text: string): boolean {
|
|
505
|
-
return /[\u4e00-\u9fa5]/.test(text);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function deriveDialogTitle(explicitTitle?: string): string {
|
|
509
|
-
if (explicitTitle && explicitTitle.trim()) return explicitTitle.trim();
|
|
510
|
-
if (activeWorkflow) {
|
|
511
|
-
return formatReadableWorkflowSubject(activeWorkflow.route, activeWorkflow.phase);
|
|
512
|
-
}
|
|
513
|
-
return "Decision";
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function deriveDialogButtonNames(question: string): { ok: string; cancel: string } {
|
|
517
|
-
const isZh = isChineseText(question);
|
|
518
|
-
return {
|
|
519
|
-
ok: isZh ? "确认" : "Select",
|
|
520
|
-
cancel: isZh ? "取消" : "Cancel",
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
715
|
pi.registerTool({
|
|
525
716
|
name: "matt_pocock_ask",
|
|
526
717
|
label: "Matt Pocock Ask",
|
|
527
718
|
description: "Ask a structured workflow decision question via Pi UI selection.",
|
|
528
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
|
+
],
|
|
529
724
|
parameters: Type.Object({
|
|
530
725
|
title: Type.Optional(Type.String({ description: "Short title summarizing the decision context; defaults to workflow route and phase." })),
|
|
531
726
|
question: Type.String({ description: "The interview or decision question to ask the user." }),
|
|
532
|
-
options: Type.Array(Type.String(), { description: "2 to 4 suggested options, recommended option first." }),
|
|
533
|
-
recommended: Type.
|
|
534
|
-
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." })),
|
|
535
730
|
allow_custom: Type.Optional(Type.Boolean({ description: "Allow a custom typed answer; default true." })),
|
|
536
|
-
}),
|
|
731
|
+
}, { additionalProperties: false }),
|
|
537
732
|
renderShell: "self",
|
|
538
733
|
renderCall: () => mattPocockRows.emptyCall(),
|
|
539
734
|
renderResult(result, options, theme, context) {
|
|
@@ -547,140 +742,118 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
547
742
|
: answerLines.map((line, index) => index === 0 ? `Answer: ${line}` : ` ${line}`);
|
|
548
743
|
const metadata = [
|
|
549
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.
|
|
550
747
|
details.timed_out && details.pending ? fieldLine("reason", "selection timed out") : undefined,
|
|
551
748
|
details.source === "no_ui" ? fieldLine("reason", "no UI available") : undefined,
|
|
552
749
|
details.is_custom ? fieldLine("source", "custom input") : undefined,
|
|
553
750
|
].filter((line): line is string => Boolean(line));
|
|
554
751
|
return mattPocockRows.result(() => eventToolLifecycle("matt pocock", safeDisplayText(params.question ?? "question"), {
|
|
555
752
|
label: "ask",
|
|
556
|
-
|
|
557
|
-
|
|
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],
|
|
558
756
|
}))(result, options, theme, context);
|
|
559
757
|
},
|
|
560
|
-
async execute(_toolCallId, params,
|
|
758
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
561
759
|
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
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) {
|
|
616
812
|
return {
|
|
617
|
-
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." }],
|
|
618
814
|
details: { pending: true, timed_out: false, source: "cancelled" },
|
|
619
815
|
};
|
|
620
816
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
+
}
|
|
632
837
|
if (!answer) {
|
|
633
838
|
return {
|
|
634
|
-
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." }],
|
|
635
840
|
details: { pending: true, source: "custom_input_cancelled" },
|
|
636
841
|
};
|
|
637
842
|
}
|
|
638
|
-
return {
|
|
639
|
-
content: [{ type: "text", text: `User answered (custom): ${answer}` }],
|
|
640
|
-
details: { answer, is_custom: true, source: "custom_input" },
|
|
641
|
-
};
|
|
642
843
|
}
|
|
643
|
-
|
|
644
844
|
return {
|
|
645
|
-
content: [{ type: "text", text:
|
|
646
|
-
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" },
|
|
647
847
|
};
|
|
648
|
-
} catch {
|
|
649
|
-
// Graceful fallback to TUI on any native dialog error
|
|
650
848
|
}
|
|
651
|
-
}
|
|
652
849
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return {
|
|
658
|
-
content: [{ type: "text", text: `User selected (timeout default): ${answer}` }],
|
|
659
|
-
details: { answer, is_custom: false, timed_out: true, pending: false, source: "timeout_recommended" },
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
return {
|
|
663
|
-
content: [{ type: "text", text: "[Pending user decision] No answer was selected. Do not proceed until the user responds." }],
|
|
664
|
-
details: { pending: true, timed_out: false, source: "cancelled" },
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
if (selected === customOption) {
|
|
668
|
-
const answer = (await ctx.ui.input(params.question, "Enter your answer..."))?.trim();
|
|
669
|
-
if (!answer) {
|
|
670
|
-
return {
|
|
671
|
-
content: [{ type: "text", text: "[Pending user decision] No custom answer was provided. Do not proceed until the user responds." }],
|
|
672
|
-
details: { pending: true, source: "custom_input_cancelled" },
|
|
673
|
-
};
|
|
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
|
+
}
|
|
674
854
|
}
|
|
675
|
-
return
|
|
676
|
-
|
|
677
|
-
details: { answer, is_custom: true, source: "custom_input" },
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
return {
|
|
681
|
-
content: [{ type: "text", text: `User selected: ${selected}` }],
|
|
682
|
-
details: { answer: selected, is_custom: false, source: "choice_selected" },
|
|
683
|
-
};
|
|
855
|
+
return await ask(false);
|
|
856
|
+
});
|
|
684
857
|
},
|
|
685
858
|
});
|
|
686
859
|
|
|
@@ -700,6 +873,10 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
700
873
|
const token = command.match(/^\S+/)?.[0] ?? command;
|
|
701
874
|
const task = command.slice(token.length).trim();
|
|
702
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
|
+
}
|
|
703
880
|
if (token === "transition" && !task) {
|
|
704
881
|
if (!ctx.hasUI) {
|
|
705
882
|
notifyPi(ctx.ui, "Name a target: /matt-pocock transition <target>, or use the interactive menu.", "error");
|
|
@@ -708,16 +885,15 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
708
885
|
return chooseTransition(ctx);
|
|
709
886
|
}
|
|
710
887
|
if (token === "transition") return applyTransition(ctx, task);
|
|
711
|
-
if (token === "complete" && !task) return
|
|
888
|
+
if (token === "complete" && !task) return endWorkflow(ctx, "completed");
|
|
712
889
|
if (token === "cancel" || token === "end") {
|
|
713
|
-
return
|
|
890
|
+
return endWorkflow(ctx, "cancelled", task || `Cancelled by user with /matt-pocock ${token}.`);
|
|
714
891
|
}
|
|
715
892
|
|
|
716
893
|
if (findWorkflowRoute(token)) {
|
|
717
|
-
|
|
718
|
-
const state = startWorkflow(token);
|
|
894
|
+
const step = startWorkflow(token, task);
|
|
719
895
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
720
|
-
deliverProcedure(
|
|
896
|
+
deliverProcedure(step.text, { ...step.state, request: task });
|
|
721
897
|
return;
|
|
722
898
|
}
|
|
723
899
|
const capability = findProcedure(token);
|