@yagni-app/code-staging 1.2.4-staging.1803.1 → 1.2.4-staging.1821.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.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * `checks.json`, the mission file that hands the build's verify gate what to
3
+ * run besides its own detected scripts: the plan's approved checks (each with
4
+ * what the host already knows: it fails on the unchanged code, or the sandbox
5
+ * cannot start it) and the pull request's CI jobs, read from the repository's
6
+ * workflows at build time.
7
+ *
8
+ * The format's producer is `renderMissionChecksJson` in
9
+ * `packages/backend/src/integrations/e2b/missionFiles.ts`; this package does
10
+ * not depend on the backend, so it reads the same version 1 with its own
11
+ * parser. A change there is a change here. A file that is missing or does not
12
+ * read as version 1 means no repository checks, never a stopped build.
13
+ */
14
+ import { readFileSync } from "node:fs";
15
+ export const MISSION_CHECKS_FILE_NAME = "checks.json";
16
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17
+ const isString = (value) => typeof value === "string";
18
+ function stringList(value) {
19
+ return Array.isArray(value) && value.every(isString) ? value : null;
20
+ }
21
+ function stringRecord(value) {
22
+ if (!isRecord(value))
23
+ return null;
24
+ return Object.values(value).every(isString) ? value : null;
25
+ }
26
+ function parseStep(value) {
27
+ if (!isRecord(value) || !isString(value.name) || !isString(value.run))
28
+ return null;
29
+ const step = { name: value.name, run: value.run };
30
+ if (value.workingDirectory !== undefined) {
31
+ if (!isString(value.workingDirectory))
32
+ return null;
33
+ step.workingDirectory = value.workingDirectory;
34
+ }
35
+ if (value.env !== undefined) {
36
+ const env = stringRecord(value.env);
37
+ if (!env)
38
+ return null;
39
+ step.env = env;
40
+ }
41
+ return step;
42
+ }
43
+ function parseJob(value) {
44
+ if (!isRecord(value) ||
45
+ !isString(value.workflow) ||
46
+ !isString(value.job) ||
47
+ !isString(value.name))
48
+ return null;
49
+ if (!Array.isArray(value.steps) || !Array.isArray(value.cannotRun))
50
+ return null;
51
+ const steps = [];
52
+ for (const raw of value.steps) {
53
+ const step = parseStep(raw);
54
+ if (!step)
55
+ return null;
56
+ steps.push(step);
57
+ }
58
+ const cannotRun = [];
59
+ for (const raw of value.cannotRun) {
60
+ if (!isRecord(raw) || !isString(raw.step) || !isString(raw.reason))
61
+ return null;
62
+ cannotRun.push({ step: raw.step, reason: raw.reason });
63
+ }
64
+ const job = {
65
+ workflow: value.workflow,
66
+ job: value.job,
67
+ name: value.name,
68
+ steps,
69
+ cannotRun,
70
+ };
71
+ for (const key of ["paths", "pathsIgnore"]) {
72
+ if (value[key] === undefined)
73
+ continue;
74
+ const list = stringList(value[key]);
75
+ if (!list)
76
+ return null;
77
+ job[key] = list;
78
+ }
79
+ return job;
80
+ }
81
+ /** The file, or null when the text is not a well-formed version 1 file. */
82
+ export function parseMissionChecksFile(text) {
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(text);
86
+ }
87
+ catch {
88
+ // Not JSON: the caller reports the null.
89
+ return null;
90
+ }
91
+ if (!isRecord(parsed) ||
92
+ parsed.version !== 1 ||
93
+ !Array.isArray(parsed.checks) ||
94
+ !Array.isArray(parsed.ciJobs)) {
95
+ return null;
96
+ }
97
+ const checks = [];
98
+ for (const raw of parsed.checks) {
99
+ if (!isRecord(raw) || !isString(raw.command))
100
+ return null;
101
+ if (raw.status !== "runnable" && raw.status !== "fails_on_base" && raw.status !== "cannot_run")
102
+ return null;
103
+ const check = { command: raw.command, status: raw.status };
104
+ if (isString(raw.cause))
105
+ check.cause = raw.cause;
106
+ checks.push(check);
107
+ }
108
+ const ciJobs = [];
109
+ for (const raw of parsed.ciJobs) {
110
+ const job = parseJob(raw);
111
+ if (!job)
112
+ return null;
113
+ ciJobs.push(job);
114
+ }
115
+ return { version: 1, checks, ciJobs };
116
+ }
117
+ /**
118
+ * Read the file. `missing` (no such file: a host that predates it, or a run
119
+ * with no mission) is silent; `unreadable` and `malformed` are the caller's to
120
+ * report once.
121
+ */
122
+ export function readMissionChecksFile(path, readFile = (p) => readFileSync(p, "utf8")) {
123
+ let text;
124
+ try {
125
+ text = readFile(path);
126
+ }
127
+ catch (err) {
128
+ const code = isRecord(err) && typeof err.code === "string" ? err.code : null;
129
+ return { file: null, problem: code === "ENOENT" ? "missing" : "unreadable" };
130
+ }
131
+ const file = parseMissionChecksFile(text);
132
+ return file ? { file } : { file: null, problem: "malformed" };
133
+ }
134
+ /** GitHub's filter glob: `*` within one path segment, `**` across segments, `?` one character. */
135
+ export function globToRegExp(pattern) {
136
+ let source = "";
137
+ for (let index = 0; index < pattern.length; index += 1) {
138
+ const char = pattern[index];
139
+ if (char === "*" && pattern[index + 1] === "*") {
140
+ source += ".*";
141
+ index += 1;
142
+ }
143
+ else if (char === "*") {
144
+ source += "[^/]*";
145
+ }
146
+ else if (char === "?") {
147
+ source += "[^/]";
148
+ }
149
+ else {
150
+ source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
151
+ }
152
+ }
153
+ return new RegExp(`^${source}$`);
154
+ }
155
+ /** The part of a pattern before its first glob character. */
156
+ function literalPrefix(pattern) {
157
+ const at = pattern.search(/[*?[]/);
158
+ return at < 0 ? pattern : pattern.slice(0, at);
159
+ }
160
+ /**
161
+ * Whether one changed path matches a pattern. Porcelain reports a new
162
+ * untracked folder as `dir/`, not its files: it matches when the pattern's
163
+ * literal start and the folder overlap, so a doubt runs the job.
164
+ */
165
+ function pathMatches(pattern, path) {
166
+ if (!path.endsWith("/"))
167
+ return globToRegExp(pattern).test(path);
168
+ const prefix = literalPrefix(pattern);
169
+ return prefix.startsWith(path) || path.startsWith(prefix);
170
+ }
171
+ /** An ordered list with `!` exclusions: the last pattern that matches decides. */
172
+ function listMatches(patterns, path) {
173
+ let matched = false;
174
+ for (const raw of patterns) {
175
+ const negated = raw.startsWith("!");
176
+ if (pathMatches(negated ? raw.slice(1) : raw, path))
177
+ matched = !negated;
178
+ }
179
+ return matched;
180
+ }
181
+ /**
182
+ * Whether a change to `changedPaths` runs this job on its pull request, by
183
+ * GitHub's rules: with `paths`, when any changed path matches; with
184
+ * `paths-ignore`, unless every changed path is ignored (a new folder is never
185
+ * taken as ignored). No filter: always. No changed paths: never.
186
+ */
187
+ export function ciJobInScope(job, changedPaths) {
188
+ if (changedPaths.length === 0)
189
+ return false;
190
+ if (job.paths)
191
+ return changedPaths.some((path) => listMatches(job.paths, path));
192
+ if (job.pathsIgnore) {
193
+ return changedPaths.some((path) => path.endsWith("/") || !listMatches(job.pathsIgnore, path));
194
+ }
195
+ return true;
196
+ }
197
+ //# sourceMappingURL=missionChecks.js.map
@@ -98,6 +98,14 @@ export interface RunPipelineDeps {
98
98
  grounded?: boolean;
99
99
  /** Environment inherited by spawned child stages. Defaults to the parent process env. */
100
100
  childEnv?: NodeJS.ProcessEnv;
101
+ /**
102
+ * Extra pi flags for the stages that write code: the single-writer
103
+ * implement, each builder, the synthesizer, every fixer, and the review
104
+ * loop's fix stage. Headless /go passes the launcher's `--skill` /
105
+ * `--prompt-template` paths here (YAG-784). Map, plan, the partitioner and
106
+ * the review lenses never get them.
107
+ */
108
+ extraArgs?: string[];
101
109
  /**
102
110
  * The no-op guard's git seam (injected for unit tests). Captures the repo
103
111
  * HEAD + porcelain status so the orchestrator can tell whether the build half
@@ -275,12 +275,14 @@ export async function runPipeline(ticket, deps) {
275
275
  // `go:implement:<workstream>`), so per-workstream spend attributes for free the
276
276
  // way review lenses do. Every other stage leaves it undefined and keeps the
277
277
  // runner's own `go:<stage>` default.
278
- const stageDepsFor = (tag, callerLabel) => {
278
+ // `writes` marks a build or fix child, the only ones that get `extraArgs`.
279
+ const stageDepsFor = (tag, callerLabel, writes = false) => {
279
280
  const onEvent = deps.onEvent;
280
281
  return {
281
282
  cwd: deps.cwd,
282
283
  signal: deps.signal,
283
284
  ...(deps.childEnv ? { env: deps.childEnv } : {}),
285
+ ...(writes && deps.extraArgs?.length ? { extraArgs: deps.extraArgs } : {}),
284
286
  ...(runId ? { attribution: { runId } } : {}),
285
287
  ...(callerLabel ? { callerLabel } : {}),
286
288
  onEvent: (ev) => {
@@ -462,7 +464,7 @@ export async function runPipeline(ticket, deps) {
462
464
  // after the builders have fixed what it pointed at.
463
465
  let summaryDefects = [];
464
466
  if (fanned) {
465
- const synth = await runStageFn({ ...synthesizerStage(), tools: stage.tools }, { ticket: promptTicket, previous: composeSynthesizerInput(handoff, report), grounded }, stageDepsFor({ stageId: stage.id }, SYNTHESIZER_CALLER_LABEL));
467
+ const synth = await runStageFn({ ...synthesizerStage(), tools: stage.tools }, { ticket: promptTicket, previous: composeSynthesizerInput(handoff, report), grounded }, stageDepsFor({ stageId: stage.id }, SYNTHESIZER_CALLER_LABEL, true));
466
468
  stages.push(synth);
467
469
  if (!isFailed(synth) && synth.finalOutput.trim()) {
468
470
  body = synth.finalOutput;
@@ -495,7 +497,7 @@ export async function runPipeline(ticket, deps) {
495
497
  const fixes = await mapWithConcurrencyLimit(assigned, ceiling, async (a) => {
496
498
  const siblings = workstreams.filter((s) => s.name !== a.workstream.name);
497
499
  progress({ kind: "stage_start", stageId: stage.id, workstream: a.workstream.name });
498
- const fixed = await runStageFn({ ...fixerStage(a.workstream, renderFindings(a.findings), siblings), tools: stage.tools }, { ticket: promptTicket, grounded }, stageDepsFor({ stageId: stage.id, workstream: a.workstream.name }, workstreamCallerLabel(a.workstream.name)));
500
+ const fixed = await runStageFn({ ...fixerStage(a.workstream, renderFindings(a.findings), siblings), tools: stage.tools }, { ticket: promptTicket, grounded }, stageDepsFor({ stageId: stage.id, workstream: a.workstream.name }, workstreamCallerLabel(a.workstream.name), true));
499
501
  progress({
500
502
  kind: "stage_done",
501
503
  stageId: stage.id,
@@ -520,7 +522,7 @@ export async function runPipeline(ticket, deps) {
520
522
  if (unassigned.length > 0) {
521
523
  const findingsText = renderFindings(unassigned);
522
524
  const fixStage = fanned ? synthesizerFixStage(findingsText) : fixerStage(undefined, findingsText);
523
- const fixed = await runStageFn({ ...fixStage, tools: stage.tools }, { ticket: promptTicket, grounded }, stageDepsFor({ stageId: stage.id }, fanned ? SYNTHESIZER_CALLER_LABEL : undefined));
525
+ const fixed = await runStageFn({ ...fixStage, tools: stage.tools }, { ticket: promptTicket, grounded }, stageDepsFor({ stageId: stage.id }, fanned ? SYNTHESIZER_CALLER_LABEL : undefined, true));
524
526
  stages.push(fixed);
525
527
  if (fanned) {
526
528
  if (!isFailed(fixed) && fixed.finalOutput.trim()) {
@@ -587,7 +589,7 @@ export async function runPipeline(ticket, deps) {
587
589
  const partitionTag = { stageId: stage.id };
588
590
  const runSingleWriter = async (decision) => {
589
591
  recordDecision(decision, beforeBuild);
590
- const written = await runStageFn(stage, { ticket: promptTicket, previous, grounded }, stageDepsFor({ stageId: stage.id }));
592
+ const written = await runStageFn(stage, { ticket: promptTicket, previous, grounded }, stageDepsFor({ stageId: stage.id }, undefined, true));
591
593
  stages.push(written);
592
594
  // A failed or credit-starved writer never reaches the checker: the build
593
595
  // loop's own guard turns it into `partial` or the honest stage error, and
@@ -670,7 +672,7 @@ export async function runPipeline(ticket, deps) {
670
672
  // starts and finishes on its own timing, tagged so interleaved events stay
671
673
  // attributed, and callers that ignore the tag still see one implement pair.
672
674
  progress({ kind: "stage_start", stageId: stage.id, workstream: w.name });
673
- const built = await runStageFn({ ...builderStage(w, siblings), tools: stage.tools }, { ticket: promptTicket, previous, grounded }, stageDepsFor({ stageId: stage.id, workstream: w.name }, workstreamCallerLabel(w.name)));
675
+ const built = await runStageFn({ ...builderStage(w, siblings), tools: stage.tools }, { ticket: promptTicket, previous, grounded }, stageDepsFor({ stageId: stage.id, workstream: w.name }, workstreamCallerLabel(w.name), true));
674
676
  // A child that died says so on its own done beat (the `degraded` flag the
675
677
  // review lenses already use), so no surface paints a dead builder as built.
676
678
  progress({
@@ -1107,7 +1109,7 @@ export async function runPipeline(ticket, deps) {
1107
1109
  const fixStartedAt = clock();
1108
1110
  progress({ kind: "stage_start", stageId: "fix", round });
1109
1111
  stageEvent("fix", "start", { round });
1110
- const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: designContext ? `${serializeBlocking(findings)}\n\n${designContext}` : serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }));
1112
+ const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: designContext ? `${serializeBlocking(findings)}\n\n${designContext}` : serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }, undefined, true));
1111
1113
  observedMs.fix = clock() - fixStartedAt;
1112
1114
  stages.push(fixResult);
1113
1115
  log("stage_done", { stageId: "fix", exitCode: fixResult.exitCode });
@@ -35,6 +35,12 @@ export interface RunStageDeps {
35
35
  };
36
36
  /** Optional environment for spawned child stages. Defaults to Node's inherited env. */
37
37
  env?: NodeJS.ProcessEnv;
38
+ /**
39
+ * Extra pi flags for this child, placed before the positional task prompt.
40
+ * Headless /go passes the launcher's `--skill` / `--prompt-template` paths
41
+ * here for build and fix stages only (YAG-784).
42
+ */
43
+ extraArgs?: string[];
38
44
  /**
39
45
  * YAG-471 attribution: the tracked run's server-assigned id (a UUID), sent to
40
46
  * the child as `YAGNI_RUN_ID` so the model proxy's `x-yagni-run-id` header
@@ -122,7 +122,11 @@ export async function runStage(stage, ctx, deps) {
122
122
  ...(tierCap ? { tierCap } : {}),
123
123
  });
124
124
  const { piCli, extensionPath } = resolveChild();
125
- const argv = groundedChildArgv(passthrough, { piCli, extensionPath });
125
+ const argv = groundedChildArgv(passthrough, {
126
+ piCli,
127
+ extensionPath,
128
+ ...(deps.extraArgs?.length ? { extraArgs: deps.extraArgs } : {}),
129
+ });
126
130
  // Fold events into a fixed-size accumulator as they stream — never retain
127
131
  // the full array (that OOMs on a verbose stage; YAG-317 follow-up).
128
132
  const acc = newEventAccumulator();
@@ -46,6 +46,8 @@
46
46
  * command output passes through {@link scrubSecrets} before it can enter
47
47
  * findings, handoffs, or run records (§0.9).
48
48
  */
49
+ import { type ToolchainFile } from "../toolchain/spawnHook.js";
50
+ import { type MissionChecksFile } from "./missionChecks.js";
49
51
  import type { Finding } from "./types.js";
50
52
  /**
51
53
  * Total wall-clock budget for a single verify run before it is abandoned
@@ -55,6 +57,17 @@ import type { Finding } from "./types.js";
55
57
  export declare const VERIFY_TIMEOUT_MS: number;
56
58
  /** Per-PACKAGE wall-clock budget for the test half (spec §3d); overrun fails open. Ten minutes until 2026-10-09. */
57
59
  export declare const VERIFY_TEST_TIMEOUT_MS: number;
60
+ /**
61
+ * The repository-checks half (the plan's approved checks and the pull
62
+ * request's CI steps) across a whole build: the gate runs after implement,
63
+ * after each fix turn and every review round, so a cap per call alone could
64
+ * add an hour to a build.
65
+ */
66
+ export declare const REPO_CHECKS_BUILD_BUDGET_MS: number;
67
+ /** The same half's cap in one gate call, inside the run budget's verify allowance. */
68
+ export declare const REPO_CHECKS_CALL_BUDGET_MS: number;
69
+ /** One approved check's or CI step's cap. */
70
+ export declare const REPO_CHECK_TIMEOUT_MS: number;
58
71
  /** A resolved verify command plus how it was found. */
59
72
  export interface VerifyCommand {
60
73
  /** Shell-free command + args, e.g. ["pnpm", "run", "typecheck"]. */
@@ -116,7 +129,10 @@ export declare function isFannedOutScript(body: string): boolean;
116
129
  * Parse `git status --porcelain` into repo-relative changed paths. Each line is
117
130
  * `XY <path>`; we drop the 2-char XY status + its separating space, and for a
118
131
  * rename/copy (`R old -> new`) take the destination. Pure; the impure git
119
- * shell-out is {@link snapshotWorkspace}.
132
+ * shell-out is {@link snapshotWorkspace}, whose status is trimmed as a whole:
133
+ * a first line ` M path` arrives as `M path`. A whole line always has a space
134
+ * third, so a line without one lost its leading space and its path starts one
135
+ * column earlier.
120
136
  */
121
137
  export declare function parseChangedPaths(status: string): string[];
122
138
  /**
@@ -261,6 +277,25 @@ export declare function buildVerifyEnv(source?: NodeJS.ProcessEnv): NodeJS.Proce
261
277
  * once instead of parking in watch mode until the 10-min cap kills it.
262
278
  */
263
279
  export declare function buildTestEnv(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
280
+ /**
281
+ * The environment an approved check or CI step runs under: the test half's
282
+ * (no `YAGNI_*`, no NODE_OPTIONS, CI on) minus the secrets the host strips
283
+ * from its own check runs, on the repository's toolchain when the build named
284
+ * one (the same PATH and variables the model's shell gets), then the step's
285
+ * own literal variables.
286
+ */
287
+ export declare function buildRepoCheckEnv(source?: NodeJS.ProcessEnv, toolchain?: ToolchainFile | null, stepEnv?: Record<string, string>): NodeJS.ProcessEnv;
288
+ /** One porcelain line's path, and whether it is untracked. */
289
+ interface TreeEntry {
290
+ path: string;
291
+ untracked: boolean;
292
+ }
293
+ /**
294
+ * The porcelain entries in `after` that `before` did not have: what the
295
+ * repository checks wrote into the tree. A path that was already dirty is the
296
+ * build's, and is not listed. Pure.
297
+ */
298
+ export declare function newTreeEntries(before: string, after: string): TreeEntry[];
264
299
  export declare function makeDefaultExec(opts?: {
265
300
  graceMs?: number;
266
301
  killTree?: (pid: number) => void;
@@ -289,6 +324,29 @@ export interface MakeRunVerifyOpts {
289
324
  testTimeoutMs?: number;
290
325
  /** Test-half env override for determinism in tests (default: buildTestEnv() per run). */
291
326
  testEnv?: NodeJS.ProcessEnv;
327
+ /**
328
+ * The mission's `checks.json` (the approved checks and the pull request's CI
329
+ * jobs). Absent or null: no repository-checks half, as before.
330
+ */
331
+ repoChecks?: () => MissionChecksFile | null;
332
+ /**
333
+ * This run's own changed paths (repo-relative), which scope the CI jobs by
334
+ * their `paths` filters. Default: the same `git status` the scope reads,
335
+ * minus the pre-run dirt.
336
+ */
337
+ changedPaths?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<string[]>;
338
+ /** Repository-checks env override for determinism in tests (default: buildRepoCheckEnv() per run). */
339
+ repoChecksEnv?: NodeJS.ProcessEnv;
340
+ /** The toolchain the repository checks run on (default: `YAGNI_TOOLCHAIN_FILE`, when it reads). */
341
+ toolchain?: () => ToolchainFile | null;
342
+ repoChecksBuildBudgetMs?: number;
343
+ repoChecksCallBudgetMs?: number;
344
+ repoCheckTimeoutMs?: number;
345
+ now?: () => number;
346
+ /** The tree's porcelain status before and after the half (default: `git status`). */
347
+ treeStatus?: (cwd: string, signal?: AbortSignal) => Promise<string>;
348
+ /** Undo what the half wrote into the tree (default: `git clean` / `git checkout` of exactly those paths). */
349
+ undoTreeWrites?: (cwd: string, entries: TreeEntry[]) => Promise<void>;
292
350
  }
293
351
  /**
294
352
  * One workstream as the scoped checker sees it: the name its findings are
@@ -365,4 +423,5 @@ export declare function makeWorkstreamCheck(opts?: MakeWorkstreamCheckOpts): (cw
365
423
  * blocks a real change on its own failure.
366
424
  */
367
425
  export declare function makeRunVerify(opts?: MakeRunVerifyOpts): (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<VerifyOutcome>;
426
+ export {};
368
427
  //# sourceMappingURL=verify.d.ts.map