@jwilger/pi-development-system 0.53.0 → 0.54.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.
|
@@ -19,6 +19,7 @@ import { registerRedFirstGuard } from "../src/gates/red-first-guard.ts";
|
|
|
19
19
|
import { createRequestApprovalTool } from "../src/gates/request-approval-tool.ts";
|
|
20
20
|
import { registerTestGuard } from "../src/gates/test-guard.ts";
|
|
21
21
|
import { createJevHolder } from "../src/jev/holder.ts";
|
|
22
|
+
import { createIntakeTool } from "../src/planning/intake-tool.ts";
|
|
22
23
|
import { createReviewRecordTool, createReviewStartTool } from "../src/review/review-tools.ts";
|
|
23
24
|
import { registerCiCommand } from "../src/state/ci-command.ts";
|
|
24
25
|
import { loadConfig } from "../src/state/config.ts";
|
|
@@ -138,6 +139,7 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
138
139
|
pi.registerTool(createRequestApprovalTool({ pi, approvals }));
|
|
139
140
|
pi.registerTool(createModelsTool());
|
|
140
141
|
pi.registerTool(createRouteTaskTool({ jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
142
|
+
pi.registerTool(createIntakeTool({ state, jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
141
143
|
const reviewDeps = {
|
|
142
144
|
state,
|
|
143
145
|
jev: (ctx: ExtensionContext) => jevHolder.forContext(ctx),
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Size new work and get the planning artifacts it needs before building
|
|
3
|
+
argument-hint: "<what you want done>"
|
|
4
|
+
---
|
|
5
|
+
Start new work with the work-intake-and-slicing skill.
|
|
6
|
+
|
|
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 proposed size and artifacts. The tool asks them to confirm or change the size.
|
|
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.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { parseSizing } from "../core/sizing.ts";
|
|
4
|
+
import { isParseError, type Sizing, type SliceRef } from "../core/types.ts";
|
|
5
|
+
import type { Jev } from "../jev/client.ts";
|
|
6
|
+
import { judgeSizing } from "../jev/questions/sizing.ts";
|
|
7
|
+
import type { SessionState } from "../state/session-state.ts";
|
|
8
|
+
import { phaseFor, proposeArtifacts, renderProposal, sliceSlug } from "./intake.ts";
|
|
9
|
+
|
|
10
|
+
const Parameters = Type.Object({
|
|
11
|
+
request: Type.String({ description: "What the user asked for, in their words." }),
|
|
12
|
+
repoSummary: Type.Optional(
|
|
13
|
+
Type.String({ description: "One or two sentences on the repository, if known." }),
|
|
14
|
+
),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const SIZES: readonly Sizing[] = ["fix", "change", "capability", "product"];
|
|
18
|
+
|
|
19
|
+
const reply = (text: string, isError = false) => ({
|
|
20
|
+
content: [{ type: "text" as const, text }],
|
|
21
|
+
details: undefined,
|
|
22
|
+
isError,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Proposed size first, so the default selection is the proposal. */
|
|
26
|
+
const sizeChoices = (proposed: Sizing): string[] => [
|
|
27
|
+
proposed,
|
|
28
|
+
...SIZES.filter((s) => s !== proposed),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** `devsys_intake`: Jev proposes a size and artifact set; the user confirms; state moves to the first phase. */
|
|
32
|
+
export function createIntakeTool(deps: {
|
|
33
|
+
state: SessionState;
|
|
34
|
+
jev: (ctx: ExtensionContext) => Jev;
|
|
35
|
+
}): ToolDefinition<typeof Parameters> {
|
|
36
|
+
return {
|
|
37
|
+
name: "devsys_intake",
|
|
38
|
+
label: "Start work",
|
|
39
|
+
description:
|
|
40
|
+
"Size a request (fix, change, capability, product) and list the planning artifacts that size needs. " +
|
|
41
|
+
"The user confirms the size; then phase, sizing and the active slice are set. Call at the start of any non-trivial work.",
|
|
42
|
+
promptSnippet: "Size new work and propose the planning artifacts it needs",
|
|
43
|
+
parameters: Parameters,
|
|
44
|
+
exposure: "direct",
|
|
45
|
+
async execute(_id, params: Static<typeof Parameters>, _signal, _onUpdate, ctx) {
|
|
46
|
+
const request = params.request.trim();
|
|
47
|
+
if (request === "") return reply("request must not be empty", true);
|
|
48
|
+
const judged = await judgeSizing(deps.jev(ctx), {
|
|
49
|
+
request,
|
|
50
|
+
repoSummary: params.repoSummary ?? "",
|
|
51
|
+
});
|
|
52
|
+
const proposedSize: Sizing = judged.ok ? judged.value.sizing : "change";
|
|
53
|
+
const basis = judged.ok
|
|
54
|
+
? `Jev judged ${judged.value.sizing} (confidence ${judged.value.confidence.toFixed(2)}).`
|
|
55
|
+
: `Jev unavailable (${judged.error.kind}); defaulted to change.`;
|
|
56
|
+
const artifactNeed = judged.ok ? judged.value.artifactNeed : {};
|
|
57
|
+
const proposalFor = (size: Sizing) => proposeArtifacts(size, artifactNeed);
|
|
58
|
+
const proposal = renderProposal({
|
|
59
|
+
sizing: proposedSize,
|
|
60
|
+
basis,
|
|
61
|
+
proposal: proposalFor(proposedSize),
|
|
62
|
+
});
|
|
63
|
+
if (!ctx.hasUI) return reply(`${proposal}\nHeadless: proposal only, not applied.`);
|
|
64
|
+
const picked = await ctx.ui.select(`Size this work (${basis})`, sizeChoices(proposedSize));
|
|
65
|
+
if (picked === undefined) return reply(`${proposal}\nIntake cancelled; nothing changed.`);
|
|
66
|
+
const sizing = parseSizing(picked);
|
|
67
|
+
if (isParseError(sizing)) return reply(sizing.message, true);
|
|
68
|
+
const slice = sliceSlug(request) as SliceRef;
|
|
69
|
+
deps.state.update((s) => ({
|
|
70
|
+
...s,
|
|
71
|
+
phase: phaseFor(sizing),
|
|
72
|
+
sizing,
|
|
73
|
+
activeSlice: slice,
|
|
74
|
+
}));
|
|
75
|
+
const final = renderProposal({ sizing, basis, proposal: proposalFor(sizing) });
|
|
76
|
+
return reply(`${final}\nPhase: ${phaseFor(sizing)}. Active slice: ${slice}.`);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ARTIFACTS, type ArtifactId, recommendedArtifacts } from "../core/sizing.ts";
|
|
2
|
+
import type { Phase, Sizing } from "../core/types.ts";
|
|
3
|
+
|
|
4
|
+
const SLUG_MAX = 40;
|
|
5
|
+
/** Jev need at or above this offers an artifact the sizing table did not list. */
|
|
6
|
+
const CONSIDER_AT = 0.5;
|
|
7
|
+
|
|
8
|
+
export const sliceSlug = (request: string): string => {
|
|
9
|
+
const slug = request
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
12
|
+
.replace(/^-+/, "")
|
|
13
|
+
.slice(0, SLUG_MAX)
|
|
14
|
+
.replace(/-+$/, "");
|
|
15
|
+
return slug === "" ? "work" : slug;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ArtifactProposal = {
|
|
19
|
+
readonly recommended: readonly ArtifactId[];
|
|
20
|
+
readonly consider: readonly ArtifactId[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const proposeArtifacts = (
|
|
24
|
+
sizing: Sizing,
|
|
25
|
+
artifactNeed: Readonly<Partial<Record<ArtifactId, number>>>,
|
|
26
|
+
): ArtifactProposal => {
|
|
27
|
+
const recommended = recommendedArtifacts(sizing);
|
|
28
|
+
const consider = ARTIFACTS.filter(
|
|
29
|
+
(id) => !recommended.includes(id) && (artifactNeed[id] ?? 0) >= CONSIDER_AT,
|
|
30
|
+
);
|
|
31
|
+
return { recommended, consider };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Small work starts building; bigger work plans first (D4: proportional). */
|
|
35
|
+
export const phaseFor = (sizing: Sizing): Phase =>
|
|
36
|
+
sizing === "fix" || sizing === "change" ? "implementing" : "planning";
|
|
37
|
+
|
|
38
|
+
export const renderProposal = (input: {
|
|
39
|
+
sizing: Sizing;
|
|
40
|
+
basis: string;
|
|
41
|
+
proposal: ArtifactProposal;
|
|
42
|
+
}): string => {
|
|
43
|
+
const { sizing, basis, proposal } = input;
|
|
44
|
+
const lines = [
|
|
45
|
+
`Proposed sizing: ${sizing}. ${basis}`,
|
|
46
|
+
`Recommended artifacts: ${proposal.recommended.join(", ")}.`,
|
|
47
|
+
];
|
|
48
|
+
if (proposal.consider.length > 0) lines.push(`Also consider: ${proposal.consider.join(", ")}.`);
|
|
49
|
+
lines.push(
|
|
50
|
+
"Skipping a recommended artifact is allowed but recorded: call devsys_record_departure with gate artifact.skipped:<artifact>.",
|
|
51
|
+
"The event model and architecture are never blocked by a gate; they are recommended, not enforced.",
|
|
52
|
+
);
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
};
|