@jwilger/pi-development-system 0.86.0 → 0.88.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 +8 -0
- package/agents/reviewer.md +2 -1
- package/extensions/development-system.ts +5 -0
- package/package.json +1 -1
- package/prompts/devsys-plan.md +1 -1
- package/prompts/devsys-review.md +1 -1
- package/prompts/devsys-start.md +2 -2
- package/skills/code-review/SKILL.md +10 -6
- package/skills/work-intake-and-slicing/SKILL.md +1 -1
- package/src/context/phase-guide.ts +1 -1
- package/src/context/status.ts +2 -3
- package/src/core/review-flow.ts +4 -2
- package/src/core/review-submission.ts +60 -0
- package/src/planning/intake-tool.ts +72 -25
- package/src/planning/intake.ts +33 -0
- package/src/review/review-tools.ts +55 -15
- package/src/review/submissions.ts +38 -0
- package/src/review/submit-tool.ts +85 -0
package/README.md
CHANGED
|
@@ -27,6 +27,10 @@ terminology, journeys). `devsys_lens_review` plans a review of the brief by five
|
|
|
27
27
|
writes the packets to `docs/product/reviews/`; `devsys_adr_new` creates the next numbered ADR, and
|
|
28
28
|
a commit that shapes the architecture without one is stopped by the soft gate `adr.missing`.
|
|
29
29
|
|
|
30
|
+
A reviewer subagent hands over its result with `devsys_submit_review` (typed arguments; a result that
|
|
31
|
+
contradicts itself or is for the wrong round is refused with an error id), and `devsys_review_record`
|
|
32
|
+
reads it. A markdown packet is still accepted (ADR 0006).
|
|
33
|
+
|
|
30
34
|
Event modelling has a skill (`event-modelling`: three slice patterns, Given/When/Then, completeness).
|
|
31
35
|
`devsys_event_model_check` validates a directory of slice files (schema v1) and renders the swimlane
|
|
32
36
|
Markdown or a Mermaid diagram; each profile's `gwt-tests` reference turns scenarios into failing tests.
|
|
@@ -40,6 +44,10 @@ in `local-only` mode; CI is not awaited), and `devsys_finish_slice` closes it ex
|
|
|
40
44
|
reason, abandons it. A plan's increments each end in a push, so each increment is its own slice: the
|
|
41
45
|
next one starts with `devsys_intake`.
|
|
42
46
|
|
|
47
|
+
`devsys_intake` does not interrupt autonomous work: Jev's size is used when Jev is at least 50%
|
|
48
|
+
confident, and the user is asked to pick only when it is less sure (or unavailable). A size decided up
|
|
49
|
+
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).
|
|
50
|
+
|
|
43
51
|
With `codemode` enabled (`"defaultTools": ["+codemode"]` in pi settings), rarely used tools are
|
|
44
52
|
reached through scripts and the `judge_*` Jev wrappers exist for scripts only; without codemode
|
|
45
53
|
the rarely used tools are declared directly and the wrappers are absent.
|
package/agents/reviewer.md
CHANGED
|
@@ -18,6 +18,7 @@ tools:
|
|
|
18
18
|
- find
|
|
19
19
|
- ls
|
|
20
20
|
- agent_update
|
|
21
|
+
- devsys_submit_review
|
|
21
22
|
- agent_pause
|
|
22
23
|
---
|
|
23
24
|
|
|
@@ -41,7 +42,7 @@ Do not modify files. bash is not a sandbox: use it only for read-only inspection
|
|
|
41
42
|
|
|
42
43
|
## Output
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
Submit your result by calling `devsys_submit_review` (slice and round exactly as the task gives them): it checks the call and refuses a self-contradicting one with an error id, which you correct and resubmit. After it accepts, end with one short line. Only when that tool is not available, return exactly this packet instead; the coordinator parses it.
|
|
45
46
|
|
|
46
47
|
```markdown
|
|
47
48
|
## Review — <slice> — round <n> — lenses: <a, b>
|
|
@@ -35,6 +35,8 @@ import { createFinishSliceTool, registerSliceClose } from "../src/planning/slice
|
|
|
35
35
|
import { createTaskCheckTool } from "../src/planning/task-check-tool.ts";
|
|
36
36
|
import { createLensReviewTool } from "../src/review/lens-review-tool.ts";
|
|
37
37
|
import { createReviewRecordTool, createReviewStartTool } from "../src/review/review-tools.ts";
|
|
38
|
+
import { createSubmissionStore } from "../src/review/submissions.ts";
|
|
39
|
+
import { createSubmitReviewTool } from "../src/review/submit-tool.ts";
|
|
38
40
|
import { registerCiCommand } from "../src/state/ci-command.ts";
|
|
39
41
|
import { loadConfig } from "../src/state/config.ts";
|
|
40
42
|
import { createModelsTool, registerModelsCommand } from "../src/state/models-command.ts";
|
|
@@ -186,13 +188,16 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
186
188
|
createLensReviewTool({ state, jev: (ctx) => jevHolder.forContext(ctx), now: () => new Date() }),
|
|
187
189
|
);
|
|
188
190
|
pi.registerTool(createIntakeTool({ pi, state, jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
191
|
+
const submissions = createSubmissionStore();
|
|
189
192
|
const reviewDeps = {
|
|
190
193
|
state,
|
|
194
|
+
submissions,
|
|
191
195
|
jev: (ctx: ExtensionContext) => jevHolder.forContext(ctx),
|
|
192
196
|
exec,
|
|
193
197
|
} satisfies Parameters<typeof createReviewStartTool>[0];
|
|
194
198
|
pi.registerTool(createReviewStartTool(reviewDeps));
|
|
195
199
|
pi.registerTool(createReviewRecordTool(reviewDeps));
|
|
200
|
+
pi.registerTool(createSubmitReviewTool({ state, submissions }));
|
|
196
201
|
registerModelsCommand(pi);
|
|
197
202
|
piSubagent(pi);
|
|
198
203
|
|
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-review.md
CHANGED
|
@@ -6,5 +6,5 @@ Review the work in progress with the code-review skill.
|
|
|
6
6
|
|
|
7
7
|
1. Call `devsys_review_start` (slice and diff range: $ARGUMENTS; omit either to use the active slice and `HEAD`; only the `HEAD` range clears the commit gate).
|
|
8
8
|
2. Run the `agent_spawn` payload it returns, unchanged.
|
|
9
|
-
3.
|
|
9
|
+
3. The reviewer submits its result with `devsys_submit_review`; call `devsys_review_record` with the same `slice`, `diffDigest` and `diffRange` the start reply gave (no packets needed). Only if the reviewer returned a markdown packet instead, pass it verbatim in `packets`.
|
|
10
10
|
4. Report `review: N/R clean` and the next action. If the next action is `fix-findings`, list each blocking and should-fix finding with its `path:line`.
|
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.
|
|
@@ -17,11 +17,13 @@ Review is a soft gate (`review.unsatisfied`): skipping it needs a recorded depar
|
|
|
17
17
|
diff digest, lets Jev choose lenses, and returns an exact `agent_spawn` payload.
|
|
18
18
|
2. Run that `agent_spawn` unchanged. The reviewer is a top-level agent, not a
|
|
19
19
|
child of this conversation, so it does not inherit your context.
|
|
20
|
-
3.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
3. The reviewer submits its result with `devsys_submit_review` (typed arguments; a
|
|
21
|
+
self-contradicting or wrong-round result is refused to the reviewer with an error id).
|
|
22
|
+
Call `devsys_review_record` with the `slice` and `diffDigest` the start reply gave, and
|
|
23
|
+
the same `diffRange` if you gave one; it reads the submitted results of the round. If a
|
|
24
|
+
reviewer returned a markdown packet instead, pass it verbatim in `packets: [...]` (all
|
|
25
|
+
lens packets of one round in one call). A result is recorded once, in the round it
|
|
26
|
+
names; if the diff changed since the start, the round is refused: start again.
|
|
25
27
|
4. Read the reply: `review: N/R clean` and `next:`.
|
|
26
28
|
- `fix-findings`: fix every blocking and should-fix finding, then start a new round
|
|
27
29
|
(starting again on an unchanged diff is refused).
|
|
@@ -29,7 +31,9 @@ Review is a soft gate (`review.unsatisfied`): skipping it needs a recorded depar
|
|
|
29
31
|
- `stale-diff`: the diff changed after a satisfied review; one more round.
|
|
30
32
|
- `done`: commit.
|
|
31
33
|
|
|
32
|
-
## The
|
|
34
|
+
## The result
|
|
35
|
+
|
|
36
|
+
The fields of `devsys_submit_review` mirror this packet; the markdown form is the fallback (ADR 0006).
|
|
33
37
|
|
|
34
38
|
```markdown
|
|
35
39
|
## Review — <slice> — round <n> — lenses: <a, b>
|
|
@@ -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
|
|
|
@@ -12,7 +12,7 @@ export function phaseGuide(phase: Phase): string {
|
|
|
12
12
|
case "implementing":
|
|
13
13
|
return "Implementing one slice. Write a failing test first, make it pass with the least code, run the tests and read the result before claiming anything. Push when the slice is complete and reviewed: a push of a clean tree closes it (devsys_finish_slice closes it explicitly).";
|
|
14
14
|
case "reviewing":
|
|
15
|
-
return "Reviewing. Call devsys_review_start for a fresh-context review,
|
|
15
|
+
return "Reviewing. Call devsys_review_start for a fresh-context review, give its diffDigest to devsys_review_record (the reviewer submits through devsys_submit_review), fix every blocking and should-fix finding (red-first still applies), and repeat until the review is satisfied; then the slice moves to delivering.";
|
|
16
16
|
case "delivering":
|
|
17
17
|
return "Delivering. The review is satisfied. Commit with a rationale, push, and release; a push of a clean tree closes the slice (devsys_finish_slice closes it explicitly, or abandons it with a reason). Editing source reopens implementing. A red trunk (CI) is repaired first.";
|
|
18
18
|
default:
|
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
|
|
@@ -175,7 +176,8 @@ export function reviewerTask(input: {
|
|
|
175
176
|
`Review slice ${input.slice}, round ${input.round}, through these lenses: ${lenses}.`,
|
|
176
177
|
`See the change with \`git diff ${input.diffRange}\` (and \`git diff --stat ${input.diffRange}\`), plus any untracked files listed by \`git ls-files --others --exclude-standard\` (git diff does not show them; read them directly). Read the callers, callees and tests it depends on, no more.`,
|
|
177
178
|
"Do not modify files; report demonstrable defects with a realistic trigger and the smallest local repair.",
|
|
178
|
-
"
|
|
179
|
+
`Report your result by calling devsys_submit_review with slice "${input.slice}", round ${input.round}, lenses ${JSON.stringify(input.lenses)}, your sources, your findings (lens, severity, path, line, summary) and the verdict. If it refuses, for example with verdict-contradicts-findings or wrong-round, correct the call and submit again; when it accepts, end with one short line.`,
|
|
180
|
+
"Only if devsys_submit_review is not available, return the review packet from your instructions instead, with exactly this header (slice and round must not change):",
|
|
179
181
|
"",
|
|
180
182
|
`## Review — ${input.slice} — round ${input.round} — lenses: ${lenses}`,
|
|
181
183
|
"### Sources inspected",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Finding, Severity } from "./review.ts";
|
|
2
|
+
import type { ReviewPacket } from "./review-packet.ts";
|
|
3
|
+
|
|
4
|
+
/** A reviewer's result as it arrives from the typed submit tool (see ADR 0006). */
|
|
5
|
+
export type Submission = {
|
|
6
|
+
readonly slice: string;
|
|
7
|
+
readonly round: number;
|
|
8
|
+
readonly lenses: ReadonlyArray<string>;
|
|
9
|
+
readonly sources: ReadonlyArray<string>;
|
|
10
|
+
readonly findings: ReadonlyArray<{
|
|
11
|
+
readonly lens: string;
|
|
12
|
+
readonly severity: Severity;
|
|
13
|
+
readonly path?: string | undefined;
|
|
14
|
+
readonly line?: number | undefined;
|
|
15
|
+
readonly summary: string;
|
|
16
|
+
}>;
|
|
17
|
+
readonly verdict: "no-blocking" | "blocking";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Why a submission was refused: a stable kebab-case id and a message the reviewer can act on. */
|
|
21
|
+
export type SubmitError = {
|
|
22
|
+
readonly id: "verdict-contradicts-findings" | "finding-lens-not-listed";
|
|
23
|
+
readonly message: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const counts = (f: { severity: Severity }): boolean =>
|
|
27
|
+
f.severity === "blocking" || f.severity === "should-fix";
|
|
28
|
+
|
|
29
|
+
/** The packet a submission stands for, or the refusal. The schema has already checked its shape. */
|
|
30
|
+
export function packetFromSubmission(input: Submission): ReviewPacket | SubmitError {
|
|
31
|
+
const unlisted = input.findings.find((f) => !input.lenses.includes(f.lens));
|
|
32
|
+
if (unlisted !== undefined) {
|
|
33
|
+
return {
|
|
34
|
+
id: "finding-lens-not-listed",
|
|
35
|
+
message: `a finding names lens "${unlisted.lens}" but the packet's lenses are ${input.lenses.join(", ")}; list every lens you used in \`lenses\``,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (input.findings.some(counts) !== (input.verdict === "blocking")) {
|
|
39
|
+
return {
|
|
40
|
+
id: "verdict-contradicts-findings",
|
|
41
|
+
message: `verdict "${input.verdict}" contradicts the findings: blocking/should-fix findings mean "blocking", otherwise "no-blocking"`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const findings: Finding[] = input.findings.map((f, i) => ({
|
|
45
|
+
id: `${f.lens}-${i + 1}`,
|
|
46
|
+
severity: f.severity,
|
|
47
|
+
...(f.path === undefined ? {} : { path: f.path }),
|
|
48
|
+
...(f.line === undefined ? {} : { line: f.line }),
|
|
49
|
+
summary: f.summary,
|
|
50
|
+
lens: f.lens,
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
slice: input.slice,
|
|
54
|
+
round: input.round,
|
|
55
|
+
lenses: input.lenses,
|
|
56
|
+
sources: input.sources,
|
|
57
|
+
findings,
|
|
58
|
+
verdict: input.verdict,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -30,11 +30,14 @@ import { CONFIG_FILE, loadConfig } from "../state/config.ts";
|
|
|
30
30
|
import { availableModels } from "../state/models-command.ts";
|
|
31
31
|
import type { SessionState } from "../state/session-state.ts";
|
|
32
32
|
import { snapshotDiff } from "./digest.ts";
|
|
33
|
+
import type { SubmissionStore } from "./submissions.ts";
|
|
33
34
|
|
|
34
35
|
export type ReviewToolDeps = {
|
|
35
36
|
state: SessionState;
|
|
36
37
|
jev: (ctx: ExtensionContext) => Jev;
|
|
37
38
|
exec: Exec;
|
|
39
|
+
/** Results reviewers submitted with `devsys_submit_review`, read by `devsys_review_record`. */
|
|
40
|
+
submissions: SubmissionStore;
|
|
38
41
|
now?: () => Date;
|
|
39
42
|
};
|
|
40
43
|
|
|
@@ -55,6 +58,9 @@ const sliceOf = (given: string | undefined, state: SessionState): SliceRef | und
|
|
|
55
58
|
const NO_SLICE =
|
|
56
59
|
"no slice: pass `slice`, or start work on a slice first (there is no active slice)";
|
|
57
60
|
|
|
61
|
+
const NO_RESULT = (slice: string, round: number): string =>
|
|
62
|
+
`no result for round ${round} of "${slice}": the reviewer submits it with devsys_submit_review before you record, or you pass its packet markdown in \`packets\``;
|
|
63
|
+
|
|
58
64
|
const StartParameters = Type.Object({
|
|
59
65
|
slice: Type.Optional(
|
|
60
66
|
Type.String({ description: "Slice being reviewed; defaults to the active slice." }),
|
|
@@ -120,6 +126,7 @@ function spawnPayload(input: {
|
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
type Reply = ReturnType<typeof reply>;
|
|
129
|
+
const replyText = (r: Reply): string => r.content.map((c) => c.text).join("\n");
|
|
123
130
|
type Config = Extract<Awaited<ReturnType<typeof loadConfig>>, { ok: true }>["value"];
|
|
124
131
|
type Snapshot = Extract<Awaited<ReturnType<typeof snapshotDiff>>, { ok: true }>["value"];
|
|
125
132
|
type Prepared = { slice: SliceRef; config: Config; range: string; snap: Snapshot };
|
|
@@ -181,6 +188,8 @@ async function startRound(
|
|
|
181
188
|
const required = requiredRounds(p.config);
|
|
182
189
|
const existing = reviewOf(deps.state.get(), p.slice) ?? startReview(p.slice, required);
|
|
183
190
|
const review: ReviewState = { ...existing, required };
|
|
191
|
+
// A new round only sees what reviewers submit after it began.
|
|
192
|
+
deps.submissions.clear(p.slice);
|
|
184
193
|
deps.state.update((s) => upsertReview(s, review));
|
|
185
194
|
deps.state.update((s) => afterReviewRound(s, nextAction(review, p.snap.digest), p.slice));
|
|
186
195
|
const refusal = startRefusal(review, p);
|
|
@@ -200,7 +209,7 @@ async function startRound(
|
|
|
200
209
|
[
|
|
201
210
|
`${reviewLabel(review)}; round ${round}; diff ${p.snap.digest}. ${basis}`,
|
|
202
211
|
`lenses: ${lenses.join(", ")}`,
|
|
203
|
-
`Spawn the reviewer with agent_spawn using exactly this payload, then
|
|
212
|
+
`Spawn the reviewer with agent_spawn using exactly this payload, then call devsys_review_record with slice "${p.slice}", diffDigest "${p.snap.digest}"${rangeArg}. The reviewer submits its result with devsys_submit_review, so pass no packets (only a markdown packet the reviewer returned instead goes in packets):`,
|
|
204
213
|
JSON.stringify(spawn),
|
|
205
214
|
].join("\n"),
|
|
206
215
|
);
|
|
@@ -214,7 +223,7 @@ export function createReviewStartTool(
|
|
|
214
223
|
name: "devsys_review_start",
|
|
215
224
|
label: "Start review round",
|
|
216
225
|
description:
|
|
217
|
-
"Begin a review round for a slice: computes the diff digest, chooses review lenses, and returns the agent_spawn payload for a fresh-context reviewer. Run that spawn, then
|
|
226
|
+
"Begin a review round for a slice: computes the diff digest, chooses review lenses, and returns the agent_spawn payload for a fresh-context reviewer. Run that spawn, then call devsys_review_record (the reviewer submits its result with devsys_submit_review).",
|
|
218
227
|
promptSnippet: "Start a fresh-context review round for a slice",
|
|
219
228
|
parameters: StartParameters,
|
|
220
229
|
exposure: "model-only",
|
|
@@ -243,9 +252,12 @@ const RecordParameters = Type.Object({
|
|
|
243
252
|
slice: Type.Optional(
|
|
244
253
|
Type.String({ description: "Slice reviewed; defaults to the active slice." }),
|
|
245
254
|
),
|
|
246
|
-
packets: Type.
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
packets: Type.Optional(
|
|
256
|
+
Type.Array(Type.String(), {
|
|
257
|
+
description:
|
|
258
|
+
"Reviewer packets, verbatim markdown, for a reviewer that could not call devsys_submit_review. Results submitted through that tool for this round are read without being passed.",
|
|
259
|
+
}),
|
|
260
|
+
),
|
|
249
261
|
diffRange: Type.Optional(
|
|
250
262
|
Type.String({ description: "The range that was reviewed; defaults to HEAD." }),
|
|
251
263
|
),
|
|
@@ -275,15 +287,22 @@ async function adjusted(
|
|
|
275
287
|
return { findings: out, notes };
|
|
276
288
|
}
|
|
277
289
|
|
|
290
|
+
const lensKey = (p: ReviewPacket): string => [...p.lenses].sort().join("\u0000");
|
|
291
|
+
|
|
292
|
+
/** Submitted results win over a markdown packet for the same lenses (a reviewer that did both). */
|
|
293
|
+
function mergeResults(
|
|
294
|
+
markdown: readonly ReviewPacket[],
|
|
295
|
+
submitted: readonly ReviewPacket[],
|
|
296
|
+
): { packets: ReviewPacket[]; dropped: number } {
|
|
297
|
+
const taken = new Set(submitted.map(lensKey));
|
|
298
|
+
const kept = markdown.filter((m) => !taken.has(lensKey(m)));
|
|
299
|
+
return { packets: [...kept, ...submitted], dropped: markdown.length - kept.length };
|
|
300
|
+
}
|
|
301
|
+
|
|
278
302
|
/** Parses every packet, or the reply naming the first malformed one. */
|
|
279
303
|
function parsePackets(
|
|
280
304
|
raw: readonly string[],
|
|
281
305
|
): { ok: true; value: ReviewPacket[] } | { ok: false; reply: Reply } {
|
|
282
|
-
if (raw.length === 0)
|
|
283
|
-
return {
|
|
284
|
-
ok: false,
|
|
285
|
-
reply: reply("no packets: pass the reviewer's packet markdown in `packets`", true),
|
|
286
|
-
};
|
|
287
306
|
const packets: ReviewPacket[] = [];
|
|
288
307
|
for (const [i, text] of raw.entries()) {
|
|
289
308
|
const parsed = parseReviewPacket(text);
|
|
@@ -359,7 +378,7 @@ export function createReviewRecordTool(
|
|
|
359
378
|
name: "devsys_review_record",
|
|
360
379
|
label: "Record review round",
|
|
361
380
|
description:
|
|
362
|
-
"Record one review round from the reviewer's
|
|
381
|
+
"Record one review round from the reviewer's submitted result (or markdown packets): reads findings, lets Jev adjust severity when confident, and returns the clean-round count and the next action.",
|
|
363
382
|
promptSnippet: "Record a reviewer packet as a review round",
|
|
364
383
|
parameters: RecordParameters,
|
|
365
384
|
exposure: "direct",
|
|
@@ -370,9 +389,23 @@ export function createReviewRecordTool(
|
|
|
370
389
|
_onUpdate,
|
|
371
390
|
ctx: ExtensionContext,
|
|
372
391
|
) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
392
|
+
const slice = sliceOf(params.slice, deps.state);
|
|
393
|
+
if (slice === undefined) return reply(NO_SLICE, true);
|
|
394
|
+
const round = (reviewOf(deps.state.get(), slice)?.rounds.length ?? 0) + 1;
|
|
395
|
+
const markdown = parsePackets(params.packets ?? []);
|
|
396
|
+
if (!markdown.ok) {
|
|
397
|
+
return deps.submissions.forRound(slice, round).length === 0
|
|
398
|
+
? markdown.reply
|
|
399
|
+
: reply(
|
|
400
|
+
`${replyText(markdown.reply)}. A submitted result for round ${round} is waiting: omit \`packets\` to record it.`,
|
|
401
|
+
true,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const { packets, dropped } = mergeResults(
|
|
405
|
+
markdown.value,
|
|
406
|
+
deps.submissions.forRound(slice, round),
|
|
407
|
+
);
|
|
408
|
+
if (packets.length === 0) return reply(NO_RESULT(slice, round), true);
|
|
376
409
|
const prepared = await prepare(
|
|
377
410
|
deps,
|
|
378
411
|
ctx,
|
|
@@ -386,7 +419,14 @@ export function createReviewRecordTool(
|
|
|
386
419
|
true,
|
|
387
420
|
);
|
|
388
421
|
}
|
|
389
|
-
|
|
422
|
+
const recorded = await recordRound(deps, ctx, prepared.value, packets);
|
|
423
|
+
if (recorded.isError === true) return recorded;
|
|
424
|
+
deps.submissions.clear(slice);
|
|
425
|
+
return dropped === 0
|
|
426
|
+
? recorded
|
|
427
|
+
: reply(
|
|
428
|
+
`${replyText(recorded)}\nNote: ${dropped} markdown packet(s) for lenses already submitted were dropped; the submitted result is the one recorded.`,
|
|
429
|
+
);
|
|
390
430
|
},
|
|
391
431
|
};
|
|
392
432
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReviewPacket } from "../core/review-packet.ts";
|
|
2
|
+
|
|
3
|
+
/** Reviewer results submitted through `devsys_submit_review`, waiting for `devsys_review_record`. */
|
|
4
|
+
export type SubmissionStore = {
|
|
5
|
+
/**
|
|
6
|
+
* Keep a packet. One for the same slice and round that shares a lens replaces the earlier one
|
|
7
|
+
* (a corrected resubmission); packets for disjoint lenses (parallel lens reviewers) are all kept.
|
|
8
|
+
*/
|
|
9
|
+
put(packet: ReviewPacket): void;
|
|
10
|
+
forRound(slice: string, round: number): ReviewPacket[];
|
|
11
|
+
/** Drop every packet of a slice, when a new round starts or one is recorded. */
|
|
12
|
+
clear(slice: string): void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const sameRound = (a: ReviewPacket, b: ReviewPacket): boolean =>
|
|
16
|
+
a.slice === b.slice && a.round === b.round;
|
|
17
|
+
|
|
18
|
+
const sharesLens = (a: ReviewPacket, b: ReviewPacket): boolean =>
|
|
19
|
+
a.lenses.some((lens) => b.lenses.includes(lens));
|
|
20
|
+
|
|
21
|
+
/** In memory on purpose (ADR 0006): a round takes minutes and a lost submission is visible. */
|
|
22
|
+
export function createSubmissionStore(): SubmissionStore {
|
|
23
|
+
let packets: ReviewPacket[] = [];
|
|
24
|
+
return {
|
|
25
|
+
put(packet) {
|
|
26
|
+
packets = [
|
|
27
|
+
...packets.filter((p) => !(sameRound(p, packet) && sharesLens(p, packet))),
|
|
28
|
+
packet,
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
forRound(slice, round) {
|
|
32
|
+
return packets.filter((p) => p.slice === slice && p.round === round);
|
|
33
|
+
},
|
|
34
|
+
clear(slice) {
|
|
35
|
+
packets = packets.filter((p) => p.slice !== slice);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { SEVERITIES } from "../core/review.ts";
|
|
4
|
+
import { reviewOf } from "../core/review-flow.ts";
|
|
5
|
+
import { packetFromSubmission } from "../core/review-submission.ts";
|
|
6
|
+
import type { SliceRef } from "../core/types.ts";
|
|
7
|
+
import type { SessionState } from "../state/session-state.ts";
|
|
8
|
+
import type { SubmissionStore } from "./submissions.ts";
|
|
9
|
+
|
|
10
|
+
export const SubmitParameters = Type.Object({
|
|
11
|
+
slice: Type.String({ description: "The slice you were asked to review, exactly as given." }),
|
|
12
|
+
round: Type.Integer({ minimum: 1, description: "The round number you were asked to review." }),
|
|
13
|
+
lenses: Type.Array(Type.String({ minLength: 1 }), {
|
|
14
|
+
minItems: 1,
|
|
15
|
+
description: "Every lens you reviewed through.",
|
|
16
|
+
}),
|
|
17
|
+
sources: Type.Array(Type.String(), {
|
|
18
|
+
description: "What you inspected (path:line ranges), and what you checked and refuted.",
|
|
19
|
+
}),
|
|
20
|
+
findings: Type.Array(
|
|
21
|
+
Type.Object({
|
|
22
|
+
lens: Type.String({ minLength: 1, description: "One of `lenses`." }),
|
|
23
|
+
severity: Type.Union(SEVERITIES.map((s) => Type.Literal(s))),
|
|
24
|
+
path: Type.Optional(Type.String({ description: "File the finding is about." })),
|
|
25
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
26
|
+
summary: Type.String({
|
|
27
|
+
minLength: 1,
|
|
28
|
+
description: "One sentence: the defect, why it matters, and the smallest repair.",
|
|
29
|
+
}),
|
|
30
|
+
}),
|
|
31
|
+
{ description: "Empty when there is nothing to report." },
|
|
32
|
+
),
|
|
33
|
+
verdict: Type.Union([Type.Literal("no-blocking"), Type.Literal("blocking")], {
|
|
34
|
+
description: "`blocking` when any finding is blocking or should-fix, otherwise `no-blocking`.",
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type SubmitDeps = { state: SessionState; submissions: SubmissionStore };
|
|
39
|
+
|
|
40
|
+
const reply = (text: string, isError = false) => ({
|
|
41
|
+
content: [{ type: "text" as const, text }],
|
|
42
|
+
details: undefined,
|
|
43
|
+
isError,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function submit(deps: SubmitDeps, params: Static<typeof SubmitParameters>) {
|
|
47
|
+
const state = deps.state.get();
|
|
48
|
+
const slice = params.slice as SliceRef;
|
|
49
|
+
const review = reviewOf(state, slice);
|
|
50
|
+
if (review === undefined && state.activeSlice !== slice) {
|
|
51
|
+
return reply(
|
|
52
|
+
`unknown-slice: "${params.slice}" is neither the active slice nor under review; use the slice name exactly as given in your task`,
|
|
53
|
+
true,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const expected = (review?.rounds.length ?? 0) + 1;
|
|
57
|
+
if (params.round !== expected) {
|
|
58
|
+
return reply(
|
|
59
|
+
`wrong-round: this is for round ${params.round}, but round ${expected} of "${params.slice}" is the one being reviewed; use the round number from your task`,
|
|
60
|
+
true,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const packet = packetFromSubmission(params);
|
|
64
|
+
if ("id" in packet) return reply(`${packet.id}: ${packet.message}`, true);
|
|
65
|
+
deps.submissions.put(packet);
|
|
66
|
+
return reply(
|
|
67
|
+
`submitted: ${params.slice} round ${params.round}, lenses ${params.lenses.join(", ")}, verdict ${params.verdict}, ${params.findings.length} finding(s). End with one short line.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `devsys_submit_review`: a reviewer hands over its packet as validated arguments (ADR 0006). */
|
|
72
|
+
export function createSubmitReviewTool(deps: SubmitDeps): ToolDefinition<typeof SubmitParameters> {
|
|
73
|
+
return {
|
|
74
|
+
name: "devsys_submit_review",
|
|
75
|
+
label: "Submit review result",
|
|
76
|
+
description:
|
|
77
|
+
"For reviewer subagents only: submit your review result as structured arguments. A result that contradicts itself or is for another round is refused with an error id; fix it and submit again. Then end with one short line.",
|
|
78
|
+
promptSnippet: "Submit a review result (reviewer subagents)",
|
|
79
|
+
parameters: SubmitParameters,
|
|
80
|
+
exposure: "direct",
|
|
81
|
+
execute(_id, params: Static<typeof SubmitParameters>) {
|
|
82
|
+
return Promise.resolve(submit(deps, params));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|