pi-matt-pocock 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +2 -2
- package/src/index.ts +66 -28
- package/src/resolver.ts +30 -0
- package/src/workflow.ts +22 -3
package/README.md
CHANGED
|
@@ -191,3 +191,7 @@ You can optionally configure native macOS dialog prompts for `matt_pocock_ask` v
|
|
|
191
191
|
By default (`useNativeDialog: false` or missing file), Pi uses its built-in TUI selection dialog. Native dialogs are automatically guarded to only trigger on local macOS GUI sessions (SSH sessions, non-macOS platforms, and CI environments always fall back cleanly to terminal TUI).
|
|
192
192
|
|
|
193
193
|
See the detailed [中文架构说明](ARCHITECTURE.zh-CN.md), the upstream [selection metadata](upstream-selection.json) and [sync rules](UPSTREAM.md), and the deliberately deferred items in [TODO.md](TODO.md).
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-matt-pocock",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BDD-first engineering and productivity skills adapted from Matt Pocock's skills",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"typebox": "*"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@fradser/pi-kit": "0.5.
|
|
42
|
+
"@fradser/pi-kit": "0.5.1"
|
|
43
43
|
},
|
|
44
44
|
"pi": {
|
|
45
45
|
"extensions": [
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
workflowDefinitions,
|
|
30
30
|
workflowPlacement,
|
|
31
31
|
} from "./catalog.ts";
|
|
32
|
-
import { resolveAccessibleReference, resolveProcedureBundle, resolveWorkflowContext } from "./resolver.ts";
|
|
32
|
+
import { resolveAccessibleReference, resolveProcedureBundle, resolveProcedureDelta, resolveWorkflowContext } from "./resolver.ts";
|
|
33
33
|
/** Geometry bound once: every Matt Pocock row shares hint and wrapping. */
|
|
34
34
|
const mattPocockRows = bindLifecycleRenderers({
|
|
35
35
|
fit: truncateToWidth,
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
transitionState,
|
|
50
50
|
workflowGuidance,
|
|
51
51
|
workflowRoutes,
|
|
52
|
+
withDeliveryDefaults,
|
|
52
53
|
WORKFLOW_STATE_ENTRY,
|
|
53
54
|
WORKFLOW_STATE_VERSION,
|
|
54
55
|
type TerminalWorkflowState,
|
|
@@ -164,9 +165,29 @@ function userRequestSection(request: string): string {
|
|
|
164
165
|
return request ? `\n\nUser target/request:\n${request}` : "";
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
/** Delivered when a procedure's body is already in the session, so the prompt is never empty. */
|
|
169
|
+
const ALREADY_DELIVERED = "Its instructions were delivered earlier in this session and are unchanged. Continue against them; do not re-request them.";
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The prompt for a workflow step, plus the state that records what it delivered.
|
|
173
|
+
*
|
|
174
|
+
* Only procedures this session has not been shown are embedded. A transition re-delivers
|
|
175
|
+
* the active context every time, and a bundle carries its whole dependency closure, so
|
|
176
|
+
* re-sending that closure would spend the context the work needs and end a long task early
|
|
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
|
+
};
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
function standalonePrompt(capability: string, request = ""): string {
|
|
@@ -185,24 +206,33 @@ function standaloneReferencePrompt(capability: string, reference: string): strin
|
|
|
185
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}`;
|
|
186
207
|
}
|
|
187
208
|
|
|
188
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Begin a workflow and persist its state exactly once, carrying the delivery set
|
|
211
|
+
* this first step shows. A second entry would read as two workflows begun, and a
|
|
212
|
+
* persisted state without the set would re-deliver the whole closure on restore.
|
|
213
|
+
*/
|
|
214
|
+
function startWorkflow(route: string, request = ""): { state: WorkflowState; text: string } {
|
|
189
215
|
if (!findWorkflowRoute(route)) throw new Error(`Unknown Matt Pocock route: ${route}`);
|
|
190
|
-
const
|
|
191
|
-
if (!
|
|
192
|
-
resolveProcedureBundle(
|
|
193
|
-
|
|
194
|
-
|
|
216
|
+
const created = routeEntryState(route, randomUUID());
|
|
217
|
+
if (!created) throw new Error(`Workflow ${route} has no catalog entry procedure.`);
|
|
218
|
+
resolveProcedureBundle(created.procedure);
|
|
219
|
+
const step = deliveryStep(created, "started workflow", request);
|
|
220
|
+
persistWorkflow(step.state);
|
|
221
|
+
return step;
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
function loadActiveReference(reference: string): { state: WorkflowState; content: string } {
|
|
198
225
|
if (!activeWorkflow) throw new Error("No active Matt Pocock workflow.");
|
|
199
226
|
const bundle = resolveAccessibleReference(activeWorkflow.procedure, activeWorkflow.loadedReferences, reference);
|
|
200
227
|
const loadedReferences = [...new Set([...activeWorkflow.loadedReferences, ...bundle.loaded])];
|
|
201
|
-
|
|
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 };
|
|
202
232
|
persistWorkflow(state);
|
|
203
233
|
return {
|
|
204
234
|
state,
|
|
205
|
-
content: `# Matt Pocock workflow reference\n\nWork item: ${state.workItemId}\nReference: ${bundle.root}\n\n${
|
|
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}`)}`,
|
|
206
236
|
};
|
|
207
237
|
}
|
|
208
238
|
|
|
@@ -227,9 +257,9 @@ async function chooseRoute(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
227
257
|
if (!choice) return;
|
|
228
258
|
const route = routeFromChoice(choice);
|
|
229
259
|
if (!route) return;
|
|
230
|
-
const
|
|
260
|
+
const step = startWorkflow(route);
|
|
231
261
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
232
|
-
deliverProcedure(
|
|
262
|
+
deliverProcedure(step.text, { ...step.state, request: "" });
|
|
233
263
|
}
|
|
234
264
|
|
|
235
265
|
async function chooseCapability(ctx: ExtensionCommandContext): Promise<void> {
|
|
@@ -247,8 +277,9 @@ function applyTransition(ctx: ExtensionCommandContext, target: string): void {
|
|
|
247
277
|
}
|
|
248
278
|
const state = transitionState(activeWorkflow, target);
|
|
249
279
|
resolveProcedureBundle(state.procedure);
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
const step = deliveryStep(state, `transitioned to ${state.procedure}`);
|
|
281
|
+
persistWorkflow(step.state);
|
|
282
|
+
deliverProcedure(step.text, { ...step.state, request: "" });
|
|
252
283
|
}
|
|
253
284
|
|
|
254
285
|
async function chooseTransition(ctx: ExtensionCommandContext): Promise<void> {
|
|
@@ -342,15 +373,21 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
342
373
|
if (!workflowPlacement(record.route, record.procedure)) {
|
|
343
374
|
throw new Error(`Procedure ${record.procedure} is not part of workflow ${record.route}.`);
|
|
344
375
|
}
|
|
345
|
-
|
|
346
|
-
|
|
376
|
+
// A restore is a context boundary: this session may have been compacted or restarted,
|
|
377
|
+
// so the whole active context is re-delivered rather than deduplicated. Delivery
|
|
378
|
+
// tracking only suppresses repeats inside one continuous session.
|
|
379
|
+
const restored = withDeliveryDefaults(record);
|
|
380
|
+
const step = deliveryStep(restored, "restored workflow", "", true);
|
|
381
|
+
// Not persisted here: session_start must not write to the branch. The next transition
|
|
382
|
+
// records the delivery set it has actually shown.
|
|
383
|
+
activeWorkflow = step.state;
|
|
347
384
|
refreshActiveTools(true);
|
|
348
385
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
349
386
|
pi.sendMessage({
|
|
350
387
|
customType: PROCEDURE_ENTRY,
|
|
351
|
-
content,
|
|
388
|
+
content: step.text,
|
|
352
389
|
display: false,
|
|
353
|
-
details:
|
|
390
|
+
details: step.state,
|
|
354
391
|
}, { deliverAs: "nextTurn" });
|
|
355
392
|
} catch (error) {
|
|
356
393
|
activeWorkflow = record;
|
|
@@ -416,10 +453,10 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
416
453
|
if (activeWorkflow) {
|
|
417
454
|
throw new Error(`Workflow ${activeWorkflow.workItemId} is already active. Complete or cancel it before starting another.`);
|
|
418
455
|
}
|
|
419
|
-
const
|
|
456
|
+
const step = startWorkflow(params.route);
|
|
420
457
|
return {
|
|
421
|
-
content: [{ type: "text", text:
|
|
422
|
-
details: { mode: "workflow", ...state },
|
|
458
|
+
content: [{ type: "text", text: step.text }],
|
|
459
|
+
details: { mode: "workflow", ...step.state },
|
|
423
460
|
};
|
|
424
461
|
}
|
|
425
462
|
if (params.mode === "reference") {
|
|
@@ -471,11 +508,12 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
471
508
|
if (params.action === "transition") {
|
|
472
509
|
const state = transitionState(activeWorkflow, params.target);
|
|
473
510
|
resolveProcedureBundle(state.procedure);
|
|
474
|
-
|
|
511
|
+
const step = deliveryStep(state, `transitioned to ${state.procedure}`);
|
|
512
|
+
persistWorkflow(step.state);
|
|
475
513
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
476
514
|
return {
|
|
477
|
-
content: [{ type: "text", text:
|
|
478
|
-
details: { action: "transition", subject: workflowEventSubject("transition", state.phase), state },
|
|
515
|
+
content: [{ type: "text", text: step.text }],
|
|
516
|
+
details: { action: "transition", subject: workflowEventSubject("transition", state.phase), state: step.state },
|
|
479
517
|
};
|
|
480
518
|
}
|
|
481
519
|
if (params.action === "load") {
|
|
@@ -715,9 +753,9 @@ export default function mattPocock(extensionApi: ExtensionAPI): void {
|
|
|
715
753
|
|
|
716
754
|
if (findWorkflowRoute(token)) {
|
|
717
755
|
if (activeWorkflow) throw new Error(`Workflow ${activeWorkflow.workItemId} is already active.`);
|
|
718
|
-
const
|
|
756
|
+
const step = startWorkflow(token, task);
|
|
719
757
|
clearPiStatus(ctx.ui, "matt-pocock");
|
|
720
|
-
deliverProcedure(
|
|
758
|
+
deliverProcedure(step.text, { ...step.state, request: task });
|
|
721
759
|
return;
|
|
722
760
|
}
|
|
723
761
|
const capability = findProcedure(token);
|
package/src/resolver.ts
CHANGED
|
@@ -125,3 +125,33 @@ export function resolveAccessibleReference(
|
|
|
125
125
|
}
|
|
126
126
|
return resolveProcedureBundle(target.id);
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
export interface ProcedureDelta {
|
|
130
|
+
content: string;
|
|
131
|
+
delivered: string[];
|
|
132
|
+
availableReferences: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The part of the active context this session has not been shown yet.
|
|
137
|
+
*
|
|
138
|
+
* A workflow re-delivers its prompt on every transition, and a procedure's bundle carries
|
|
139
|
+
* its whole dependency closure -- so re-sending the closure each time spends the context
|
|
140
|
+
* the work itself needs, and a long task stops partway for lack of room rather than for
|
|
141
|
+
* lack of work. A procedure already delivered in this session is therefore not sent again:
|
|
142
|
+
* the instructions the model has already read stay read, and only what is new arrives.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveProcedureDelta(
|
|
145
|
+
activeProcedure: string,
|
|
146
|
+
loadedReferences: string[],
|
|
147
|
+
delivered: string[],
|
|
148
|
+
): ProcedureDelta {
|
|
149
|
+
const context = resolveWorkflowContext(activeProcedure, loadedReferences);
|
|
150
|
+
const already = new Set(delivered);
|
|
151
|
+
const pending = context.loaded.filter((id) => !already.has(id));
|
|
152
|
+
return {
|
|
153
|
+
content: pending.map((id) => renderSource(requireDefinition(id))).join("\n\n"),
|
|
154
|
+
delivered: [...new Set([...delivered, ...context.loaded])],
|
|
155
|
+
availableReferences: context.availableReferences,
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/workflow.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface WorkflowState {
|
|
|
18
18
|
phase: string;
|
|
19
19
|
status: "active";
|
|
20
20
|
loadedReferences: string[];
|
|
21
|
+
/** Procedures whose body has already been delivered in this session. */
|
|
22
|
+
deliveredProcedures: string[];
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface TerminalWorkflowState {
|
|
@@ -54,6 +56,7 @@ export function routeEntryState(route: string, workItemId: string): WorkflowStat
|
|
|
54
56
|
phase: placement.phase,
|
|
55
57
|
status: "active",
|
|
56
58
|
loadedReferences: [],
|
|
59
|
+
deliveredProcedures: [],
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -113,7 +116,17 @@ export function isWorkflowState(value: unknown): value is WorkflowState {
|
|
|
113
116
|
&& typeof candidate.phase === "string"
|
|
114
117
|
&& candidate.status === "active"
|
|
115
118
|
&& Array.isArray(candidate.loadedReferences)
|
|
116
|
-
&& candidate.loadedReferences.every((reference) => typeof reference === "string")
|
|
119
|
+
&& candidate.loadedReferences.every((reference) => typeof reference === "string")
|
|
120
|
+
// Absent is tolerated so a workflow persisted before delivery tracking existed still
|
|
121
|
+
// restores; it simply re-delivers once, which is harmless.
|
|
122
|
+
&& (candidate.deliveredProcedures === undefined
|
|
123
|
+
|| (Array.isArray(candidate.deliveredProcedures)
|
|
124
|
+
&& candidate.deliveredProcedures.every((id) => typeof id === "string")));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Backfill delivery tracking on a state persisted before it existed. */
|
|
128
|
+
export function withDeliveryDefaults(state: WorkflowState): WorkflowState {
|
|
129
|
+
return { ...state, deliveredProcedures: state.deliveredProcedures ?? [] };
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
export function isTerminalWorkflowState(value: unknown): value is TerminalWorkflowState {
|
|
@@ -138,13 +151,19 @@ export function latestWorkflowRecord(entries: unknown[]): WorkflowSessionData |
|
|
|
138
151
|
|
|
139
152
|
export function latestWorkflowState(entries: unknown[]): WorkflowState | undefined {
|
|
140
153
|
const record = latestWorkflowRecord(entries);
|
|
141
|
-
return record?.status === "active" ? record : undefined;
|
|
154
|
+
return record?.status === "active" ? withDeliveryDefaults(record) : undefined;
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
export function workflowGuidance(state: WorkflowState, availableReferences: string[]): string {
|
|
145
158
|
const next = allowedTransitions(state.route, state.procedure);
|
|
146
159
|
return `Matt Pocock workflow active: ${state.route} · ${state.phase}.
|
|
147
|
-
Work item: ${state.workItemId}. Follow the loaded ${state.procedure} procedure.
|
|
160
|
+
Work item: ${state.workItemId}. Follow the loaded ${state.procedure} procedure.
|
|
161
|
+
|
|
162
|
+
Drive the current procedure to its end in this turn. A multi-step deliverable is not finished because one step passed: keep implementing, testing and fixing until every step the procedure covers is done, rather than stopping at the first green slice or handing back a plan for the remainder. Do not end the turn to ask whether to continue.
|
|
163
|
+
|
|
164
|
+
Use matt_pocock_active to transition only to: ${next.join(", ") || "none"}; load an available reference; complete the work; or cancel it with a reason. Available references: ${availableReferences.join(", ") || "none"}. Exact shapes: {"action": "transition", "target": "<allowedNext id>"}; {"action": "load", "reference": "<availableReferences id>"}; {"action": "complete"}; {"action": "cancel", "reason": "<why>"}. Proceed autonomously through non-user-owned work: transition to the next applicable procedure as soon as this one's deliverables are complete, without stopping to ask permission. Ask only for a genuinely user-owned decision, an unavailable fact, or a required external action, and use matt_pocock_ask for those.
|
|
165
|
+
|
|
166
|
+
Yield only when the remaining work genuinely depends on an external actor, on a teammate result you cannot produce yourself, or on context you no longer have — and when you yield, state plainly what is unfinished rather than implying completion. Never announce completion early: call matt_pocock_active with action complete only once the integrated candidate has applicable verification and all blocking reviews have returned with their findings resolved; a completed review assignment is not itself a PASS verdict, and evidence must match that candidate.`;
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
export function availableWorkflowsGuidance(): string {
|