@jwilger/pi-development-system 0.65.0 → 0.66.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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.65.0",
3
+ "version": "0.66.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"
@@ -125,10 +125,8 @@ async function reviewNeeds(
125
125
  ctx: ExtensionContext,
126
126
  command: string,
127
127
  ): Promise<Need[]> {
128
- const { phase, activeSlice, sizing } = deps.state.get();
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 { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
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,40 @@ const slicesInUse = (state: DevsysState): Set<string> =>
35
43
  ...state.openDepartures.flatMap((d) => (d.scope.kind === "slice" ? [d.scope.slice] : [])),
36
44
  ]);
37
45
 
46
+ /**
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.
49
+ */
50
+ async function fixWithoutReview(
51
+ deps: { pi: ExtensionAPI; state: SessionState },
52
+ ctx: ExtensionContext,
53
+ slice: SliceRef,
54
+ ): Promise<string | undefined> {
55
+ const info = lookupGate("review.unsatisfied");
56
+ const at = new Date();
57
+ const departure = parseDeparture({
58
+ id: `dep-${at.getTime()}-intake`,
59
+ gate: "review.unsatisfied",
60
+ tier: "soft",
61
+ default: info?.default ?? "finish the fresh-context review before release",
62
+ 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",
64
+ costIfWrong: "a defect in a small change ships without a fresh-context look",
65
+ approver: "user",
66
+ scope: { kind: "slice", slice },
67
+ revisitWhen: "the fix turns out to touch more than its task record named",
68
+ recordedAt: at.toISOString().replace(/\.\d{3}Z$/, "Z"),
69
+ });
70
+ if (isParseError(departure)) return undefined;
71
+ const { path } = await appendDecision(ctx.cwd, departure, at);
72
+ deps.pi.appendEntry(DEPARTURE_ENTRY_TYPE, departure);
73
+ deps.state.update((s) => ({ ...s, openDepartures: [...s.openDepartures, departure] }));
74
+ return path;
75
+ }
76
+
38
77
  /** `devsys_intake`: Jev proposes a size and artifact set; the user confirms; state moves to the first phase. */
39
78
  export function createIntakeTool(deps: {
79
+ pi: ExtensionAPI;
40
80
  state: SessionState;
41
81
  jev: (ctx: ExtensionContext) => Jev;
42
82
  }): ToolDefinition<typeof Parameters> {
@@ -79,8 +119,13 @@ export function createIntakeTool(deps: {
79
119
  sizing,
80
120
  activeSlice: slice,
81
121
  }));
122
+ const departed = sizing === "fix" ? await fixWithoutReview(deps, ctx, slice) : undefined;
82
123
  const final = renderProposal({ sizing, basis, proposal: proposalFor(sizing) });
83
- return reply(`${final}\nPhase: ${phaseFor(sizing)}. Active slice: ${slice}.`);
124
+ const note =
125
+ departed === undefined
126
+ ? ""
127
+ : `\nRecorded in ${departed}: no review rounds for this fix (you sized it fix).`;
128
+ return reply(`${final}\nPhase: ${phaseFor(sizing)}. Active slice: ${slice}.${note}`);
84
129
  },
85
130
  };
86
131
  }
@@ -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+(.*)$/;