@jwilger/pi-development-system 0.66.0 → 0.68.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.68.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"
@@ -8,5 +8,5 @@ Write the plan for the current work with the work-intake-and-slicing skill.
8
8
  2. Write `docs/plan/<slug>.md` with these sections: Goal and why; Constraints (what must not change); Increments, each shippable on its own and small enough to release, each with an **Acceptance** list (observable checks that say the increment is done) and a final Release step (commit, push, CI green, publish, update) followed by a STOP for the user; for every task a task record whose header is `## <id> — <title>` and whose sections are written exactly as `**Goal:**`, `**Files:**`, `**Interfaces:**`, `**First failing test:**`, `**Steps:**`, `**Run:**`, `**Expected:**`, `**Out of scope:**` (label and colon inside the bold, one section per label); a Progress checklist with one box per increment.
9
9
  3. Write each task so that someone who sees only that task could finish it. Never write `TBD`.
10
10
  4. Run `devsys_task_check` on every task record you wrote and fix each one until it says ready. Split any that come back too-big.
11
- 5. If a `create_goal` tool exists in this session, call it with the objective "complete docs/plan/<slug>.md honouring its Constraints and Progress checklist, one increment at a time, stopping after each increment's Release step". Otherwise tell the user the plan path so they can start a goal on it. Do not depend on any goal tool's file format.
12
- 6. Stop and ask the user to review the plan before any implementation starts.
11
+ 5. Stop and ask the user to review the plan before any implementation starts. Do not start a goal yet: a goal tool may continue on its own and begin implementing before the review.
12
+ 6. Only after the user approves the plan: if a `create_goal` tool exists in this session, call it with the objective "complete docs/plan/<slug>.md honouring its Constraints and Progress checklist, one increment at a time, stopping after each increment's Release step". Otherwise tell the user the plan path so they can start a goal on it. Do not depend on any goal tool's file format.
@@ -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
  };
@@ -29,6 +29,15 @@ const insideRepo = (cwd: string, target: string): boolean => {
29
29
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
30
30
  };
31
31
 
32
+ /** Jev's aspect keys, named as the record's own sections so the author knows what to edit. */
33
+ const SECTION_OF: Readonly<Record<string, string>> = {
34
+ goal: "Goal",
35
+ interfaces: "Interfaces",
36
+ firstFailingTest: "First failing test",
37
+ steps: "Steps",
38
+ check: "Run and Expected",
39
+ };
40
+
32
41
  /** `devsys_task_check`: structural readiness is deterministic; Jev adds "specific enough" and "one task". */
33
42
  export function createTaskCheckTool(deps: {
34
43
  jev: (ctx: ExtensionContext) => Jev;
@@ -72,7 +81,8 @@ export function createTaskCheckTool(deps: {
72
81
  `${record.id}: too-big. Split it into task records that each need one failing test, then check each.`,
73
82
  );
74
83
  }
75
- return reply(`${record.id}: needs-detail. Make these more specific: ${missing.join(", ")}.`);
84
+ const named = missing.map((m) => SECTION_OF[m] ?? m).join(", ");
85
+ return reply(`${record.id}: needs-detail. Make these more specific: ${named}.`);
76
86
  },
77
87
  };
78
88
  }
@@ -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) {