@yagni-app/code-staging 1.2.6-staging.1952.1 → 1.2.6-staging.1958.1
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/dist/extension/branding.d.ts +21 -1
- package/dist/extension/branding.js +53 -12
- package/dist/extension/index.js +5 -0
- package/dist/extension/pipeline/changeScan.d.ts +62 -0
- package/dist/extension/pipeline/changeScan.js +307 -0
- package/dist/extension/pipeline/cloud.d.ts +205 -0
- package/dist/extension/pipeline/cloud.js +382 -0
- package/dist/extension/pipeline/continuation.d.ts +7 -3
- package/dist/extension/pipeline/continuation.js +29 -13
- package/dist/extension/pipeline/findings.d.ts +9 -4
- package/dist/extension/pipeline/findings.js +25 -10
- package/dist/extension/pipeline/finish.d.ts +11 -1
- package/dist/extension/pipeline/finish.js +51 -5
- package/dist/extension/pipeline/headlessGo.d.ts +20 -1
- package/dist/extension/pipeline/headlessGo.js +205 -33
- package/dist/extension/pipeline/orchestrator.d.ts +8 -2
- package/dist/extension/pipeline/orchestrator.js +249 -68
- package/dist/extension/pipeline/personas.d.ts +8 -3
- package/dist/extension/pipeline/personas.js +42 -7
- package/dist/extension/pipeline/planOutput.d.ts +42 -0
- package/dist/extension/pipeline/planOutput.js +94 -0
- package/dist/extension/pipeline/stages.d.ts +34 -5
- package/dist/extension/pipeline/stages.js +136 -7
- package/dist/extension/pipeline/tierCap.d.ts +22 -1
- package/dist/extension/pipeline/tierCap.js +48 -1
- package/dist/extension/pipeline/types.d.ts +31 -0
- package/dist/extension/pipeline/types.js +2 -0
- package/dist/extension/pipeline/verify.d.ts +33 -0
- package/dist/extension/pipeline/verify.js +101 -9
- package/package.json +2 -2
|
@@ -37,6 +37,8 @@ export declare const PERSONA_BODIES: Record<string, string>;
|
|
|
37
37
|
* measurement.
|
|
38
38
|
*/
|
|
39
39
|
export declare const BLIND_PERSONA_BODIES: Record<string, string>;
|
|
40
|
+
/** The findings contract with the lens column the one reviewer writes (`parseFindings` `lensColumn`). */
|
|
41
|
+
export declare const ONE_REVIEWER_FINDINGS_CONTRACT: string;
|
|
40
42
|
/**
|
|
41
43
|
* The per-workstream clause appended to a fan-out builder's (or fixer's) persona:
|
|
42
44
|
* the files it owns, what its siblings own, and the shared-tree discipline. With
|
|
@@ -46,9 +48,12 @@ export declare const BLIND_PERSONA_BODIES: Record<string, string>;
|
|
|
46
48
|
export declare function builderPersonaClause(workstream?: PartitionWorkstream, siblings?: PartitionWorkstream[]): string;
|
|
47
49
|
/**
|
|
48
50
|
* Resolve the system-prompt body for a stage. For non-review stages this is the
|
|
49
|
-
* role body; for the review stage it appends the lens-specific clause
|
|
50
|
-
*
|
|
51
|
-
*
|
|
51
|
+
* role body; for the review stage it appends the lens-specific clause (or, with
|
|
52
|
+
* no lens, the one-reviewer clause covering all three) and the required
|
|
53
|
+
* findings output contract so the reviewer's output parses. A stage carrying a
|
|
54
|
+
* `personaClause` (the fan-out builders) gets it appended last, and one
|
|
55
|
+
* carrying a `persona` (the cloud plan stage's host persona) runs with that in
|
|
56
|
+
* place of its agent's body.
|
|
52
57
|
*
|
|
53
58
|
* `grounded` defaults true (every real /go stage). The M6 eval passes
|
|
54
59
|
* `grounded: false` to select the grounding-free bodies + lens clauses for the
|
|
@@ -62,6 +62,15 @@ Complete the task fully — do not gold-plate, but do not leave it half-done. Fi
|
|
|
62
62
|
Budget discipline: you have a hard output budget, and a plan that gets cut off mid-thought is worth less than a short plan that ships. Explore only until you can name the files and the steps — do not read broadly for completeness, and do not re-verify what you have already established. Aim for 5-10 short steps; the worker fills small gaps from the ticket. When in doubt, write the plan NOW.
|
|
63
63
|
|
|
64
64
|
Keep it concrete; the worker executes it verbatim.`;
|
|
65
|
+
/**
|
|
66
|
+
* The cloud plan stage's default body, used when the host sent no
|
|
67
|
+
* `plan-persona.md`. Its output contract rides the stage's task, so a host
|
|
68
|
+
* persona that replaces this body keeps it.
|
|
69
|
+
*/
|
|
70
|
+
const CLOUD_PLANNER_ROLE = `You are a planning specialist. You plan one ticket in the repository checked out in your working directory: it is installed, and its commands run. Your plan is what a builder executes, and your questions are what a person answers before anything is built. You do not change files. A fact you can settle by reading or running code is settled, not asked.`;
|
|
71
|
+
const CLOUD_PLANNER_GROUNDING = `You are grounded. Call ask_yagni before inferring a convention or making any organization- or codebase-specific assumption. To check a fact outside this repository (what is deployed, what the data holds, what a ticket or page says), call team_read with the Team's connected tools before you ask a person.`;
|
|
72
|
+
const CLOUD_PLANNER_FINISH = `Complete the task fully: do not gold-plate, but do not leave it half-done. Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format the task gives. Explore until you can name the files and the steps, then write the plan.`;
|
|
73
|
+
const CLOUD_PLANNER_BODY = [CLOUD_PLANNER_ROLE, CLOUD_PLANNER_GROUNDING, CLOUD_PLANNER_FINISH].join("\n\n");
|
|
65
74
|
const WORKER_BODY = `You are a worker with full capabilities, operating in an isolated context to implement a plan. Work autonomously and use the tools as needed.
|
|
66
75
|
|
|
67
76
|
You are grounded. Call ask_yagni before guessing about anything organization- or codebase-specific. To check a fact outside this repository (what is deployed, what the data holds, what a ticket or page says), call team_read with the Team's connected tools before you assume or stop. Treat a confirmed answer as settled; when an answer is an unverified assumption or an inference and your change leans on it, say so in your Notes so the reviewer knows what to check. When the plan did not settle a consequential call you are forced to make (product intent, architecture, or a team convention another engineer would need to know), call record_decision so the company's decision corpus captures it and the next agent inherits the call instead of re-litigating it. Recorded decisions are shared across the whole organization, so the bar is consequence: never record taste, styling, naming, formatting, or a restatement of the code change; when in doubt, do not record. When ask_yagni reports no recorded position, follow its instruction and record the assumption you proceed on only if it meets that bar.
|
|
@@ -224,6 +233,7 @@ Bad (too long): Investigate and fix the login button not responding on mobile de
|
|
|
224
233
|
export const PERSONA_BODIES = {
|
|
225
234
|
scout: SCOUT_BODY,
|
|
226
235
|
planner: PLANNER_BODY,
|
|
236
|
+
cloud_planner: CLOUD_PLANNER_BODY,
|
|
227
237
|
worker: WORKER_BODY,
|
|
228
238
|
reviewer: REVIEWER_BODY,
|
|
229
239
|
advisor: ADVISOR_BODY,
|
|
@@ -320,6 +330,7 @@ export const BLIND_PERSONA_BODIES = {
|
|
|
320
330
|
scout: SCOUT_BLIND,
|
|
321
331
|
prefix: PREFIX_BODY, // grounding-free by construction (no grounding text at all)
|
|
322
332
|
planner: PLANNER_BLIND,
|
|
333
|
+
cloud_planner: [CLOUD_PLANNER_ROLE, CLOUD_PLANNER_FINISH].join("\n\n"),
|
|
323
334
|
worker: WORKER_BLIND,
|
|
324
335
|
reviewer: REVIEWER_BLIND,
|
|
325
336
|
advisor: ADVISOR_BLIND,
|
|
@@ -350,6 +361,21 @@ SEVERITY | file:line | message
|
|
|
350
361
|
SEVERITY is one of: critical, high, medium, low, needs_person. needs_person means only a person can do it (a named sign-off, an account, a credential, a call outside the code); it does not block, and the person sees it on the pull request. Use \`file:line\` when you can point to a location; otherwise give a short location or omit it. Put one finding per line and nothing else inside the block. If you found no problems, emit an empty \`\`\`findings block. Only critical and high findings block the change.
|
|
351
362
|
|
|
352
363
|
Be terse. Do not narrate your process, restate the diff, or quote code back at length — spend your output on the findings themselves, at most a few short paragraphs before the block. Cap the block at the 12 most important findings, most severe first; a review cut off by its own length limit helps nobody.`;
|
|
364
|
+
/**
|
|
365
|
+
* A review stage with no lens is the round's only reviewer (the cloud lane):
|
|
366
|
+
* it judges through all three angles and names each finding's angle, so the
|
|
367
|
+
* full lane's rule holds per finding (a business-fit finding never blocks).
|
|
368
|
+
*/
|
|
369
|
+
function oneReviewerClause(clauses) {
|
|
370
|
+
return [
|
|
371
|
+
"You are this change's only reviewer, so judge it through all three angles below.",
|
|
372
|
+
clauses.correctness,
|
|
373
|
+
clauses.business_fit,
|
|
374
|
+
clauses.does_it_hold,
|
|
375
|
+
].join("\n");
|
|
376
|
+
}
|
|
377
|
+
/** The findings contract with the lens column the one reviewer writes (`parseFindings` `lensColumn`). */
|
|
378
|
+
export const ONE_REVIEWER_FINDINGS_CONTRACT = FINDINGS_CONTRACT.replace("SEVERITY | file:line | message", "SEVERITY | LENS | file:line | message").replace("Only critical and high findings block the change.", "LENS is the angle the finding is about: correctness, business_fit or does_it_hold. Only critical and high findings on correctness or does_it_hold block the change; a business_fit finding is left for the pull request's review.");
|
|
353
379
|
/**
|
|
354
380
|
* The blind lens clauses (M6): correctness + does_it_hold are not grounding
|
|
355
381
|
* specific, so they are reused verbatim; business_fit drops review_business_match
|
|
@@ -383,7 +409,10 @@ export function builderPersonaClause(workstream, siblings = []) {
|
|
|
383
409
|
return SHARED_TREE_RULES;
|
|
384
410
|
const parts = [
|
|
385
411
|
`You are building ONE workstream of a plan that was split across parallel builders: "${workstream.name}".`,
|
|
386
|
-
[
|
|
412
|
+
[
|
|
413
|
+
"Files you own (path prefixes; a directory covers its whole subtree):",
|
|
414
|
+
...workstream.files.map((f) => `- ${f}`),
|
|
415
|
+
].join("\n"),
|
|
387
416
|
"Create new files only inside the paths you own. Edit nothing outside them.",
|
|
388
417
|
];
|
|
389
418
|
if (siblings.length > 0) {
|
|
@@ -397,9 +426,12 @@ export function builderPersonaClause(workstream, siblings = []) {
|
|
|
397
426
|
}
|
|
398
427
|
/**
|
|
399
428
|
* Resolve the system-prompt body for a stage. For non-review stages this is the
|
|
400
|
-
* role body; for the review stage it appends the lens-specific clause
|
|
401
|
-
*
|
|
402
|
-
*
|
|
429
|
+
* role body; for the review stage it appends the lens-specific clause (or, with
|
|
430
|
+
* no lens, the one-reviewer clause covering all three) and the required
|
|
431
|
+
* findings output contract so the reviewer's output parses. A stage carrying a
|
|
432
|
+
* `personaClause` (the fan-out builders) gets it appended last, and one
|
|
433
|
+
* carrying a `persona` (the cloud plan stage's host persona) runs with that in
|
|
434
|
+
* place of its agent's body.
|
|
403
435
|
*
|
|
404
436
|
* `grounded` defaults true (every real /go stage). The M6 eval passes
|
|
405
437
|
* `grounded: false` to select the grounding-free bodies + lens clauses for the
|
|
@@ -408,13 +440,16 @@ export function builderPersonaClause(workstream, siblings = []) {
|
|
|
408
440
|
export function personaBody(stage, opts = {}) {
|
|
409
441
|
const grounded = opts.grounded !== false;
|
|
410
442
|
const bodies = grounded ? PERSONA_BODIES : BLIND_PERSONA_BODIES;
|
|
411
|
-
const base = bodies[stage.agent];
|
|
443
|
+
const base = stage.persona?.trim() || bodies[stage.agent];
|
|
412
444
|
if (!base)
|
|
413
445
|
throw new Error(`No persona body for agent "${stage.agent}"`);
|
|
414
446
|
const parts = [base];
|
|
415
447
|
if (stage.id === "review") {
|
|
416
|
-
const
|
|
417
|
-
|
|
448
|
+
const clauses = grounded ? LENS_CLAUSES : BLIND_LENS_CLAUSES;
|
|
449
|
+
if (stage.lens)
|
|
450
|
+
parts.push(clauses[stage.lens], FINDINGS_CONTRACT);
|
|
451
|
+
else
|
|
452
|
+
parts.push(oneReviewerClause(clauses), ONE_REVIEWER_FINDINGS_CONTRACT);
|
|
418
453
|
}
|
|
419
454
|
if (stage.personaClause?.trim())
|
|
420
455
|
parts.push(stage.personaClause.trim());
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloud plan stage's outputs (one-attempt lane, spec decision 4): the plan
|
|
3
|
+
* as `plan.md` and its ```plan-json block as `plan.json`, the two files the
|
|
4
|
+
* host collects and takes to the plan gate. The block's contract is
|
|
5
|
+
* `PLAN_JSON_CONTRACT` in stages.ts.
|
|
6
|
+
*
|
|
7
|
+
* The session only shapes the file. The questions pass through as the model
|
|
8
|
+
* wrote them: the host normalizes, settles and grounds them with the same
|
|
9
|
+
* functions it runs on its own plan (`groundSessionPlan` in the backend's
|
|
10
|
+
* builder/planQuestions.ts), so there is one normalizer, on the host.
|
|
11
|
+
*/
|
|
12
|
+
/** `plan.json`, version 1, as the host reads it. */
|
|
13
|
+
export interface PlanFile {
|
|
14
|
+
version: 1;
|
|
15
|
+
/** Each the shape `packages/shared/src/planQuestions.ts` reads, unchanged. */
|
|
16
|
+
questions: unknown[];
|
|
17
|
+
decisionsCited: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
why: string;
|
|
20
|
+
}>;
|
|
21
|
+
checks: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
command: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** The package folders the plan touches, repo-relative. */
|
|
27
|
+
paths: string[];
|
|
28
|
+
}
|
|
29
|
+
/** The plan's ```plan-json block as an object, or null when it has none that parses. */
|
|
30
|
+
export declare function extractPlanJson(text: string): Record<string, unknown> | null;
|
|
31
|
+
/** The plan for a person to read: the text without its machine-readable block. */
|
|
32
|
+
export declare function planMarkdown(text: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* `plan.json` from the plan's text. `found` is false when the plan carried no
|
|
35
|
+
* block that parses: the file then holds no questions, and `paths` falls back
|
|
36
|
+
* to the package folders read off the plan's own text.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildPlanFile(planText: string, fallbackPaths: () => string[]): {
|
|
39
|
+
file: PlanFile;
|
|
40
|
+
found: boolean;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=planOutput.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloud plan stage's outputs (one-attempt lane, spec decision 4): the plan
|
|
3
|
+
* as `plan.md` and its ```plan-json block as `plan.json`, the two files the
|
|
4
|
+
* host collects and takes to the plan gate. The block's contract is
|
|
5
|
+
* `PLAN_JSON_CONTRACT` in stages.ts.
|
|
6
|
+
*
|
|
7
|
+
* The session only shapes the file. The questions pass through as the model
|
|
8
|
+
* wrote them: the host normalizes, settles and grounds them with the same
|
|
9
|
+
* functions it runs on its own plan (`groundSessionPlan` in the backend's
|
|
10
|
+
* builder/planQuestions.ts), so there is one normalizer, on the host.
|
|
11
|
+
*/
|
|
12
|
+
/** Matches every ```plan-json block; the last one that reads as an object wins. */
|
|
13
|
+
const PLAN_JSON_BLOCK = /```plan-json[^\n]*\n([\s\S]*?)```/gi;
|
|
14
|
+
/** The host keeps at most this many questions (`MAX_QUESTIONS`); the rest are noise. */
|
|
15
|
+
const MAX_PLAN_QUESTIONS = 20;
|
|
16
|
+
const MAX_LIST = 40;
|
|
17
|
+
const MAX_TEXT = 500;
|
|
18
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
/** The plan's ```plan-json block as an object, or null when it has none that parses. */
|
|
20
|
+
export function extractPlanJson(text) {
|
|
21
|
+
let found = null;
|
|
22
|
+
for (const match of text.matchAll(PLAN_JSON_BLOCK)) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(match[1] ?? "");
|
|
25
|
+
if (isRecord(parsed))
|
|
26
|
+
found = parsed;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// A block that does not parse is skipped; an earlier or later one may.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
/** The plan for a person to read: the text without its machine-readable block. */
|
|
35
|
+
export function planMarkdown(text) {
|
|
36
|
+
return `${text.replace(PLAN_JSON_BLOCK, "").trim()}\n`;
|
|
37
|
+
}
|
|
38
|
+
function text(value, max = MAX_TEXT) {
|
|
39
|
+
if (typeof value !== "string")
|
|
40
|
+
return null;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
return trimmed ? trimmed.slice(0, max) : null;
|
|
43
|
+
}
|
|
44
|
+
function list(value) {
|
|
45
|
+
return Array.isArray(value) ? value : [];
|
|
46
|
+
}
|
|
47
|
+
/** A repo-relative folder the plan names, or null when it is not one. */
|
|
48
|
+
function repoPath(value) {
|
|
49
|
+
const path = text(value)
|
|
50
|
+
?.replace(/^\.\/+/, "")
|
|
51
|
+
.replace(/\/+$/, "");
|
|
52
|
+
if (!path || path.startsWith("/") || path.split("/").includes(".."))
|
|
53
|
+
return null;
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `plan.json` from the plan's text. `found` is false when the plan carried no
|
|
58
|
+
* block that parses: the file then holds no questions, and `paths` falls back
|
|
59
|
+
* to the package folders read off the plan's own text.
|
|
60
|
+
*/
|
|
61
|
+
export function buildPlanFile(planText, fallbackPaths) {
|
|
62
|
+
const block = extractPlanJson(planText);
|
|
63
|
+
const questions = list(block?.questions)
|
|
64
|
+
.filter((entry) => typeof entry === "string" || isRecord(entry))
|
|
65
|
+
.slice(0, MAX_PLAN_QUESTIONS);
|
|
66
|
+
const decisionsCited = [];
|
|
67
|
+
for (const entry of list(block?.decisionsCited)) {
|
|
68
|
+
const id = isRecord(entry) ? text(entry.id, 100) : null;
|
|
69
|
+
if (!id || decisionsCited.some((cited) => cited.id === id))
|
|
70
|
+
continue;
|
|
71
|
+
decisionsCited.push({ id, why: (isRecord(entry) && text(entry.why)) || "" });
|
|
72
|
+
if (decisionsCited.length === MAX_LIST)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const checks = [];
|
|
76
|
+
for (const entry of list(block?.checks)) {
|
|
77
|
+
const command = isRecord(entry) ? text(entry.command) : null;
|
|
78
|
+
if (!isRecord(entry) || !command)
|
|
79
|
+
continue;
|
|
80
|
+
checks.push({
|
|
81
|
+
name: text(entry.name, 100) ?? command.slice(0, 100),
|
|
82
|
+
command,
|
|
83
|
+
cwd: repoPath(entry.cwd) ?? ".",
|
|
84
|
+
});
|
|
85
|
+
if (checks.length === MAX_LIST)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const named = list(block?.paths)
|
|
89
|
+
.map(repoPath)
|
|
90
|
+
.filter((path) => path !== null);
|
|
91
|
+
const paths = [...new Set(named.length > 0 ? named : fallbackPaths())].slice(0, MAX_LIST);
|
|
92
|
+
return { file: { version: 1, questions, decisionsCited, checks, paths }, found: block !== null };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=planOutput.js.map
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* rail, and the tier cap treat the whole diamond as today's single stage.
|
|
28
28
|
*/
|
|
29
29
|
import type { PartitionWorkstream } from "./fanout.js";
|
|
30
|
-
import type
|
|
30
|
+
import { type ModelTier, type PipelineLane, type PipelineStage, type ReviewLens } from "./types.js";
|
|
31
31
|
/** The three adversarial review angles, fanned out per round (spec §3). */
|
|
32
32
|
export declare const REVIEW_LENSES: ReviewLens[];
|
|
33
33
|
/**
|
|
@@ -48,6 +48,34 @@ export declare function reviewStage(): PipelineStage;
|
|
|
48
48
|
* the identity selector and the extension point for v2 complexity-lanes.
|
|
49
49
|
*/
|
|
50
50
|
export declare function selectStages(_lane?: "full"): PipelineStage[];
|
|
51
|
+
/** Interactive /go and mission builds: the diamond, three lenses, two rounds. */
|
|
52
|
+
export declare const FULL_LANE: PipelineLane;
|
|
53
|
+
/** The cloud lane's cap: one reviewer per round, so more rounds than the full lane. */
|
|
54
|
+
export declare const CLOUD_MAX_REVIEW_ROUNDS = 4;
|
|
55
|
+
/** The tool every cloud plan stage calls for the decisions a person already made on this repository. */
|
|
56
|
+
export declare const DECISIONS_READ_TOOL = "yagni_decisions_read_for_repository";
|
|
57
|
+
/**
|
|
58
|
+
* The plan contract the cloud plan stage ends on. The pipeline writes
|
|
59
|
+
* `plan.md` (the plan without this block) and `plan.json` (this block) for
|
|
60
|
+
* the host, which grounds the questions exactly as it grounds its own plan's
|
|
61
|
+
* (`groundSessionPlan` in the backend's builder/planQuestions.ts). Each
|
|
62
|
+
* question is the shape `packages/shared/src/planQuestions.ts` reads.
|
|
63
|
+
*/
|
|
64
|
+
export declare const PLAN_JSON_CONTRACT: string;
|
|
65
|
+
/**
|
|
66
|
+
* The one-attempt lane's session (`yagni go --headless --cloud`): plan,
|
|
67
|
+
* implement, review, fix, then verify and finish, which run no model. One
|
|
68
|
+
* writer, one reviewer per round, up to four rounds. Execution runs on the
|
|
69
|
+
* strongest rung the contract allows, never a Flash model (the host's
|
|
70
|
+
* `YAGNI_GO_STAGE_TIERS` may move any stage; see tierCap.ts). The plan stage
|
|
71
|
+
* ends its own run: implement starts in a later run, on the approved plan.
|
|
72
|
+
*/
|
|
73
|
+
export declare function cloudLane(inputDir: string): PipelineLane;
|
|
74
|
+
/**
|
|
75
|
+
* The plan contract re-ask: the same cheap format-recovery pass the review
|
|
76
|
+
* lenses get, pointed at the ```plan-json block. One attempt only.
|
|
77
|
+
*/
|
|
78
|
+
export declare function planJsonReaskStage(plan: PipelineStage): PipelineStage;
|
|
51
79
|
/**
|
|
52
80
|
* A fix round's map (fixRound.ts): the same scout on the same tier, limited to
|
|
53
81
|
* the files the pull request touched and the files the round's findings name.
|
|
@@ -56,11 +84,12 @@ export declare function selectStages(_lane?: "full"): PipelineStage[];
|
|
|
56
84
|
*/
|
|
57
85
|
export declare function fixRoundMapStage(files: readonly string[], base?: PipelineStage): PipelineStage;
|
|
58
86
|
/**
|
|
59
|
-
* A fix round's builder (fixRound.ts): ONE writer at the
|
|
60
|
-
*
|
|
61
|
-
*
|
|
87
|
+
* A fix round's builder (fixRound.ts): ONE writer at the lane's fix tier
|
|
88
|
+
* (`standard` on the full lane), no partitioner, addressing the round's
|
|
89
|
+
* findings from the ticket against the map brief. The approved plan rides
|
|
90
|
+
* along as pinned context, never as work to redo.
|
|
62
91
|
*/
|
|
63
|
-
export declare function fixRoundImplementStage(base?: PipelineStage): PipelineStage;
|
|
92
|
+
export declare function fixRoundImplementStage(base?: PipelineStage, model?: ModelTier): PipelineStage;
|
|
64
93
|
/**
|
|
65
94
|
* The partitioner's caller label. Deliberately a `go:*` caller and never
|
|
66
95
|
* `subagent:*`: the server clamps `subagent:*` callers to `advanced`, which would
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { sanitizeCallerSegment } from "../config.js";
|
|
30
30
|
import { builderPersonaClause } from "./personas.js";
|
|
31
|
+
import { MAX_REVIEW_ROUNDS, } from "./types.js";
|
|
31
32
|
/** The three adversarial review angles, fanned out per round (spec §3). */
|
|
32
33
|
export const REVIEW_LENSES = ["correctness", "business_fit", "does_it_hold"];
|
|
33
34
|
/**
|
|
@@ -43,7 +44,18 @@ export const REQUIRED_LENSES = ["correctness"];
|
|
|
43
44
|
/** Read-only recon + grounding tools shared by scout and reviewer. */
|
|
44
45
|
const READ_TOOLS = ["read", "grep", "find", "ls", "bash", "prototype_read"];
|
|
45
46
|
/** Full write tools for the worker stages. */
|
|
46
|
-
const WRITE_TOOLS = [
|
|
47
|
+
const WRITE_TOOLS = [
|
|
48
|
+
"read",
|
|
49
|
+
"write",
|
|
50
|
+
"edit",
|
|
51
|
+
"bash",
|
|
52
|
+
"grep",
|
|
53
|
+
"find",
|
|
54
|
+
"ls",
|
|
55
|
+
"prototype_read",
|
|
56
|
+
"prototype_verification",
|
|
57
|
+
"prototype_verify",
|
|
58
|
+
];
|
|
47
59
|
const TICKET_CONTEXT = "Follow the approved plan and its explicit constraints, including approved answers. " +
|
|
48
60
|
"Prior summaries and review findings do not authorize expanding scope or publication " +
|
|
49
61
|
"permissions. Report conflicts requiring a scope change for approval.\n\n" +
|
|
@@ -112,6 +124,121 @@ export function reviewStage() {
|
|
|
112
124
|
export function selectStages(_lane = "full") {
|
|
113
125
|
return PIPELINE_V1;
|
|
114
126
|
}
|
|
127
|
+
/** Interactive /go and mission builds: the diamond, three lenses, two rounds. */
|
|
128
|
+
export const FULL_LANE = {
|
|
129
|
+
stages: PIPELINE_V1,
|
|
130
|
+
reviewLenses: REVIEW_LENSES,
|
|
131
|
+
maxReviewRounds: MAX_REVIEW_ROUNDS,
|
|
132
|
+
fanout: true,
|
|
133
|
+
stopAfterPlan: false,
|
|
134
|
+
skillsEveryStage: false,
|
|
135
|
+
};
|
|
136
|
+
/** The cloud lane's cap: one reviewer per round, so more rounds than the full lane. */
|
|
137
|
+
export const CLOUD_MAX_REVIEW_ROUNDS = 4;
|
|
138
|
+
/** The tool every cloud plan stage calls for the decisions a person already made on this repository. */
|
|
139
|
+
export const DECISIONS_READ_TOOL = "yagni_decisions_read_for_repository";
|
|
140
|
+
/**
|
|
141
|
+
* The plan contract the cloud plan stage ends on. The pipeline writes
|
|
142
|
+
* `plan.md` (the plan without this block) and `plan.json` (this block) for
|
|
143
|
+
* the host, which grounds the questions exactly as it grounds its own plan's
|
|
144
|
+
* (`groundSessionPlan` in the backend's builder/planQuestions.ts). Each
|
|
145
|
+
* question is the shape `packages/shared/src/planQuestions.ts` reads.
|
|
146
|
+
*/
|
|
147
|
+
export const PLAN_JSON_CONTRACT = "End your answer with a fenced ```plan-json block and nothing after it:\n" +
|
|
148
|
+
"```plan-json\n" +
|
|
149
|
+
"{\n" +
|
|
150
|
+
' "questions": [{"text": "...", "kind": "fact|decision", "oneWay": false, ' +
|
|
151
|
+
'"recommendation": {"answer": "...", "why": "...", "planChange": {"addFiles": ["path"], ' +
|
|
152
|
+
'"addSteps": [{"title": "...", "description": "..."}], "editSteps": [{"step": 1, "description": "..."}], ' +
|
|
153
|
+
'"addChecks": ["command"], "addAcceptanceCriteria": ["..."]}}, ' +
|
|
154
|
+
'"alternatives": [{"answer": "...", "why": "...", "planChange": {}}], ' +
|
|
155
|
+
'"checked": [{"status": "checked|not_checked", "text": "..."}]}],\n' +
|
|
156
|
+
' "decisionsCited": [{"id": "<the decision id the read returned>", "why": "one line"}],\n' +
|
|
157
|
+
' "checks": [{"name": "lint", "command": "pnpm lint", "cwd": "apps/web"}],\n' +
|
|
158
|
+
' "paths": ["apps/web"]\n' +
|
|
159
|
+
"}\n" +
|
|
160
|
+
"```\n" +
|
|
161
|
+
"Rules the host enforces, so getting them wrong loses the question:\n" +
|
|
162
|
+
'- "kind" is "fact" when the answer could be looked up and "decision" when it is a person\'s call. ' +
|
|
163
|
+
'A decision you would make yourself carries your recommendation and is stated, not asked; mark "oneWay": true only when it cannot be undone.\n' +
|
|
164
|
+
'- On a fact, "checked" says in at most 4 short lines what you read or ran for it and what it showed, and what you could not read. ' +
|
|
165
|
+
"A fact whose every answer plans the same work (no option carries a planChange) is stated as your assumption, not asked.\n" +
|
|
166
|
+
"- At most two alternatives per question. A planChange only adds or rewords; it never removes.\n" +
|
|
167
|
+
'- "checks" are the commands that prove the change, each run from "cwd" (repo-relative, "." for the root). ' +
|
|
168
|
+
'"paths" are the package folders the plan touches.\n' +
|
|
169
|
+
'- "decisionsCited" lists only decisions the read returned that the plan relies on. Omit what you do not use.';
|
|
170
|
+
/**
|
|
171
|
+
* The cloud plan stage's task: the input folder the host wrote, read first,
|
|
172
|
+
* then a plan proven on the installed checkout. `inputDir` is the folder the
|
|
173
|
+
* ticket file sits in.
|
|
174
|
+
*/
|
|
175
|
+
function cloudPlanTask(inputDir) {
|
|
176
|
+
const at = (file) => `${inputDir.replace(/\/+$/, "")}/${file}`;
|
|
177
|
+
return ("Plan this ticket on the repository checked out in your working directory. It is installed and its " +
|
|
178
|
+
"commands run. Nothing is built until the plan is approved, so change nothing in the repository.\n\n" +
|
|
179
|
+
"Read first:\n" +
|
|
180
|
+
`- ${at("ticket.md")}: the ticket and the evidence behind it. Open every image it names with the read tool.\n` +
|
|
181
|
+
`- ${at("design.md")} and ${at("images")}: the linked design and its frames, when they exist.\n` +
|
|
182
|
+
`- ${at("memo.md")}: what runs in this repository.\n` +
|
|
183
|
+
`- ${at("checks.json")}: the repository's checks, and which already fail on the unchanged code.\n` +
|
|
184
|
+
`- ${at("playbook.md")}: the Team's planning rules, when it exists. Always follow them.\n` +
|
|
185
|
+
`- ${at("plan-line.md")}: this Team's instructions for planning, when it exists.\n\n` +
|
|
186
|
+
"Prove facts by running code here (a check, a test, a script) before you state what the code does. " +
|
|
187
|
+
"When the work depends on another repository the ticket's Other repositories section lists, clone it " +
|
|
188
|
+
"read-only on demand with `git clone --filter=blob:none --depth 1` into ~/context/<repo> and read it there.\n\n" +
|
|
189
|
+
`Call team_read with tool "${DECISIONS_READ_TOOL}" before you ask a person anything: a question an earlier ` +
|
|
190
|
+
"decision on this repository already answers is not a question. Cite the decisions the plan relies on.\n\n" +
|
|
191
|
+
"When the change shows in a page, plan a render check the build runs (open the page in the sandbox's " +
|
|
192
|
+
"browser and take a screenshot) instead of asking a person to look at it.\n\n" +
|
|
193
|
+
"Write the plan as ## Goal (one sentence), ## Plan (numbered steps, each naming the file or function), " +
|
|
194
|
+
"## Files to modify / create, ## Checks and ## Risks.\n\n" +
|
|
195
|
+
`${PLAN_JSON_CONTRACT}\n\n` +
|
|
196
|
+
"Ticket:\n{ticket}");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The one-attempt lane's session (`yagni go --headless --cloud`): plan,
|
|
200
|
+
* implement, review, fix, then verify and finish, which run no model. One
|
|
201
|
+
* writer, one reviewer per round, up to four rounds. Execution runs on the
|
|
202
|
+
* strongest rung the contract allows, never a Flash model (the host's
|
|
203
|
+
* `YAGNI_GO_STAGE_TIERS` may move any stage; see tierCap.ts). The plan stage
|
|
204
|
+
* ends its own run: implement starts in a later run, on the approved plan.
|
|
205
|
+
*/
|
|
206
|
+
export function cloudLane(inputDir) {
|
|
207
|
+
const fix = PIPELINE_V1.find((s) => s.id === "fix");
|
|
208
|
+
if (!fix)
|
|
209
|
+
throw new Error("PIPELINE_V1 is missing the fix stage");
|
|
210
|
+
return {
|
|
211
|
+
stages: [
|
|
212
|
+
{
|
|
213
|
+
id: "plan",
|
|
214
|
+
agent: "cloud_planner",
|
|
215
|
+
model: "peak",
|
|
216
|
+
tools: ["read", "grep", "find", "ls", "bash", "ask_yagni", "team_read", "prototype_read"],
|
|
217
|
+
taskTemplate: cloudPlanTask(inputDir),
|
|
218
|
+
},
|
|
219
|
+
{ ...implementStage(), model: "advanced" },
|
|
220
|
+
reviewStage(),
|
|
221
|
+
{ ...fix, model: "advanced" },
|
|
222
|
+
],
|
|
223
|
+
reviewLenses: [],
|
|
224
|
+
maxReviewRounds: CLOUD_MAX_REVIEW_ROUNDS,
|
|
225
|
+
fanout: false,
|
|
226
|
+
stopAfterPlan: true,
|
|
227
|
+
skillsEveryStage: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The plan contract re-ask: the same cheap format-recovery pass the review
|
|
232
|
+
* lenses get, pointed at the ```plan-json block. One attempt only.
|
|
233
|
+
*/
|
|
234
|
+
export function planJsonReaskStage(plan) {
|
|
235
|
+
return {
|
|
236
|
+
...plan,
|
|
237
|
+
model: "efficient",
|
|
238
|
+
tools: ["read"],
|
|
239
|
+
taskTemplate: "Re-emit ONLY the ```plan-json block for the plan you just wrote, and nothing else:\n{previous}",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
115
242
|
/** The single map stage, as the lane defines it. */
|
|
116
243
|
function mapStage() {
|
|
117
244
|
const stage = PIPELINE_V1.find((s) => s.id === "map");
|
|
@@ -139,14 +266,15 @@ export function fixRoundMapStage(files, base = mapStage()) {
|
|
|
139
266
|
};
|
|
140
267
|
}
|
|
141
268
|
/**
|
|
142
|
-
* A fix round's builder (fixRound.ts): ONE writer at the
|
|
143
|
-
*
|
|
144
|
-
*
|
|
269
|
+
* A fix round's builder (fixRound.ts): ONE writer at the lane's fix tier
|
|
270
|
+
* (`standard` on the full lane), no partitioner, addressing the round's
|
|
271
|
+
* findings from the ticket against the map brief. The approved plan rides
|
|
272
|
+
* along as pinned context, never as work to redo.
|
|
145
273
|
*/
|
|
146
|
-
export function fixRoundImplementStage(base = implementStage()) {
|
|
274
|
+
export function fixRoundImplementStage(base = implementStage(), model = "standard") {
|
|
147
275
|
return {
|
|
148
276
|
...base,
|
|
149
|
-
model
|
|
277
|
+
model,
|
|
150
278
|
taskTemplate: TICKET_CONTEXT +
|
|
151
279
|
"This is a fix round on an open pull request. The approved plan is already built on this " +
|
|
152
280
|
"branch: do not re-plan it or rebuild it. Address every finding the ticket lists for this " +
|
|
@@ -195,7 +323,8 @@ function repoContextSection(ctx) {
|
|
|
195
323
|
if (brief)
|
|
196
324
|
sections.push(`Repo map (from this run's map stage):\n${brief}`);
|
|
197
325
|
if (memo) {
|
|
198
|
-
sections.push("Repo context (cached scoping memo; this run entered at implement, so no map stage ran):\n" +
|
|
326
|
+
sections.push("Repo context (cached scoping memo; this run entered at implement, so no map stage ran):\n" +
|
|
327
|
+
memo);
|
|
199
328
|
}
|
|
200
329
|
if (sections.length > 0)
|
|
201
330
|
return sections.join("\n\n");
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* A cap only ever lowers a tier: naming a cap ABOVE a stage's own tier never
|
|
16
16
|
* promotes it (the pipeline's routing is the judgment; the cap is a budget).
|
|
17
17
|
*/
|
|
18
|
-
import type { ModelTier } from "./types.js";
|
|
18
|
+
import type { ModelTier, PipelineStage } from "./types.js";
|
|
19
19
|
/** The documented env var. Eval/smoke lanes only; unset everywhere else. */
|
|
20
20
|
export declare const TIER_CAP_ENV = "YAGNI_GO_TIER_CAP";
|
|
21
21
|
/**
|
|
@@ -29,4 +29,25 @@ export declare function parseTierCap(raw: string | undefined): ModelTier | undef
|
|
|
29
29
|
export declare function clampTier(tier: ModelTier, cap: ModelTier | undefined): ModelTier;
|
|
30
30
|
/** Resolve the run's cap from an environment (the child env, or process.env). */
|
|
31
31
|
export declare function resolveTierCap(env: NodeJS.ProcessEnv): ModelTier | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The host's per-stage tiers for a cloud session (spec decision 7): a JSON map
|
|
34
|
+
* `{ "plan": "peak", "implement": "advanced", "review": "peak", "fix": "advanced" }`
|
|
35
|
+
* the backend resolves per workspace (`resolveStageTiers` in the backend's
|
|
36
|
+
* llm/stageTiers.ts). A stage the map leaves out keeps its compiled tier;
|
|
37
|
+
* verify runs no model. The cap above still clamps the result.
|
|
38
|
+
*/
|
|
39
|
+
export declare const STAGE_TIERS_ENV = "YAGNI_GO_STAGE_TIERS";
|
|
40
|
+
/** The stages the host may move. */
|
|
41
|
+
export type TieredStage = "plan" | "implement" | "review" | "fix";
|
|
42
|
+
/**
|
|
43
|
+
* Parse the env map. Unset or empty is no map. A value that is not a JSON
|
|
44
|
+
* object, or an entry that names an unknown stage or tier, is ignored and
|
|
45
|
+
* named in `ignored`, so a typo never moves a stage and the run says so.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseStageTiers(raw: string | undefined): {
|
|
48
|
+
tiers: Partial<Record<TieredStage, ModelTier>>;
|
|
49
|
+
ignored: string[];
|
|
50
|
+
};
|
|
51
|
+
/** The stages with the map's tiers applied; a stage the map leaves out is unchanged. */
|
|
52
|
+
export declare function applyStageTiers(stages: readonly PipelineStage[], tiers: Partial<Record<TieredStage, ModelTier>>): PipelineStage[];
|
|
32
53
|
//# sourceMappingURL=tierCap.d.ts.map
|
|
@@ -42,7 +42,9 @@ export function parseTierCap(raw) {
|
|
|
42
42
|
const normalized = raw?.trim().toLowerCase();
|
|
43
43
|
if (!normalized)
|
|
44
44
|
return undefined;
|
|
45
|
-
return CAPPABLE.includes(normalized)
|
|
45
|
+
return CAPPABLE.includes(normalized)
|
|
46
|
+
? normalized
|
|
47
|
+
: undefined;
|
|
46
48
|
}
|
|
47
49
|
/** The effective tier for a stage under a cap: never above it, never promoted. */
|
|
48
50
|
export function clampTier(tier, cap) {
|
|
@@ -54,4 +56,49 @@ export function clampTier(tier, cap) {
|
|
|
54
56
|
export function resolveTierCap(env) {
|
|
55
57
|
return parseTierCap(env[TIER_CAP_ENV]);
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The host's per-stage tiers for a cloud session (spec decision 7): a JSON map
|
|
61
|
+
* `{ "plan": "peak", "implement": "advanced", "review": "peak", "fix": "advanced" }`
|
|
62
|
+
* the backend resolves per workspace (`resolveStageTiers` in the backend's
|
|
63
|
+
* llm/stageTiers.ts). A stage the map leaves out keeps its compiled tier;
|
|
64
|
+
* verify runs no model. The cap above still clamps the result.
|
|
65
|
+
*/
|
|
66
|
+
export const STAGE_TIERS_ENV = "YAGNI_GO_STAGE_TIERS";
|
|
67
|
+
const TIERED_STAGES = ["plan", "implement", "review", "fix"];
|
|
68
|
+
/**
|
|
69
|
+
* Parse the env map. Unset or empty is no map. A value that is not a JSON
|
|
70
|
+
* object, or an entry that names an unknown stage or tier, is ignored and
|
|
71
|
+
* named in `ignored`, so a typo never moves a stage and the run says so.
|
|
72
|
+
*/
|
|
73
|
+
export function parseStageTiers(raw) {
|
|
74
|
+
const text = raw?.trim();
|
|
75
|
+
if (!text)
|
|
76
|
+
return { tiers: {}, ignored: [] };
|
|
77
|
+
let data;
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.parse(text);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { tiers: {}, ignored: [text] };
|
|
83
|
+
}
|
|
84
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
85
|
+
return { tiers: {}, ignored: [text] };
|
|
86
|
+
const tiers = {};
|
|
87
|
+
const ignored = [];
|
|
88
|
+
for (const [stage, value] of Object.entries(data)) {
|
|
89
|
+
const tier = typeof value === "string" ? parseTierCap(value) : undefined;
|
|
90
|
+
if (TIERED_STAGES.includes(stage) && tier)
|
|
91
|
+
tiers[stage] = tier;
|
|
92
|
+
else
|
|
93
|
+
ignored.push(`${stage}=${JSON.stringify(value)}`);
|
|
94
|
+
}
|
|
95
|
+
return { tiers, ignored };
|
|
96
|
+
}
|
|
97
|
+
/** The stages with the map's tiers applied; a stage the map leaves out is unchanged. */
|
|
98
|
+
export function applyStageTiers(stages, tiers) {
|
|
99
|
+
return stages.map((stage) => {
|
|
100
|
+
const tier = tiers[stage.id];
|
|
101
|
+
return tier ? { ...stage, model: tier } : stage;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
57
104
|
//# sourceMappingURL=tierCap.js.map
|