@jwilger/pi-development-system 0.66.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.66.0",
3
+ "version": "0.67.0",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -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.
@@ -43,9 +43,44 @@ const slicesInUse = (state: DevsysState): Set<string> =>
43
43
  ...state.openDepartures.flatMap((d) => (d.scope.kind === "slice" ? [d.scope.slice] : [])),
44
44
  ]);
45
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
+
46
81
  /**
47
- * I8.1 sizes a fix without a review artifact, but the commit gate reviews every slice (I6.4). The user choosing
48
- * "fix" is the explicit agreement, so it is recorded as a user-approved departure for this slice only.
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.
49
84
  */
50
85
  async function fixWithoutReview(
51
86
  deps: { pi: ExtensionAPI; state: SessionState },
@@ -60,7 +95,7 @@ async function fixWithoutReview(
60
95
  tier: "soft",
61
96
  default: info?.default ?? "finish the fresh-context review before release",
62
97
  chosen: "no review rounds for this slice",
63
- why: "the user confirmed this work as a fix, which the sizing table plans without a review artifact",
98
+ why: "the user was asked and agreed: this is a fix, which the sizing table plans without a review artifact",
64
99
  costIfWrong: "a defect in a small change ships without a fresh-context look",
65
100
  approver: "user",
66
101
  scope: { kind: "slice", slice },
@@ -112,19 +147,20 @@ export function createIntakeTool(deps: {
112
147
  if (picked === undefined) return reply(`${proposal}\nIntake cancelled; nothing changed.`);
113
148
  const sizing = parseSizing(picked);
114
149
  if (isParseError(sizing)) return reply(sizing.message, true);
115
- const slice = uniqueSlice(sliceSlug(request), slicesInUse(deps.state.get())) as SliceRef;
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;
116
154
  deps.state.update((s) => ({
117
155
  ...s,
118
156
  phase: phaseFor(sizing),
119
157
  sizing,
120
158
  activeSlice: slice,
121
159
  }));
122
- const departed = sizing === "fix" ? await fixWithoutReview(deps, ctx, slice) : undefined;
160
+ const waived = sizing === "fix" ? await askToWaiveReview(ctx, request, slice) : false;
161
+ const departed = waived ? await fixWithoutReview(deps, ctx, slice) : undefined;
123
162
  const final = renderProposal({ sizing, basis, proposal: proposalFor(sizing) });
124
- const note =
125
- departed === undefined
126
- ? ""
127
- : `\nRecorded in ${departed}: no review rounds for this fix (you sized it fix).`;
163
+ const note = waiverNote(sizing, departed);
128
164
  return reply(`${final}\nPhase: ${phaseFor(sizing)}. Active slice: ${slice}.${note}`);
129
165
  },
130
166
  };
@@ -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. "Already exists" is success.
230
- const made = await gh(["label", "create", IN_PROGRESS]);
231
- if (!(made.ok || /already exists/i.test(made.error.message))) return made;
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) {