@jwilger/pi-development-system 0.86.0 → 0.87.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/README.md +4 -0
- package/package.json +1 -1
- package/prompts/devsys-plan.md +1 -1
- package/prompts/devsys-start.md +2 -2
- package/skills/work-intake-and-slicing/SKILL.md +1 -1
- package/src/context/status.ts +2 -3
- package/src/core/review-flow.ts +2 -1
- package/src/planning/intake-tool.ts +72 -25
- package/src/planning/intake.ts +33 -0
package/README.md
CHANGED
|
@@ -40,6 +40,10 @@ in `local-only` mode; CI is not awaited), and `devsys_finish_slice` closes it ex
|
|
|
40
40
|
reason, abandons it. A plan's increments each end in a push, so each increment is its own slice: the
|
|
41
41
|
next one starts with `devsys_intake`.
|
|
42
42
|
|
|
43
|
+
`devsys_intake` does not interrupt autonomous work: Jev's size is used when Jev is at least 50%
|
|
44
|
+
confident, and the user is asked to pick only when it is less sure (or unavailable). A size decided up
|
|
45
|
+
front, such as in a goal's planning, is passed as `size` and skips the sizing question entirely (a `fix` still asks whether to waive review).
|
|
46
|
+
|
|
43
47
|
With `codemode` enabled (`"defaultTools": ["+codemode"]` in pi settings), rarely used tools are
|
|
44
48
|
reached through scripts and the `judge_*` Jev wrappers exist for scripts only; without codemode
|
|
45
49
|
the rarely used tools are declared directly and the wrappers are absent.
|
package/package.json
CHANGED
package/prompts/devsys-plan.md
CHANGED
|
@@ -9,4 +9,4 @@ Write the plan for the current work with the work-intake-and-slicing skill.
|
|
|
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
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: call `devsys_begin_work` (so the review and red-first gates are on while the increment is built; it does nothing if the work is already implementing). A push of a clean, reviewed tree closes the slice, so each later increment starts with `devsys_intake`. Then, 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. Only after the user approves the plan: call `devsys_begin_work` (so the review and red-first gates are on while the increment is built; it does nothing if the work is already implementing). A push of a clean, reviewed tree closes the slice, so each later increment starts with `devsys_intake`. Then, if a `create_goal` tool exists in this session, call it with the objective (name each increment's size in it, so every later `devsys_intake` passes `size` and nobody is asked to size it; a `fix` still asks the user whether to waive review) "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.
|
package/prompts/devsys-start.md
CHANGED
|
@@ -5,7 +5,7 @@ argument-hint: "<what you want done>"
|
|
|
5
5
|
Start new work with the work-intake-and-slicing skill.
|
|
6
6
|
|
|
7
7
|
1. Call `devsys_intake` with `request` set to: $ARGUMENTS (if empty, ask the user what they want done first).
|
|
8
|
-
2. Show the user the
|
|
8
|
+
2. Show the user the size and artifacts. Jev sizes the work and intake proceeds without asking when it is at least 50% confident; only a less confident Jev makes the tool ask the user to pick. If the size is already decided (say, in a goal's or plan's up-front planning), pass it as `size` and nobody is asked to size it (a `fix` still asks whether to waive review).
|
|
9
9
|
3. Produce the planning artifacts first (brief, decision register, journeys, event model, architecture, lens review, in that order, whichever are recommended), then the task record. `review` and `adr-if-needed` are not written up front: review happens on the finished slice, and an ADR when a decision needs one. 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
10
|
4. For `capability` and `product` the phase stays `planning` while you produce the artifacts and the plan. The single point where it changes is the user approving the whole plan: then call `devsys_begin_work` so the review and red-first gates switch on (it does nothing if already implementing).
|
|
11
|
-
5. For a
|
|
11
|
+
5. For a `fix` the user is also asked (when a user is present) 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.
|
|
@@ -16,7 +16,7 @@ Ask these in order and stop at the first yes:
|
|
|
16
16
|
3. Does it change existing behaviour in one area? That is a `change`.
|
|
17
17
|
4. Otherwise it is a `fix`.
|
|
18
18
|
|
|
19
|
-
`/devsys-start`
|
|
19
|
+
`/devsys-start` sizes the work with Jev and proceeds when Jev is at least 50% confident, saying so; below that it asks you to pick. If the size is already decided (a goal's planning, a plan's increment), pass it as `size` and nobody is asked to size it (a `fix` still asks whether to waive review). If you disagree with a size, change it; it is a judgement, not a rule.
|
|
20
20
|
|
|
21
21
|
## Artifacts follow size
|
|
22
22
|
|
package/src/context/status.ts
CHANGED
|
@@ -14,9 +14,8 @@ const ciLabel = (state: DevsysState): string | undefined => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const activeReview = (state: DevsysState): string | undefined => {
|
|
17
|
-
//
|
|
18
|
-
const review =
|
|
19
|
-
state.activeSlice === undefined ? state.reviews?.at(-1) : reviewOf(state, state.activeSlice);
|
|
17
|
+
// No active slice, no review: a closed slice's last count would only mislead the next one.
|
|
18
|
+
const review = state.activeSlice === undefined ? undefined : reviewOf(state, state.activeSlice);
|
|
20
19
|
return review === undefined ? undefined : reviewLabel(review);
|
|
21
20
|
};
|
|
22
21
|
|
package/src/core/review-flow.ts
CHANGED
|
@@ -19,8 +19,9 @@ export const upsertReview = (state: DevsysState, review: ReviewState): DevsysSta
|
|
|
19
19
|
return { ...state, reviews: [...others, review] };
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
/** Capped at the requirement: extra clean rounds (after a late change) are not progress past "done". */
|
|
22
23
|
export const reviewLabel = (review: ReviewState): string =>
|
|
23
|
-
`review: ${cleanStreak(review)}/${review.required} clean`;
|
|
24
|
+
`review: ${Math.min(cleanStreak(review), review.required)}/${review.required} clean`;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Why a commit on this slice lacks a satisfied review, or `undefined` when the review is complete
|
|
@@ -13,13 +13,27 @@ import type { Jev } from "../jev/client.ts";
|
|
|
13
13
|
import { judgeSizing } from "../jev/questions/sizing.ts";
|
|
14
14
|
import { appendDecision } from "../state/decision-log.ts";
|
|
15
15
|
import type { SessionState } from "../state/session-state.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
decideSize,
|
|
18
|
+
phaseFor,
|
|
19
|
+
proposeArtifacts,
|
|
20
|
+
renderProposal,
|
|
21
|
+
type SizeDecision,
|
|
22
|
+
sliceSlug,
|
|
23
|
+
uniqueSlice,
|
|
24
|
+
} from "./intake.ts";
|
|
17
25
|
|
|
18
26
|
const Parameters = Type.Object({
|
|
19
27
|
request: Type.String({ description: "What the user asked for, in their words." }),
|
|
20
28
|
repoSummary: Type.Optional(
|
|
21
29
|
Type.String({ description: "One or two sentences on the repository, if known." }),
|
|
22
30
|
),
|
|
31
|
+
size: Type.Optional(
|
|
32
|
+
Type.String({
|
|
33
|
+
description:
|
|
34
|
+
"fix, change, capability or product, when the size is already decided (for example during a goal's up-front planning). Skips Jev's sizing question and the user's.",
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
23
37
|
});
|
|
24
38
|
|
|
25
39
|
const SIZES: readonly Sizing[] = ["fix", "change", "capability", "product"];
|
|
@@ -62,6 +76,9 @@ async function declinedToReplace(
|
|
|
62
76
|
const { activeSlice, phase } = before;
|
|
63
77
|
const inFlight = phase === "implementing" || phase === "reviewing" || phase === "delivering";
|
|
64
78
|
if (activeSlice === undefined || !inFlight) return undefined;
|
|
79
|
+
if (!ctx.hasUI) {
|
|
80
|
+
return `Slice "${activeSlice}" is still ${phase}; replacing it needs the user's yes, and there is no UI to ask. Intake cancelled; slice "${activeSlice}" is unchanged.`;
|
|
81
|
+
}
|
|
65
82
|
const go = await ctx.ui.confirm(
|
|
66
83
|
"Slice already in flight",
|
|
67
84
|
`Slice "${activeSlice}" is still ${phase}. Its uncommitted or unpushed changes would ship under the new slice's rules. Start new work anyway?`,
|
|
@@ -69,6 +86,28 @@ async function declinedToReplace(
|
|
|
69
86
|
return go ? undefined : `Intake cancelled; slice "${activeSlice}" is unchanged.`;
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
type Chosen = { readonly sizing: Sizing } | { readonly stop: string; readonly isError: boolean };
|
|
90
|
+
|
|
91
|
+
/** A decided size passes through; an undecided one asks the user (headless: stops with the proposal). */
|
|
92
|
+
async function chooseSize(
|
|
93
|
+
ctx: ExtensionContext,
|
|
94
|
+
decision: SizeDecision,
|
|
95
|
+
proposalFor: (size: Sizing) => ReturnType<typeof proposeArtifacts>,
|
|
96
|
+
): Promise<Chosen> {
|
|
97
|
+
if (decision.kind === "decided") return { sizing: decision.size };
|
|
98
|
+
const { basis, proposed } = decision;
|
|
99
|
+
const proposal = renderProposal({ sizing: proposed, basis, proposal: proposalFor(proposed) });
|
|
100
|
+
if (!ctx.hasUI) {
|
|
101
|
+
return { stop: `${proposal}\nHeadless: proposal only, not applied.`, isError: false };
|
|
102
|
+
}
|
|
103
|
+
const picked = await ctx.ui.select(`Size this work (${basis})`, sizeChoices(proposed));
|
|
104
|
+
if (picked === undefined) {
|
|
105
|
+
return { stop: `${proposal}\nIntake cancelled; nothing changed.`, isError: false };
|
|
106
|
+
}
|
|
107
|
+
const parsed = parseSizing(picked);
|
|
108
|
+
return isParseError(parsed) ? { stop: parsed.message, isError: true } : { sizing: parsed };
|
|
109
|
+
}
|
|
110
|
+
|
|
72
111
|
const waiverNote = (sizing: Sizing, departed: string | undefined): string => {
|
|
73
112
|
if (departed !== undefined)
|
|
74
113
|
return `\nRecorded in ${departed}: you waived fresh-context review for this fix.`;
|
|
@@ -108,7 +147,7 @@ async function fixWithoutReview(
|
|
|
108
147
|
return path;
|
|
109
148
|
}
|
|
110
149
|
|
|
111
|
-
/** `devsys_intake`: Jev
|
|
150
|
+
/** `devsys_intake`: Jev (or the caller) sizes the work; only a doubtful Jev asks the user; state moves to the first phase. */
|
|
112
151
|
export function createIntakeTool(deps: {
|
|
113
152
|
pi: ExtensionAPI;
|
|
114
153
|
state: SessionState;
|
|
@@ -119,36 +158,39 @@ export function createIntakeTool(deps: {
|
|
|
119
158
|
label: "Start work",
|
|
120
159
|
description:
|
|
121
160
|
"Size a request (fix, change, capability, product) and list the planning artifacts that size needs. " +
|
|
122
|
-
"
|
|
161
|
+
"Jev sizes the work and the user is asked only when Jev is under 50% confident; pass `size` when it is already decided. Then phase, sizing and the active slice are set. Call at the start of any non-trivial work.",
|
|
123
162
|
promptSnippet: "Size new work and propose the planning artifacts it needs",
|
|
124
163
|
parameters: Parameters,
|
|
125
164
|
exposure: "model-only",
|
|
126
165
|
async execute(_id, params: Static<typeof Parameters>, _signal, _onUpdate, ctx) {
|
|
127
166
|
const request = params.request.trim();
|
|
128
167
|
if (request === "") return reply("request must not be empty", true);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
const given = params.size === undefined ? undefined : parseSizing(params.size.trim());
|
|
169
|
+
if (given !== undefined && isParseError(given)) return reply(given.message, true);
|
|
170
|
+
const judged =
|
|
171
|
+
given === undefined
|
|
172
|
+
? await judgeSizing(deps.jev(ctx), { request, repoSummary: params.repoSummary ?? "" })
|
|
173
|
+
: undefined;
|
|
174
|
+
const decision = decideSize(
|
|
175
|
+
given,
|
|
176
|
+
judged?.ok ? judged.value : undefined,
|
|
177
|
+
judged?.ok === false ? judged.error.kind : "not asked",
|
|
178
|
+
);
|
|
179
|
+
const artifactNeed = judged?.ok ? judged.value.artifactNeed : {};
|
|
138
180
|
const proposalFor = (size: Sizing) => proposeArtifacts(size, artifactNeed);
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
proposal: proposalFor(proposedSize),
|
|
143
|
-
});
|
|
144
|
-
if (!ctx.hasUI) return reply(`${proposal}\nHeadless: proposal only, not applied.`);
|
|
145
|
-
const picked = await ctx.ui.select(`Size this work (${basis})`, sizeChoices(proposedSize));
|
|
146
|
-
if (picked === undefined) return reply(`${proposal}\nIntake cancelled; nothing changed.`);
|
|
147
|
-
const sizing = parseSizing(picked);
|
|
148
|
-
if (isParseError(sizing)) return reply(sizing.message, true);
|
|
181
|
+
const chosen = await chooseSize(ctx, decision, proposalFor);
|
|
182
|
+
if ("stop" in chosen) return reply(chosen.stop, chosen.isError);
|
|
183
|
+
const { sizing } = chosen;
|
|
149
184
|
const before = deps.state.get();
|
|
150
185
|
const keep = await declinedToReplace(ctx, before);
|
|
151
|
-
if (keep !== undefined)
|
|
186
|
+
if (keep !== undefined) {
|
|
187
|
+
const proposal = renderProposal({
|
|
188
|
+
sizing,
|
|
189
|
+
basis: decision.basis,
|
|
190
|
+
proposal: proposalFor(sizing),
|
|
191
|
+
});
|
|
192
|
+
return reply(`${proposal}\n${keep}`);
|
|
193
|
+
}
|
|
152
194
|
const slice = uniqueSlice(sliceSlug(request), slicesInUse(before)) as SliceRef;
|
|
153
195
|
deps.state.update((s) => ({
|
|
154
196
|
...s,
|
|
@@ -156,9 +198,14 @@ export function createIntakeTool(deps: {
|
|
|
156
198
|
sizing,
|
|
157
199
|
activeSlice: slice,
|
|
158
200
|
}));
|
|
159
|
-
const waived =
|
|
201
|
+
const waived =
|
|
202
|
+
sizing === "fix" && ctx.hasUI ? await askToWaiveReview(ctx, request, slice) : false;
|
|
160
203
|
const departed = waived ? await fixWithoutReview(deps, ctx, slice) : undefined;
|
|
161
|
-
const final = renderProposal({
|
|
204
|
+
const final = renderProposal({
|
|
205
|
+
sizing,
|
|
206
|
+
basis: decision.basis,
|
|
207
|
+
proposal: proposalFor(sizing),
|
|
208
|
+
});
|
|
162
209
|
const note = waiverNote(sizing, departed);
|
|
163
210
|
return reply(`${final}\nPhase: ${phaseFor(sizing)}. Active slice: ${slice}.${note}`);
|
|
164
211
|
},
|
package/src/planning/intake.ts
CHANGED
|
@@ -71,3 +71,36 @@ export const renderProposal = (input: {
|
|
|
71
71
|
);
|
|
72
72
|
return lines.join("\n");
|
|
73
73
|
};
|
|
74
|
+
|
|
75
|
+
/** Jev confidence below this asks the human; at or above it intake proceeds with Jev's size. */
|
|
76
|
+
const ASK_BELOW = 0.5;
|
|
77
|
+
|
|
78
|
+
export type SizeDecision =
|
|
79
|
+
| { readonly kind: "decided"; readonly size: Sizing; readonly basis: string }
|
|
80
|
+
| { readonly kind: "ask"; readonly proposed: Sizing; readonly basis: string };
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Pure: who sizes the work. A size given up front (the caller, e.g. a goal's planning, already
|
|
84
|
+
* decided) wins; otherwise Jev decides when it is confident enough; only a doubtful or missing
|
|
85
|
+
* judgement asks the human, with Jev's size (or `change`) as the proposal.
|
|
86
|
+
*/
|
|
87
|
+
export function decideSize(
|
|
88
|
+
given: Sizing | undefined,
|
|
89
|
+
judged: { readonly sizing: Sizing; readonly confidence: number } | undefined,
|
|
90
|
+
unavailable: string,
|
|
91
|
+
): SizeDecision {
|
|
92
|
+
if (given !== undefined) {
|
|
93
|
+
return { kind: "decided", size: given, basis: `Size given up front: ${given}.` };
|
|
94
|
+
}
|
|
95
|
+
if (judged === undefined) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "ask",
|
|
98
|
+
proposed: "change",
|
|
99
|
+
basis: `Jev unavailable (${unavailable}); defaulted to change.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const said = `Jev judged ${judged.sizing} (confidence ${judged.confidence.toFixed(2)})`;
|
|
103
|
+
return judged.confidence >= ASK_BELOW
|
|
104
|
+
? { kind: "decided", size: judged.sizing, basis: `${said}; proceeding without asking.` }
|
|
105
|
+
: { kind: "ask", proposed: judged.sizing, basis: `${said}, too unsure to proceed alone.` };
|
|
106
|
+
}
|