@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI init pass (Onramp Door B, spec §5B/§7).
|
|
3
|
+
*
|
|
4
|
+
* On a FRESH workspace, the first `yagni` run in a repo should not drop the
|
|
5
|
+
* user into a bare prompt. Instead it runs a one-time init pass:
|
|
6
|
+
* 1. detect a fresh workspace (an empty/thin grounding corpus, via the existing
|
|
7
|
+
* `GET /api/yagni-code/context` brief),
|
|
8
|
+
* 2. read the repo (README, AGENTS.md/CLAUDE.md, package.json scripts, ADRs),
|
|
9
|
+
* 3. draft the ENGINEERING HALF of the company brief + a proposed Engineering
|
|
10
|
+
* Team, and record the salient ADRs/conventions as decisions so the corpus
|
|
11
|
+
* is non-empty for the very first `/go`,
|
|
12
|
+
* 4. present the draft for approve/edit in-terminal (mirrored to the app via the
|
|
13
|
+
* recorded decisions), NEVER auto-committing the Team, and
|
|
14
|
+
* 5. offer the ONE default next action (`suggest_next_work`) with escape hatches.
|
|
15
|
+
*
|
|
16
|
+
* Honesty rails (non-negotiable, spec §9): when a repo has no README/AGENTS/ADRs
|
|
17
|
+
* the engineering half stays THIN and SAYS SO — nothing is fabricated, and no
|
|
18
|
+
* decision is seeded from thin air. The proposed Team is only ever a DRAFT; it is
|
|
19
|
+
* never created/committed automatically.
|
|
20
|
+
*
|
|
21
|
+
* No new backend transport: the only writes are through the existing token-scoped
|
|
22
|
+
* `record_decision` endpoint (reused via {@link recordDecision}). Every seam is
|
|
23
|
+
* injectable so the whole pass is unit-testable without a network or a filesystem.
|
|
24
|
+
*/
|
|
25
|
+
import { readFile as fsReadFile, readdir as fsReaddir } from "node:fs/promises";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fetchContextBrief as defaultFetchContextBrief, } from "./config.js";
|
|
28
|
+
import { defaultNextAction } from "./nextWorkTool.js";
|
|
29
|
+
import { recordDecision as defaultRecordDecision, } from "./recordDecisionTool.js";
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Fresh-workspace detection
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* Below this many non-whitespace characters (and with no sources), a returned
|
|
35
|
+
* context brief is treated as thin/empty — i.e. the corpus has not been seeded
|
|
36
|
+
* yet, so this is a fresh workspace that should run the init pass.
|
|
37
|
+
*/
|
|
38
|
+
export const FRESH_BRIEF_MIN_CHARS = 40;
|
|
39
|
+
/**
|
|
40
|
+
* Is this a fresh workspace (an empty/thin grounding corpus)? A `null` brief, or
|
|
41
|
+
* a brief with only a stub of text and no sources, means nothing has grounded the
|
|
42
|
+
* workspace yet. A brief with real text OR any sources means it is already
|
|
43
|
+
* populated and the init pass must be skipped.
|
|
44
|
+
*/
|
|
45
|
+
export function isFreshWorkspace(brief) {
|
|
46
|
+
if (!brief)
|
|
47
|
+
return true;
|
|
48
|
+
const text = (brief.brief ?? "").trim();
|
|
49
|
+
const hasSources = Array.isArray(brief.sources) && brief.sources.length > 0;
|
|
50
|
+
return text.length < FRESH_BRIEF_MIN_CHARS && !hasSources;
|
|
51
|
+
}
|
|
52
|
+
/** The default fs seam: node:fs/promises, resolving to empty on any error. */
|
|
53
|
+
const nodeIntakeFs = {
|
|
54
|
+
async readFile(p) {
|
|
55
|
+
try {
|
|
56
|
+
return await fsReadFile(p, "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async readdir(p) {
|
|
63
|
+
try {
|
|
64
|
+
return await fsReaddir(p);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/** Candidate filenames, tried in order; the first that reads non-empty wins. */
|
|
72
|
+
const README_CANDIDATES = ["README.md", "README", "readme.md", "README.markdown"];
|
|
73
|
+
const AGENTS_CANDIDATES = ["AGENTS.md", "CLAUDE.md"];
|
|
74
|
+
const ADR_DIRS = ["docs/adr", "docs/adrs", "docs/decisions"];
|
|
75
|
+
/** Cap the amount of prose we lift so a large README never bloats the brief. */
|
|
76
|
+
const MAX_DOC_CHARS = 4000;
|
|
77
|
+
const MAX_SUMMARY_CHARS = 280;
|
|
78
|
+
const MAX_ADRS = 25;
|
|
79
|
+
async function firstNonEmpty(fs, cwd, names) {
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const body = (await fs.readFile(path.join(cwd, name))).trim();
|
|
82
|
+
if (body)
|
|
83
|
+
return body.slice(0, MAX_DOC_CHARS);
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Parse the `scripts` map out of a package.json body; {} on any problem. */
|
|
88
|
+
function parseScripts(raw) {
|
|
89
|
+
if (!raw.trim())
|
|
90
|
+
return {};
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
const scripts = parsed.scripts;
|
|
94
|
+
if (!scripts || typeof scripts !== "object")
|
|
95
|
+
return {};
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [k, v] of Object.entries(scripts)) {
|
|
98
|
+
if (typeof v === "string")
|
|
99
|
+
out[k] = v;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** The first `# heading` (or the filename) as a title; first paragraph as summary. */
|
|
108
|
+
function extractAdr(relPath, content) {
|
|
109
|
+
const heading = content.match(/^#{1,3}\s+(.+)$/m);
|
|
110
|
+
const title = heading?.[1]?.trim() || path.basename(relPath).replace(/\.[^.]+$/, "");
|
|
111
|
+
// First non-heading, non-empty paragraph.
|
|
112
|
+
const paragraphs = content
|
|
113
|
+
.split(/\n\s*\n/)
|
|
114
|
+
.map((p) => p.trim())
|
|
115
|
+
.filter((p) => p && !p.startsWith("#"));
|
|
116
|
+
const summary = paragraphs[0]?.replace(/\s+/g, " ").slice(0, MAX_SUMMARY_CHARS);
|
|
117
|
+
return { path: relPath, title, summary: summary || undefined };
|
|
118
|
+
}
|
|
119
|
+
async function readAdrs(fs, cwd) {
|
|
120
|
+
const adrs = [];
|
|
121
|
+
for (const dir of ADR_DIRS) {
|
|
122
|
+
const entries = await fs.readdir(path.join(cwd, dir));
|
|
123
|
+
for (const entry of entries.filter((e) => /\.m(d|arkdown)$/i.test(e)).sort()) {
|
|
124
|
+
if (adrs.length >= MAX_ADRS)
|
|
125
|
+
return adrs;
|
|
126
|
+
const rel = `${dir}/${entry}`;
|
|
127
|
+
const content = await fs.readFile(path.join(cwd, rel));
|
|
128
|
+
if (content.trim())
|
|
129
|
+
adrs.push(extractAdr(rel, content));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return adrs;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read the repo at `cwd` for the signals that seed the engineering half of the
|
|
136
|
+
* brief. Every read is best-effort; a missing file simply drops that signal.
|
|
137
|
+
*/
|
|
138
|
+
export async function readRepoIntake(cwd, fs = nodeIntakeFs) {
|
|
139
|
+
const [readme, agents, pkgRaw, adrs] = await Promise.all([
|
|
140
|
+
firstNonEmpty(fs, cwd, README_CANDIDATES),
|
|
141
|
+
firstNonEmpty(fs, cwd, AGENTS_CANDIDATES),
|
|
142
|
+
fs.readFile(path.join(cwd, "package.json")),
|
|
143
|
+
readAdrs(fs, cwd),
|
|
144
|
+
]);
|
|
145
|
+
const packageScripts = parseScripts(pkgRaw);
|
|
146
|
+
return {
|
|
147
|
+
readme,
|
|
148
|
+
agents,
|
|
149
|
+
packageScripts,
|
|
150
|
+
adrs,
|
|
151
|
+
testCommand: packageScripts.test || undefined,
|
|
152
|
+
buildCommand: packageScripts.build || undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const THIN_BRIEF = "I couldn't find a README, AGENTS.md/CLAUDE.md, or ADRs in this repo, so I don't " +
|
|
156
|
+
"know much about how you build yet. Tell me in a line what this repo is and how " +
|
|
157
|
+
"you build it, and I'll fill in the engineering brief.";
|
|
158
|
+
const TEAM_MISSION = "Owns how we build: conventions, reviews, test/build, and risk areas.";
|
|
159
|
+
/** The README's first real paragraph (heading-stripped), capped. */
|
|
160
|
+
function firstParagraph(readme) {
|
|
161
|
+
if (!readme)
|
|
162
|
+
return undefined;
|
|
163
|
+
const para = readme
|
|
164
|
+
.split(/\n\s*\n/)
|
|
165
|
+
.map((p) => p.trim())
|
|
166
|
+
.find((p) => p && !p.startsWith("#") && !/^[!\[]/.test(p));
|
|
167
|
+
return para?.replace(/\s+/g, " ").slice(0, MAX_SUMMARY_CHARS);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Draft the engineering half of the brief from repo intake. PURE. Honest-when-thin:
|
|
171
|
+
* with no README/AGENTS/ADRs the brief says so and NO decision is seeded.
|
|
172
|
+
*/
|
|
173
|
+
export function draftEngineering(intake) {
|
|
174
|
+
const hasSubstance = Boolean(intake.readme || intake.agents || intake.adrs.length);
|
|
175
|
+
const thin = !hasSubstance;
|
|
176
|
+
const team = {
|
|
177
|
+
name: "Engineering",
|
|
178
|
+
mission: TEAM_MISSION,
|
|
179
|
+
responsibilities: [],
|
|
180
|
+
playbookRules: [],
|
|
181
|
+
testCommand: intake.testCommand,
|
|
182
|
+
buildCommand: intake.buildCommand,
|
|
183
|
+
riskAreas: intake.adrs
|
|
184
|
+
.filter((a) => /risk|security|migration|billing|auth/i.test(a.title))
|
|
185
|
+
.map((a) => a.title),
|
|
186
|
+
};
|
|
187
|
+
if (intake.testCommand)
|
|
188
|
+
team.playbookRules.push(`Run \`${intake.testCommand}\` before handing off a change.`);
|
|
189
|
+
if (intake.buildCommand)
|
|
190
|
+
team.playbookRules.push(`Run \`${intake.buildCommand}\` to verify the change builds.`);
|
|
191
|
+
team.playbookRules.push("Ask @yagni before guessing on anything org- or codebase-specific; record product-intent decisions as you make them.");
|
|
192
|
+
if (thin) {
|
|
193
|
+
team.responsibilities.push("Define how this team builds software (not documented yet).");
|
|
194
|
+
return { brief: THIN_BRIEF, thin: true, decisions: [], team };
|
|
195
|
+
}
|
|
196
|
+
if (intake.agents)
|
|
197
|
+
team.responsibilities.push("Uphold the conventions documented in AGENTS.md/CLAUDE.md.");
|
|
198
|
+
if (intake.testCommand || intake.buildCommand)
|
|
199
|
+
team.responsibilities.push("Keep the build and tests green.");
|
|
200
|
+
if (intake.adrs.length)
|
|
201
|
+
team.responsibilities.push("Steward the architecture decisions in docs/adr.");
|
|
202
|
+
if (team.responsibilities.length === 0)
|
|
203
|
+
team.responsibilities.push("Own how this team builds software.");
|
|
204
|
+
const lines = ["How this team builds software (drafted from repo intake — a draft, editable):"];
|
|
205
|
+
const whatItIs = firstParagraph(intake.readme);
|
|
206
|
+
if (whatItIs)
|
|
207
|
+
lines.push(`- What this is: ${whatItIs}`);
|
|
208
|
+
if (intake.agents)
|
|
209
|
+
lines.push("- Conventions: documented in AGENTS.md/CLAUDE.md.");
|
|
210
|
+
if (intake.testCommand)
|
|
211
|
+
lines.push(`- Tests: \`${intake.testCommand}\``);
|
|
212
|
+
if (intake.buildCommand)
|
|
213
|
+
lines.push(`- Build: \`${intake.buildCommand}\``);
|
|
214
|
+
if (intake.adrs.length)
|
|
215
|
+
lines.push(`- Decisions on record: ${intake.adrs.map((a) => a.title).join("; ")}`);
|
|
216
|
+
const brief = lines.join("\n");
|
|
217
|
+
// Seed the corpus so the very first /go has real ground to cite. The brief
|
|
218
|
+
// itself is banked as one grounding entry, then each ADR/convention as a
|
|
219
|
+
// decision. All are DRAFTS (editable), never treated as immutable fact.
|
|
220
|
+
const decisions = [
|
|
221
|
+
{
|
|
222
|
+
question: "How does this team build software?",
|
|
223
|
+
decision: brief,
|
|
224
|
+
rationale: "Drafted from repo intake (README/AGENTS/ADRs) during the first YAGNI Code run; a draft, editable.",
|
|
225
|
+
},
|
|
226
|
+
...intake.adrs.map((adr) => ({
|
|
227
|
+
question: `What does "${adr.title}" decide?`,
|
|
228
|
+
decision: adr.summary || adr.title,
|
|
229
|
+
rationale: `Recorded from ${adr.path} during repo intake.`,
|
|
230
|
+
})),
|
|
231
|
+
];
|
|
232
|
+
return { brief, thin: false, decisions, team };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Compact one-screen PREVIEW of the draft for the approve/edit dialog.
|
|
236
|
+
*
|
|
237
|
+
* Approval-gated (spec §5B/§9, F2/F13): NOTHING is written to the corpus until
|
|
238
|
+
* the human approves or corrects the draft, so this is a preview of INTENT — it
|
|
239
|
+
* states what will be recorded ON APPROVAL, and never claims a corpus was
|
|
240
|
+
* already seeded. When thin there is nothing to record (honest — nothing to
|
|
241
|
+
* fabricate). The ACTUAL recorded count is reported after the write runs (see
|
|
242
|
+
* `runInitPass`).
|
|
243
|
+
*/
|
|
244
|
+
export function summarizeDraft(draft) {
|
|
245
|
+
const parts = [draft.brief, "", `Proposed Team: ${draft.team.name} — ${draft.team.mission}`];
|
|
246
|
+
if (draft.team.responsibilities.length) {
|
|
247
|
+
parts.push("Responsibilities:", ...draft.team.responsibilities.map((r) => ` - ${r}`));
|
|
248
|
+
}
|
|
249
|
+
if (draft.thin) {
|
|
250
|
+
parts.push("", "(Engineering context is thin — nothing to record yet; I never fabricate one.)");
|
|
251
|
+
}
|
|
252
|
+
else if (draft.decisions.length > 0) {
|
|
253
|
+
parts.push("", `On approval I'll record ${draft.decisions.length} decision(s) into your grounding corpus.`);
|
|
254
|
+
}
|
|
255
|
+
parts.push("", "This Team is a draft — it is never created automatically. You can edit it first.");
|
|
256
|
+
return parts.join("\n");
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Run the init pass. Guards on fresh-workspace detection (defensive — the caller
|
|
260
|
+
* also guards), reads the repo, drafts the engineering half, presents it for
|
|
261
|
+
* approve/edit, and ONLY THEN seeds the corpus (approval-gated: an approve banks
|
|
262
|
+
* the drafted decisions, an edit banks the correction, a decline writes nothing),
|
|
263
|
+
* before offering the one default next action. Fully fail-soft: a UI or network
|
|
264
|
+
* hiccup never throws (it must never break session start).
|
|
265
|
+
*/
|
|
266
|
+
export async function runInitPass(pi, ctx, deps) {
|
|
267
|
+
const recordDecisionFn = deps.recordDecision ?? defaultRecordDecision;
|
|
268
|
+
const readIntake = deps.readRepoIntake ?? ((cwd) => readRepoIntake(cwd));
|
|
269
|
+
const decisionOpts = {
|
|
270
|
+
baseUrl: deps.baseUrl,
|
|
271
|
+
getToken: deps.getToken,
|
|
272
|
+
fetchImpl: deps.fetchImpl,
|
|
273
|
+
};
|
|
274
|
+
// 1. Fresh-workspace guard (skip when already grounded).
|
|
275
|
+
const brief = deps.brief !== undefined
|
|
276
|
+
? deps.brief
|
|
277
|
+
: await (deps.fetchContextBrief ?? defaultFetchContextBrief)({
|
|
278
|
+
baseUrl: deps.baseUrl,
|
|
279
|
+
getToken: deps.getToken,
|
|
280
|
+
fetchImpl: deps.fetchImpl,
|
|
281
|
+
}).catch(() => null);
|
|
282
|
+
if (!isFreshWorkspace(brief))
|
|
283
|
+
return { ran: false, reason: "not_fresh" };
|
|
284
|
+
const ui = (ctx.hasUI ? ctx.ui : undefined);
|
|
285
|
+
// 1b. Approval-gated writes (spec §5B/§9 — captured-judgment beat): the init
|
|
286
|
+
// pass records decisions into the corpus, but a decision only earns its place
|
|
287
|
+
// when a human can see and approve/edit the draft. In non-interactive / print
|
|
288
|
+
// (`-p`) / no-UI mode there is no one to approve, so we NEVER write — we skip
|
|
289
|
+
// the pass entirely (returning ran:false so the caller does NOT mark the
|
|
290
|
+
// workspace init-done, leaving the seed for the first interactive run).
|
|
291
|
+
if (!ui)
|
|
292
|
+
return { ran: false, reason: "non_interactive" };
|
|
293
|
+
const notify = (message, type) => {
|
|
294
|
+
try {
|
|
295
|
+
ui?.notify?.(message, type);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
/* a notice must never disrupt the pass */
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
notify("YAGNI Code — first run in this repo. Reading it to seed your engineering brief…", "info");
|
|
302
|
+
// 2. Read the repo + draft the engineering half.
|
|
303
|
+
let intake;
|
|
304
|
+
try {
|
|
305
|
+
intake = await readIntake(ctx.cwd);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
intake = { packageScripts: {}, adrs: [] };
|
|
309
|
+
}
|
|
310
|
+
const draft = draftEngineering(intake);
|
|
311
|
+
// 3. Present the draft for approve/edit BEFORE any durable write (spec §5B/§9,
|
|
312
|
+
// F2/F13 — approval-gated writes). A decision earns its place in the corpus
|
|
313
|
+
// only once the human has APPROVED the draft or CORRECTED it; nothing is
|
|
314
|
+
// written before the dialog. The Team itself is never auto-committed. In a
|
|
315
|
+
// UI without a `confirm` capability there is no way to obtain approval, so
|
|
316
|
+
// (consistent with the no-UI guard above) we write nothing.
|
|
317
|
+
let decisionsRecorded = 0;
|
|
318
|
+
let chosenAction;
|
|
319
|
+
try {
|
|
320
|
+
let approved = false;
|
|
321
|
+
let correctedBrief;
|
|
322
|
+
if (ui && typeof ui.confirm === "function") {
|
|
323
|
+
approved = await ui.confirm("Your draft Engineering Team", summarizeDraft(draft));
|
|
324
|
+
if (!approved && typeof ui.editor === "function") {
|
|
325
|
+
const edited = await ui.editor("Edit the engineering brief", draft.brief);
|
|
326
|
+
if (edited && edited.trim() && edited.trim() !== draft.brief.trim()) {
|
|
327
|
+
correctedBrief = edited.trim();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// 4. Approval-gated corpus writes. APPROVE → bank the drafted decisions.
|
|
332
|
+
// REJECT-then-correct → bank ONLY the correction (the rejected auto-draft
|
|
333
|
+
// is never written). REJECT without an edit (or no approval UI) → write
|
|
334
|
+
// NOTHING. Best-effort throughout: a failed write is swallowed and the
|
|
335
|
+
// recorded count stays honest (F13).
|
|
336
|
+
if (approved) {
|
|
337
|
+
for (const decision of draft.decisions) {
|
|
338
|
+
try {
|
|
339
|
+
const result = await recordDecisionFn(decisionOpts, decision, ctx.signal);
|
|
340
|
+
if (!result.spooled)
|
|
341
|
+
decisionsRecorded += 1;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
/* fail-soft: recording is best-effort */
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else if (correctedBrief) {
|
|
349
|
+
// The edit is a captured-judgment beat: bank the correction so the next
|
|
350
|
+
// run inherits the steer.
|
|
351
|
+
try {
|
|
352
|
+
const result = await recordDecisionFn(decisionOpts, {
|
|
353
|
+
question: "How does this team build software?",
|
|
354
|
+
decision: correctedBrief,
|
|
355
|
+
rationale: "The user edited the drafted engineering brief during the init pass.",
|
|
356
|
+
}, ctx.signal);
|
|
357
|
+
if (!result.spooled)
|
|
358
|
+
decisionsRecorded += 1;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
/* fail-soft */
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Report the ACTUAL recorded count once the write has run (never the
|
|
365
|
+
// intended count) so the receipt is honest even on a partial/failed write.
|
|
366
|
+
if (decisionsRecorded > 0) {
|
|
367
|
+
notify(`Recorded ${decisionsRecorded} decision(s) into your grounding corpus.`, "info");
|
|
368
|
+
}
|
|
369
|
+
// 5. Offer the ONE default next action (not a four-option chooser).
|
|
370
|
+
const next = defaultNextAction();
|
|
371
|
+
if (ui && typeof ui.select === "function") {
|
|
372
|
+
const labels = [next.primary.label, ...next.escapeHatches.map((h) => h.label)];
|
|
373
|
+
const picked = await ui.select("What next?", labels);
|
|
374
|
+
const chosen = [next.primary, ...next.escapeHatches].find((o) => o.label === picked) ?? next.primary;
|
|
375
|
+
chosenAction = chosen.id;
|
|
376
|
+
ui.setEditorText?.(chosen.prompt);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
notify(`Next: ${next.primary.prompt}`, "info");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* the presentation is best-effort; the corpus is already seeded */
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
ran: true,
|
|
387
|
+
thin: draft.thin,
|
|
388
|
+
decisionsRecorded,
|
|
389
|
+
teamDrafted: true,
|
|
390
|
+
teamCommitted: false,
|
|
391
|
+
chosenAction,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
//# sourceMappingURL=initPass.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { Citation } from "./askYagniTool.js";
|
|
4
|
+
/** Options for {@link makeSuggestNextWorkTool}. */
|
|
5
|
+
export interface MakeNextWorkToolOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
getToken: () => string | undefined;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
/** A single offered next action: its id, the label shown, and the editor prompt it prefills. */
|
|
11
|
+
export interface NextActionOption {
|
|
12
|
+
id: "suggest_next_work" | "bootstrap" | "review";
|
|
13
|
+
/** Short label shown in the selector. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** The message prefilled into the editor when this action is chosen. */
|
|
16
|
+
prompt: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one default next action plus its escape hatches. `primary` is the single
|
|
20
|
+
* most-likely thing to do (ask @yagni what to work on next); `escapeHatches` are
|
|
21
|
+
* the two alternatives (bootstrap a first ticket, or review the working diff).
|
|
22
|
+
* This is deliberately a ONE-default-plus-hatches shape, NOT a flat four-option
|
|
23
|
+
* chooser — defaults over choices (spec §7.3).
|
|
24
|
+
*/
|
|
25
|
+
export interface DefaultNextAction {
|
|
26
|
+
primary: NextActionOption;
|
|
27
|
+
escapeHatches: NextActionOption[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the default next action offered after the init pass. The recommended
|
|
31
|
+
* default is `suggest_next_work` ("ask @yagni what to work on next"); the escape
|
|
32
|
+
* hatches are "bootstrap a first ticket and /go" and "review this diff". Pure so
|
|
33
|
+
* the presenter can render it without a network.
|
|
34
|
+
*/
|
|
35
|
+
export declare function defaultNextAction(): DefaultNextAction;
|
|
36
|
+
declare const parameters: Type.TObject<{
|
|
37
|
+
context: Type.TOptional<Type.TString>;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Build the `suggest_next_work` tool.
|
|
41
|
+
*
|
|
42
|
+
* Asks the YAGNI app for the highest-value next thing to work on, RANKED BY
|
|
43
|
+
* BUSINESS PRIORITY across open work-items and desk next-moves (not by ticket
|
|
44
|
+
* order), with the business case for the top pick. Returns a ranked
|
|
45
|
+
* recommendation plus citations.
|
|
46
|
+
*/
|
|
47
|
+
export declare function makeSuggestNextWorkTool(opts: MakeNextWorkToolOptions): ToolDefinition<typeof parameters, {
|
|
48
|
+
citations: Citation[];
|
|
49
|
+
}>;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=nextWorkTool.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the default next action offered after the init pass. The recommended
|
|
5
|
+
* default is `suggest_next_work` ("ask @yagni what to work on next"); the escape
|
|
6
|
+
* hatches are "bootstrap a first ticket and /go" and "review this diff". Pure so
|
|
7
|
+
* the presenter can render it without a network.
|
|
8
|
+
*/
|
|
9
|
+
export function defaultNextAction() {
|
|
10
|
+
return {
|
|
11
|
+
primary: {
|
|
12
|
+
id: "suggest_next_work",
|
|
13
|
+
label: "Ask @yagni what to work on next (recommended)",
|
|
14
|
+
prompt: "What should I work on next?",
|
|
15
|
+
},
|
|
16
|
+
escapeHatches: [
|
|
17
|
+
{
|
|
18
|
+
id: "bootstrap",
|
|
19
|
+
label: "Bootstrap: pick a first ticket and /go",
|
|
20
|
+
prompt: "Pick a good first ticket to bootstrap on, then run /go on it.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "review",
|
|
24
|
+
label: "Review this diff",
|
|
25
|
+
prompt: "Review the current working-tree diff for business fit and correctness.",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const parameters = Type.Object({
|
|
31
|
+
context: Type.Optional(Type.String()),
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Build the `suggest_next_work` tool.
|
|
35
|
+
*
|
|
36
|
+
* Asks the YAGNI app for the highest-value next thing to work on, RANKED BY
|
|
37
|
+
* BUSINESS PRIORITY across open work-items and desk next-moves (not by ticket
|
|
38
|
+
* order), with the business case for the top pick. Returns a ranked
|
|
39
|
+
* recommendation plus citations.
|
|
40
|
+
*/
|
|
41
|
+
export function makeSuggestNextWorkTool(opts) {
|
|
42
|
+
return {
|
|
43
|
+
name: "suggest_next_work",
|
|
44
|
+
label: "Suggest next work",
|
|
45
|
+
description: "Ask YAGNI what to work on next, ranked by BUSINESS priority across open work-items " +
|
|
46
|
+
"and desk next-moves (not just ticket order), with the business case for the top " +
|
|
47
|
+
"pick and a concrete first step. Optionally pass a `context` to focus the ranking " +
|
|
48
|
+
"(e.g. 'revenue this week'). Use when deciding what to pick up.",
|
|
49
|
+
promptSnippet: "suggest_next_work: ask YAGNI for the highest-value next task, ranked by business priority.",
|
|
50
|
+
promptGuidelines: [
|
|
51
|
+
"Call suggest_next_work when the user asks what to do next or when you've finished and need the next task.",
|
|
52
|
+
"Pass a `context` if the user has a focus (a goal, a deadline, an area) so the ranking reflects it.",
|
|
53
|
+
"Present the recommendation with its business case; let the user confirm before starting.",
|
|
54
|
+
],
|
|
55
|
+
parameters,
|
|
56
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
57
|
+
onUpdate?.({
|
|
58
|
+
content: [{ type: "text", text: "Ranking the highest-value next work…" }],
|
|
59
|
+
details: { citations: [] },
|
|
60
|
+
});
|
|
61
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/next-action`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"content-type": "application/json",
|
|
65
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({ context: params.context, cwd: ctx?.cwd }),
|
|
68
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(await friendlyFetchError("suggest_next_work", res));
|
|
71
|
+
}
|
|
72
|
+
const data = (await res.json());
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: data.suggestion }],
|
|
75
|
+
details: { citations: data.citations ?? [] },
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=nextWorkTool.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 - interactive permission tiers + plan mode, wired to pi's tool-call gate.
|
|
3
|
+
*
|
|
4
|
+
* YAGNI Code registers no tool_call handler today, so the interactive session has
|
|
5
|
+
* no plan/approval surface. P3 adds one on pi's documented `tool_call` block seam
|
|
6
|
+
* plus a `/mode` command:
|
|
7
|
+
* - auto (default): never blocks. Byte-identical to today, so this is additive.
|
|
8
|
+
* - plan : blocks write/edit/bash so the agent can explore + propose without
|
|
9
|
+
* touching the tree.
|
|
10
|
+
* - review : surfaces a three-way ctx.ui.select before a write/edit/bash; a
|
|
11
|
+
* session bless (policy.isBlessed) auto-approves an action the user
|
|
12
|
+
* already OK'd for this tool + directory, so only novel actions
|
|
13
|
+
* prompt. Picking "Yes, and don't ask again for <tool> in <dir>"
|
|
14
|
+
* adds a session-scoped bless rule AND drafts a decision capture.
|
|
15
|
+
*
|
|
16
|
+
* `decideGate` is PURE; the live wiring holds the mode in a small closure (no
|
|
17
|
+
* module-global state). The default auto mode remains fail-open, but stricter
|
|
18
|
+
* modes fail closed for side-effect tools if the gate itself errors. Bless rules
|
|
19
|
+
* are session-scoped, path-prefix-bound, never persisted, and never consulted in
|
|
20
|
+
* plan mode (plan blocks outright before isBlessed is reached).
|
|
21
|
+
*
|
|
22
|
+
* Plan mode is a full mode, not just a blocker: while active, every agent turn
|
|
23
|
+
* carries a hidden [PLAN MODE ACTIVE] context message so the model plans
|
|
24
|
+
* instead of flailing against held writes, and a footer chip shows the mode.
|
|
25
|
+
* When the mode leaves plan, stale plan-context messages are filtered out of
|
|
26
|
+
* the context so the model doesn't keep believing it is restricted.
|
|
27
|
+
*/
|
|
28
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { type BlessStore } from "./bless.js";
|
|
30
|
+
export type PermissionMode = "auto" | "plan" | "review";
|
|
31
|
+
/** Which tools each tier acts on, plus the optional grounding-bless predicate. */
|
|
32
|
+
export interface PermissionPolicy {
|
|
33
|
+
/** Tools blocked outright in plan mode (write/exec). */
|
|
34
|
+
planBlockTools: string[];
|
|
35
|
+
/** Tools that prompt for confirmation in review mode. */
|
|
36
|
+
reviewConfirmTools: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Optional: a recorded decision already blesses this action, so it auto-runs in
|
|
39
|
+
* review mode instead of prompting. The hook for tying the gate to captured
|
|
40
|
+
* judgment; default undefined (nothing pre-blessed).
|
|
41
|
+
*/
|
|
42
|
+
isBlessed?: (toolName: string, params: Record<string, unknown>) => boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare const DEFAULT_PERMISSION_POLICY: PermissionPolicy;
|
|
45
|
+
/** A pure gate verdict: block outright, ask to confirm, or allow. */
|
|
46
|
+
export interface GateDecision {
|
|
47
|
+
block: boolean;
|
|
48
|
+
reason?: string;
|
|
49
|
+
/** review mode only: the caller should ctx.ui.confirm before allowing. */
|
|
50
|
+
confirm?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pure permission decision for one tool call under a mode + policy. auto always
|
|
54
|
+
* allows; plan blocks the write/exec set; review marks writes for confirmation
|
|
55
|
+
* unless a recorded decision blesses them.
|
|
56
|
+
*/
|
|
57
|
+
export declare function decideGate(toolName: string, params: Record<string, unknown>, mode: PermissionMode, policy: PermissionPolicy): GateDecision;
|
|
58
|
+
/** What was blessed with "don't ask again", handed to the capture hook. */
|
|
59
|
+
export interface BlessRememberInfo {
|
|
60
|
+
tool: string;
|
|
61
|
+
/** A human-readable directory label (cwd-relative when inside the tree). */
|
|
62
|
+
dir: string;
|
|
63
|
+
}
|
|
64
|
+
export interface RegisterPermissionDeps {
|
|
65
|
+
policy?: PermissionPolicy;
|
|
66
|
+
/** Initial mode (default auto). */
|
|
67
|
+
mode?: PermissionMode;
|
|
68
|
+
/** Session bless store factory (needs the cwd from the first tool_call). */
|
|
69
|
+
makeBlessStore?: (cwd: string) => BlessStore;
|
|
70
|
+
/**
|
|
71
|
+
* Called (fire-and-forget) when the user picks "Yes, and don't ask again".
|
|
72
|
+
* The judgment-capture hook: draft a decision so YAGNI remembers. Fail-soft;
|
|
73
|
+
* never blocks the approved tool call.
|
|
74
|
+
*/
|
|
75
|
+
onBlessRemember?: (ctx: ExtensionContext, info: BlessRememberInfo) => void | Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** The customType tag on injected plan-mode context (filterable later). */
|
|
78
|
+
export declare const PLAN_CONTEXT_TYPE = "yagni-plan-context";
|
|
79
|
+
export declare const PLAN_CONTEXT_MESSAGE = "[PLAN MODE ACTIVE]\nYou are in plan mode: explore and design, change nothing.\n- write, edit, and bash are held by the permission gate; do not attempt them.\n- Read, search, and ask_yagni freely to ground the plan in how this company works.\n- Produce a concrete numbered plan of the steps you would take, with the files involved.\n- End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.\n- Once executing, track the plan's steps with todo_write.";
|
|
80
|
+
/**
|
|
81
|
+
* Drop previously injected plan-mode context once plan mode is off, so the
|
|
82
|
+
* model stops believing writes are held. Pure; returns the SAME array when
|
|
83
|
+
* nothing needs filtering so callers can cheaply detect a no-op.
|
|
84
|
+
*/
|
|
85
|
+
export declare function filterStalePlanContext<T>(messages: T[]): T[];
|
|
86
|
+
/**
|
|
87
|
+
* Wire the tool_call gate + the /mode command onto a shared mode holder. Default
|
|
88
|
+
* auto, so absent any /mode this is a no-op over today's behavior.
|
|
89
|
+
*/
|
|
90
|
+
export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
|
|
91
|
+
//# sourceMappingURL=permission.d.ts.map
|