@jwilger/pi-development-system 0.65.0 → 0.67.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/extensions/development-system.ts +1 -1
- package/package.json +1 -1
- package/prompts/devsys-start.md +1 -1
- package/skills/work-intake-and-slicing/SKILL.md +1 -1
- package/src/gates/commit-guard.ts +1 -3
- package/src/planning/intake-tool.ts +84 -3
- package/src/planning/task-record.ts +1 -1
- package/src/tracker/github.ts +4 -3
|
@@ -141,7 +141,7 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
141
141
|
pi.registerTool(createRequestApprovalTool({ pi, approvals }));
|
|
142
142
|
pi.registerTool(createModelsTool());
|
|
143
143
|
pi.registerTool(createRouteTaskTool({ jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
144
|
-
pi.registerTool(createIntakeTool({ state, jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
144
|
+
pi.registerTool(createIntakeTool({ pi, state, jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
145
145
|
pi.registerTool(createTaskCheckTool({ jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
146
146
|
pi.registerTool(createWorkItemTool({ exec }));
|
|
147
147
|
const reviewDeps = {
|
package/package.json
CHANGED
package/prompts/devsys-start.md
CHANGED
|
@@ -7,4 +7,4 @@ Start new work with the work-intake-and-slicing skill.
|
|
|
7
7
|
1. Call `devsys_intake` with `request` set to: $ARGUMENTS (if empty, ask the user what they want done first).
|
|
8
8
|
2. Show the user the proposed size and artifacts. The tool asks them to confirm or change the size.
|
|
9
9
|
3. Follow the recommended artifacts in order. To skip one, call `devsys_record_departure` with gate `artifact.skipped:<artifact>` and the reason; do not skip silently. The event model and architecture are recommendations, never gates.
|
|
10
|
-
4. For `fix` and `change` the phase is already `implementing`: write the task record, then work it test-first.
|
|
10
|
+
4. For a confirmed `fix` the user is also asked whether to skip fresh-context review (it is logged if they say yes; if no, the slice needs its review rounds). For `fix` and `change` the phase is already `implementing`: write the task record, then work it test-first.
|
|
@@ -20,7 +20,7 @@ Ask these in order and stop at the first yes:
|
|
|
20
20
|
|
|
21
21
|
## Artifacts follow size
|
|
22
22
|
|
|
23
|
-
- `fix`: a task record.
|
|
23
|
+
- `fix`: a task record. The user is asked separately whether to waive fresh-context review for it; a yes is logged as a user-approved `review.unsatisfied` departure for that slice, a no keeps the review gate.
|
|
24
24
|
- `change`: task record, review, an ADR if a decision is hard to reverse.
|
|
25
25
|
- `capability`: add a brief-lite, journeys, an event model, and optionally lens review.
|
|
26
26
|
- `product`: the full set, including brief, decision register and architecture.
|
|
@@ -125,10 +125,8 @@ async function reviewNeeds(
|
|
|
125
125
|
ctx: ExtensionContext,
|
|
126
126
|
command: string,
|
|
127
127
|
): Promise<Need[]> {
|
|
128
|
-
const { phase, activeSlice
|
|
128
|
+
const { phase, activeSlice } = deps.state.get();
|
|
129
129
|
if ((phase !== "implementing" && phase !== "reviewing") || activeSlice === undefined) return [];
|
|
130
|
-
// A fix is sized without a review artifact (I8.1); the gate must not demand rounds the size never asked for.
|
|
131
|
-
if (sizing === "fix") return [];
|
|
132
130
|
const snap = await snapshotDiff(deps.exec, ctx.cwd, "HEAD");
|
|
133
131
|
// An unreadable diff cannot be compared; the gate asks for a departure rather than guessing.
|
|
134
132
|
const gap = reviewGap(
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
import { type Static, Type } from "typebox";
|
|
7
|
+
import { parseDeparture } from "../core/departure.ts";
|
|
8
|
+
import { lookupGate } from "../core/gates.ts";
|
|
3
9
|
import { parseSizing } from "../core/sizing.ts";
|
|
4
10
|
import { type DevsysState, isParseError, type Sizing, type SliceRef } from "../core/types.ts";
|
|
11
|
+
import { DEPARTURE_ENTRY_TYPE } from "../gates/record-departure-tool.ts";
|
|
5
12
|
import type { Jev } from "../jev/client.ts";
|
|
6
13
|
import { judgeSizing } from "../jev/questions/sizing.ts";
|
|
14
|
+
import { appendDecision } from "../state/decision-log.ts";
|
|
7
15
|
import type { SessionState } from "../state/session-state.ts";
|
|
8
16
|
import { phaseFor, proposeArtifacts, renderProposal, sliceSlug, uniqueSlice } from "./intake.ts";
|
|
9
17
|
|
|
@@ -35,8 +43,75 @@ const slicesInUse = (state: DevsysState): Set<string> =>
|
|
|
35
43
|
...state.openDepartures.flatMap((d) => (d.scope.kind === "slice" ? [d.scope.slice] : [])),
|
|
36
44
|
]);
|
|
37
45
|
|
|
46
|
+
/** The waiver is its own question, naming the request and what the review gate actually covers. */
|
|
47
|
+
const askToWaiveReview = (
|
|
48
|
+
ctx: ExtensionContext,
|
|
49
|
+
request: string,
|
|
50
|
+
slice: SliceRef,
|
|
51
|
+
): Promise<boolean> =>
|
|
52
|
+
ctx.ui.confirm(
|
|
53
|
+
"Skip fresh-context review for this fix?",
|
|
54
|
+
`"${request.slice(0, 120)}"\nThe review gate checks the whole uncommitted diff, so yes waives review for everything committed under slice "${slice}". The waiver is logged in docs/decisions.`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/** Starting new work replaces the active slice; while it is in flight that needs the user's yes. */
|
|
58
|
+
async function declinedToReplace(
|
|
59
|
+
ctx: ExtensionContext,
|
|
60
|
+
before: DevsysState,
|
|
61
|
+
): Promise<string | undefined> {
|
|
62
|
+
const { activeSlice, phase } = before;
|
|
63
|
+
if (activeSlice === undefined || (phase !== "implementing" && phase !== "reviewing")) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const go = await ctx.ui.confirm(
|
|
67
|
+
"Slice already in flight",
|
|
68
|
+
`Slice "${activeSlice}" is still ${phase}. Its uncommitted changes would be committed under the new slice's rules. Start new work anyway?`,
|
|
69
|
+
);
|
|
70
|
+
return go ? undefined : `Intake cancelled; slice "${activeSlice}" is unchanged.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const waiverNote = (sizing: Sizing, departed: string | undefined): string => {
|
|
74
|
+
if (departed !== undefined)
|
|
75
|
+
return `\nRecorded in ${departed}: you waived fresh-context review for this fix.`;
|
|
76
|
+
return sizing === "fix"
|
|
77
|
+
? "\nReview is NOT waived: this slice still needs its review rounds before commit."
|
|
78
|
+
: "";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* I8.1 sizes a fix without a review artifact, but the commit gate reviews every slice (I6.4). Only the user's
|
|
83
|
+
* explicit yes to the waiver question is recorded, as a user-approved departure for this slice alone.
|
|
84
|
+
*/
|
|
85
|
+
async function fixWithoutReview(
|
|
86
|
+
deps: { pi: ExtensionAPI; state: SessionState },
|
|
87
|
+
ctx: ExtensionContext,
|
|
88
|
+
slice: SliceRef,
|
|
89
|
+
): Promise<string | undefined> {
|
|
90
|
+
const info = lookupGate("review.unsatisfied");
|
|
91
|
+
const at = new Date();
|
|
92
|
+
const departure = parseDeparture({
|
|
93
|
+
id: `dep-${at.getTime()}-intake`,
|
|
94
|
+
gate: "review.unsatisfied",
|
|
95
|
+
tier: "soft",
|
|
96
|
+
default: info?.default ?? "finish the fresh-context review before release",
|
|
97
|
+
chosen: "no review rounds for this slice",
|
|
98
|
+
why: "the user was asked and agreed: this is a fix, which the sizing table plans without a review artifact",
|
|
99
|
+
costIfWrong: "a defect in a small change ships without a fresh-context look",
|
|
100
|
+
approver: "user",
|
|
101
|
+
scope: { kind: "slice", slice },
|
|
102
|
+
revisitWhen: "the fix turns out to touch more than its task record named",
|
|
103
|
+
recordedAt: at.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
104
|
+
});
|
|
105
|
+
if (isParseError(departure)) return undefined;
|
|
106
|
+
const { path } = await appendDecision(ctx.cwd, departure, at);
|
|
107
|
+
deps.pi.appendEntry(DEPARTURE_ENTRY_TYPE, departure);
|
|
108
|
+
deps.state.update((s) => ({ ...s, openDepartures: [...s.openDepartures, departure] }));
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
|
|
38
112
|
/** `devsys_intake`: Jev proposes a size and artifact set; the user confirms; state moves to the first phase. */
|
|
39
113
|
export function createIntakeTool(deps: {
|
|
114
|
+
pi: ExtensionAPI;
|
|
40
115
|
state: SessionState;
|
|
41
116
|
jev: (ctx: ExtensionContext) => Jev;
|
|
42
117
|
}): ToolDefinition<typeof Parameters> {
|
|
@@ -72,15 +147,21 @@ export function createIntakeTool(deps: {
|
|
|
72
147
|
if (picked === undefined) return reply(`${proposal}\nIntake cancelled; nothing changed.`);
|
|
73
148
|
const sizing = parseSizing(picked);
|
|
74
149
|
if (isParseError(sizing)) return reply(sizing.message, true);
|
|
75
|
-
const
|
|
150
|
+
const before = deps.state.get();
|
|
151
|
+
const keep = await declinedToReplace(ctx, before);
|
|
152
|
+
if (keep !== undefined) return reply(`${proposal}\n${keep}`);
|
|
153
|
+
const slice = uniqueSlice(sliceSlug(request), slicesInUse(before)) as SliceRef;
|
|
76
154
|
deps.state.update((s) => ({
|
|
77
155
|
...s,
|
|
78
156
|
phase: phaseFor(sizing),
|
|
79
157
|
sizing,
|
|
80
158
|
activeSlice: slice,
|
|
81
159
|
}));
|
|
160
|
+
const waived = sizing === "fix" ? await askToWaiveReview(ctx, request, slice) : false;
|
|
161
|
+
const departed = waived ? await fixWithoutReview(deps, ctx, slice) : undefined;
|
|
82
162
|
const final = renderProposal({ sizing, basis, proposal: proposalFor(sizing) });
|
|
83
|
-
|
|
163
|
+
const note = waiverNote(sizing, departed);
|
|
164
|
+
return reply(`${final}\nPhase: ${phaseFor(sizing)}. Active slice: ${slice}.${note}`);
|
|
84
165
|
},
|
|
85
166
|
};
|
|
86
167
|
}
|
|
@@ -26,7 +26,7 @@ const SECTIONS = [
|
|
|
26
26
|
] as const;
|
|
27
27
|
type Section = (typeof SECTIONS)[number];
|
|
28
28
|
|
|
29
|
-
const HEADER = /^## (\S+) [—-] (
|
|
29
|
+
const HEADER = /^## (\S+) [—-] (\S.*?)\s*$/;
|
|
30
30
|
const SECTION_LINE = /^\*\*([^:*]+)(?::\*\*|\*\*:)\s*(.*)$/;
|
|
31
31
|
/** Top-level list items only (CommonMark allows up to three spaces of indent); deeper items belong to the step above. */
|
|
32
32
|
const STEP = /^ {0,3}(?:\d+[.)]|[-*+])\s+(.*)$/;
|
package/src/tracker/github.ts
CHANGED
|
@@ -226,9 +226,10 @@ const updateIssue = async (
|
|
|
226
226
|
const current = toItem(raw.value);
|
|
227
227
|
const hadMarker = (raw.value.labels ?? []).some((l) => isMarker(l.name));
|
|
228
228
|
if (patch.status === "in-progress") {
|
|
229
|
-
// No --force: that would recolour the team's existing label.
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
// No --force: that would recolour the team's existing label. Creating needs push rights, which someone who can
|
|
230
|
+
// only triage lacks even when the label exists, so a failed create is not fatal: the edit below fails if the
|
|
231
|
+
// label is really missing, and says so.
|
|
232
|
+
await gh(["label", "create", IN_PROGRESS]);
|
|
232
233
|
}
|
|
233
234
|
const flags = editFlags(current, hadMarker, patch);
|
|
234
235
|
if (flags.length > 0) {
|