@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.
@@ -28,10 +28,10 @@
28
28
  *
29
29
  * Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to turn the whole bridge off.
30
30
  */
31
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
31
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, statSync, writeFileSync, } from "node:fs";
32
32
  import { homedir } from "node:os";
33
- import { delimiter, dirname, join, resolve } from "node:path";
34
- import { discoverClaudePlugins } from "./claudePlugins.js";
33
+ import { delimiter, dirname, join, resolve, sep } from "node:path";
34
+ import { containedExistingPath, discoverClaudePlugins, pluginAssets, readMarketplace, resolveLocalPluginRoot, } from "./claudePlugins.js";
35
35
  import { pluginsHome, readLedger } from "./pluginStore.js";
36
36
  export const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
37
37
  /** Plugin `agents/` dirs for the extension's subagent discovery (delimiter-joined). */
@@ -241,6 +241,137 @@ export function writeTrustDecision(agentDir, cwd, decision) {
241
241
  }
242
242
  }
243
243
  }
244
+ /** Personal root layouts, in load order. */
245
+ export const EXTRA_ROOT_LAYOUT = {
246
+ skills: ["skills", ".claude/skills", ".agents/skills"],
247
+ commands: ["commands", ".claude/commands"],
248
+ rules: ["rules", ".claude/rules"],
249
+ };
250
+ /** Project dirs pi loads only behind its own trust; wired explicitly instead. */
251
+ const PROJECT_NATIVE_SKILLS = [".agents/skills", ".pi/skills"];
252
+ const PROJECT_NATIVE_PROMPTS = [".pi/prompts"];
253
+ /** Bound on the symlink scan of one asset path, so a huge tree can't stall a launch. */
254
+ export const SYMLINK_SCAN_LIMIT = 20_000;
255
+ function isContainedIn(realRoot, real) {
256
+ return real === realRoot || real.startsWith(realRoot + sep);
257
+ }
258
+ /**
259
+ * True when `path` (inside `realRoot`) holds no symlink that resolves outside
260
+ * `realRoot`. A tree too large to scan within the bound counts as unsafe.
261
+ */
262
+ function symlinksStayInside(path, realRoot, scanLimit = SYMLINK_SCAN_LIMIT) {
263
+ let budget = scanLimit;
264
+ const stack = [path];
265
+ while (stack.length > 0) {
266
+ const current = stack.pop();
267
+ let entries;
268
+ try {
269
+ if (!lstatSync(current).isDirectory())
270
+ continue;
271
+ entries = readdirSync(current, { withFileTypes: true });
272
+ }
273
+ catch {
274
+ continue;
275
+ }
276
+ for (const entry of entries) {
277
+ if (--budget < 0)
278
+ return false;
279
+ const child = join(current, entry.name);
280
+ if (entry.isSymbolicLink()) {
281
+ let real;
282
+ try {
283
+ real = realpathSync(child);
284
+ }
285
+ catch {
286
+ continue; // Dangling: nothing can be read through it.
287
+ }
288
+ if (!isContainedIn(realRoot, real))
289
+ return false;
290
+ }
291
+ else if (entry.isDirectory()) {
292
+ stack.push(child);
293
+ }
294
+ }
295
+ }
296
+ return true;
297
+ }
298
+ /**
299
+ * `<root>/<rel>` as a realpath when it exists, is a directory, stays inside
300
+ * `root` after symlink resolution, and holds no symlink pointing outside it.
301
+ * Every source the interactive launcher never read goes through this, so a
302
+ * hostile link in a repo or snapshot can't hand pi a directory outside it.
303
+ * `scanLimit` exists for tests: a tree with more entries than it is refused.
304
+ */
305
+ export function safeSourceDir(root, rel, scanLimit = SYMLINK_SCAN_LIMIT) {
306
+ const contained = containedExistingPath(root, rel);
307
+ if (!contained || !isDirectory(contained))
308
+ return null;
309
+ let realRoot;
310
+ try {
311
+ realRoot = realpathSync(root);
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ return symlinksStayInside(contained, realRoot, scanLimit) ? contained : null;
317
+ }
318
+ function emptySummary(label, kind) {
319
+ return { label, kind, skills: [], commands: [], rulesDirs: [], plugins: [], unsupported: [] };
320
+ }
321
+ function summaryHasContent(s) {
322
+ return s.skills.length + s.commands.length + s.rulesDirs.length + s.plugins.length + s.unsupported.length > 0;
323
+ }
324
+ function addPluginsToSummary(summary, plugins) {
325
+ for (const plugin of plugins) {
326
+ summary.plugins.push(plugin.key);
327
+ summary.skills.push(...plugin.skillPaths);
328
+ summary.commands.push(...plugin.commandPaths);
329
+ summary.unsupported.push(...plugin.unsupported.map((u) => `${plugin.key}: ${u}`));
330
+ }
331
+ }
332
+ /** The enabled plugins of one marketplace checkout, plus what could not load. Never throws. */
333
+ export function marketplacePlugins(mp) {
334
+ const marketplace = readMarketplace(mp.root);
335
+ let realRoot = null;
336
+ try {
337
+ realRoot = realpathSync(mp.root);
338
+ }
339
+ catch {
340
+ realRoot = null;
341
+ }
342
+ if (!marketplace || !realRoot) {
343
+ return { plugins: [], unsupported: [".claude-plugin/marketplace.json missing or unreadable"] };
344
+ }
345
+ const plugins = [];
346
+ const unsupported = [];
347
+ for (const name of new Set(mp.enabled)) {
348
+ const entry = marketplace.plugins.find((p) => p.name === name);
349
+ if (!entry) {
350
+ unsupported.push(`${name}: not in the marketplace`);
351
+ continue;
352
+ }
353
+ const root = resolveLocalPluginRoot(marketplace, entry);
354
+ if (!root) {
355
+ unsupported.push(`${name}: plugin source not supported`);
356
+ continue;
357
+ }
358
+ if (!symlinksStayInside(root, realRoot)) {
359
+ unsupported.push(`${name}: links outside the marketplace`);
360
+ continue;
361
+ }
362
+ const assets = pluginAssets(root, name, {
363
+ key: `${name}@${marketplace.name}`,
364
+ origin: "org-marketplace",
365
+ version: entry.version ?? null,
366
+ });
367
+ if (!assets) {
368
+ unsupported.push(`${name}: nothing to load`);
369
+ continue;
370
+ }
371
+ plugins.push(assets);
372
+ }
373
+ return { plugins, unsupported };
374
+ }
244
375
  export function compatDisabled(env) {
245
376
  const value = env[CLAUDE_COMPAT_DISABLE_ENV];
246
377
  return value !== undefined && value !== "" && value !== "0";
@@ -250,27 +381,34 @@ export function compatDisabled(env) {
250
381
  * unconditionally, project-sourced dirs/plugins/rules once trusted, asking
251
382
  * (and persisting) the trust decision when needed. Everything is fail-soft —
252
383
  * compat can only add flags/env, never break a launch.
384
+ *
385
+ * The optional deps (`trustOverride`, `userScope`, `pluginMcp`,
386
+ * `projectNativeSkills`, `extraRoots`, `marketplaces`) serve headless cloud
387
+ * runs; left unset, the argv and env are exactly the interactive behavior.
253
388
  */
254
389
  export async function claudeCompatArgs(deps) {
255
390
  const env = deps.env ?? process.env;
256
391
  if (compatDisabled(env))
257
392
  return { argv: [], env: {} };
258
393
  const home = deps.homeDir ?? homedir();
259
- const projectTrust = readTrustDecision(deps.agentDir, deps.cwd);
394
+ const userScope = deps.userScope !== false;
395
+ const projectTrust = deps.trustOverride ?? readTrustDecision(deps.agentDir, deps.cwd);
260
396
  let plugins;
261
397
  try {
262
398
  plugins = discoverClaudePlugins({
263
399
  cwd: deps.cwd,
264
400
  homeDir: home,
265
- yagniLedger: readLedger(pluginsHome(env, home)),
401
+ ...(userScope ? { yagniLedger: readLedger(pluginsHome(env, home)) } : { includeUserScope: false }),
266
402
  });
267
403
  }
268
404
  catch {
269
405
  plugins = { user: [], project: [] };
270
406
  }
407
+ const projectDirs = probeClaudeDirs(deps.cwd);
408
+ const userDirs = userScope ? probeClaudeDirs(home) : { skills: null, commands: null, rules: null };
271
409
  const decision = decideClaudeCompat({
272
- project: probeClaudeDirs(deps.cwd),
273
- user: probeClaudeDirs(home),
410
+ project: projectDirs,
411
+ user: userDirs,
274
412
  projectPlugins: plugins.project,
275
413
  userPlugins: plugins.user,
276
414
  projectTrust,
@@ -286,27 +424,106 @@ export async function claudeCompatArgs(deps) {
286
424
  trusted = null;
287
425
  }
288
426
  }
289
- const argv = [...decision.userArgv];
290
- if (trusted === true)
427
+ // Backend-authorized sources (the org marketplace's enabled plugins and the
428
+ // personal roots) load whatever the folder's trust says.
429
+ const summaries = [];
430
+ const orgPlugins = [];
431
+ // Only keys that will actually load shadow an org copy: an untrusted repo's
432
+ // plugins never load, so they must not hide the org marketplace's copy.
433
+ const seenKeys = new Set([...plugins.user, ...(trusted === true ? plugins.project : [])].map((p) => p.key));
434
+ for (const mp of deps.marketplaces ?? []) {
435
+ const summary = emptySummary(mp.label, "org-marketplace");
436
+ const found = marketplacePlugins(mp);
437
+ const fresh = found.plugins.filter((p) => !seenKeys.has(p.key));
438
+ for (const p of fresh)
439
+ seenKeys.add(p.key);
440
+ orgPlugins.push(...fresh);
441
+ addPluginsToSummary(summary, fresh);
442
+ summary.unsupported.push(...found.unsupported);
443
+ summaries.push(summary);
444
+ }
445
+ const extraArgv = [];
446
+ const extraRulesDirs = [];
447
+ for (const extra of deps.extraRoots ?? []) {
448
+ const summary = emptySummary(extra.label, "personal");
449
+ const found = (rels) => rels.map((rel) => safeSourceDir(extra.root, rel)).filter((dir) => dir !== null);
450
+ summary.skills.push(...found(EXTRA_ROOT_LAYOUT.skills));
451
+ summary.commands.push(...found(EXTRA_ROOT_LAYOUT.commands));
452
+ summary.rulesDirs.push(...found(EXTRA_ROOT_LAYOUT.rules));
453
+ for (const dir of summary.skills)
454
+ extraArgv.push("--skill", dir);
455
+ for (const dir of summary.commands)
456
+ extraArgv.push("--prompt-template", dir);
457
+ extraRulesDirs.push(...summary.rulesDirs);
458
+ summaries.push(summary);
459
+ }
460
+ const nativeSkills = [];
461
+ const nativePrompts = [];
462
+ if (trusted === true && deps.projectNativeSkills === true) {
463
+ for (const rel of PROJECT_NATIVE_SKILLS) {
464
+ const dir = safeSourceDir(deps.cwd, rel);
465
+ if (dir)
466
+ nativeSkills.push(dir);
467
+ }
468
+ for (const rel of PROJECT_NATIVE_PROMPTS) {
469
+ const dir = safeSourceDir(deps.cwd, rel);
470
+ if (dir)
471
+ nativePrompts.push(dir);
472
+ }
473
+ }
474
+ const argv = [...decision.userArgv, ...pluginsToArgv(orgPlugins), ...extraArgv];
475
+ if (trusted === true) {
291
476
  argv.push(...decision.projectArgv);
477
+ for (const dir of nativeSkills)
478
+ argv.push("--skill", dir);
479
+ for (const dir of nativePrompts)
480
+ argv.push("--prompt-template", dir);
481
+ }
292
482
  const agentDirs = [
293
483
  ...decision.userAgentDirs,
484
+ ...orgPlugins.flatMap((p) => p.agentDirs),
294
485
  ...(trusted === true ? decision.projectAgentDirs : []),
295
486
  ];
296
487
  // User rules first, project rules last: the extension appends in order, so
297
488
  // project rules end up closest to the task — matching Claude Code priority.
298
489
  const rulesDirs = [
299
490
  ...decision.userRulesDirs,
491
+ ...extraRulesDirs,
300
492
  ...(trusted === true ? decision.projectRulesDirs : []),
301
493
  ];
302
- const mcpEntries = [
303
- ...decision.userMcp,
304
- ...(trusted === true ? decision.projectMcp : []),
305
- ];
494
+ // Org plugin servers come from repo config, so they ride the approval gate.
495
+ const mcpEntries = deps.pluginMcp === false
496
+ ? []
497
+ : [
498
+ ...decision.userMcp,
499
+ ...pluginMcpEntries(orgPlugins, true),
500
+ ...(trusted === true ? decision.projectMcp : []),
501
+ ];
306
502
  const loadedKeys = [
307
503
  ...plugins.user.map((p) => p.key),
504
+ ...orgPlugins.map((p) => p.key),
308
505
  ...(trusted === true ? plugins.project.map((p) => p.key) : []),
309
506
  ];
507
+ const user = emptySummary("~/.claude", "user");
508
+ if (userDirs.skills)
509
+ user.skills.push(userDirs.skills);
510
+ if (userDirs.commands)
511
+ user.commands.push(userDirs.commands);
512
+ user.rulesDirs.push(...decision.userRulesDirs);
513
+ addPluginsToSummary(user, plugins.user);
514
+ summaries.unshift(user);
515
+ if (trusted === true) {
516
+ const project = emptySummary("repository", "project");
517
+ if (projectDirs.skills)
518
+ project.skills.push(projectDirs.skills);
519
+ if (projectDirs.commands)
520
+ project.commands.push(projectDirs.commands);
521
+ project.rulesDirs.push(...decision.projectRulesDirs);
522
+ project.skills.push(...nativeSkills);
523
+ project.commands.push(...nativePrompts);
524
+ addPluginsToSummary(project, plugins.project);
525
+ summaries.push(project);
526
+ }
310
527
  const extraEnv = {};
311
528
  if (agentDirs.length > 0)
312
529
  extraEnv[CLAUDE_AGENT_DIRS_ENV] = agentDirs.join(delimiter);
@@ -316,7 +533,10 @@ export async function claudeCompatArgs(deps) {
316
533
  extraEnv[CLAUDE_PLUGIN_MCP_ENV] = JSON.stringify(mcpEntries);
317
534
  if (loadedKeys.length > 0)
318
535
  extraEnv[CLAUDE_PLUGIN_KEYS_ENV] = loadedKeys.join(delimiter);
319
- return { argv, env: extraEnv };
536
+ // Explicitly requested sources are always reported (an empty one is news);
537
+ // user and project scope only when they contributed something.
538
+ const sources = summaries.filter((s) => s.kind === "org-marketplace" || s.kind === "personal" || summaryHasContent(s));
539
+ return { argv, env: extraEnv, ...(sources.length > 0 ? { sources } : {}) };
320
540
  }
321
541
  function toInventory(plugin, gated) {
322
542
  return {
@@ -38,7 +38,12 @@
38
38
  * cannot reach outside its repo/plugin.
39
39
  */
40
40
  import type { PluginLedger } from "./pluginStore.js";
41
- export type PluginOrigin = "yagni" | "claude-code" | "repo-marketplace";
41
+ /**
42
+ * `org-marketplace`: a workspace's plugin marketplace snapshot handed to a
43
+ * cloud run (YAG-784); loaded by `claudeCompatArgs`'s `marketplaces` option,
44
+ * never by `discoverClaudePlugins`.
45
+ */
46
+ export type PluginOrigin = "yagni" | "claude-code" | "repo-marketplace" | "org-marketplace";
42
47
  export interface PluginMcp {
43
48
  /** File the servers came from (plugin `.mcp.json` or `plugin.json`). */
44
49
  sourcePath: string;
@@ -98,6 +103,8 @@ export interface EnableState {
98
103
  * project `.claude/settings.local.json`.
99
104
  */
100
105
  export declare function readEnabledPlugins(cwd: string, homeDir: string): Map<string, EnableState>;
106
+ /** Project-scope `enabledPlugins` only (no user `~/.claude/settings.json`). */
107
+ export declare function readProjectEnabledPlugins(cwd: string): Map<string, EnableState>;
101
108
  /** `~/.claude/plugins/known_marketplaces.json` → marketplace name → checkout dir. */
102
109
  export declare function readKnownMarketplaces(homeDir: string): Map<string, string>;
103
110
  /**
@@ -142,6 +149,13 @@ export interface DiscoverPluginsDeps {
142
149
  homeDir: string;
143
150
  /** YAGNI's own ledger (`readLedger` from pluginStore.ts); wins over the mirrored Claude Code state. */
144
151
  yagniLedger?: PluginLedger;
152
+ /**
153
+ * False skips everything under `homeDir` (the YAGNI ledger, Claude Code's
154
+ * install ledger, known marketplaces, user `enabledPlugins`), leaving only
155
+ * the repo's own marketplace and project `enabledPlugins` opt-outs. Headless
156
+ * cloud runs use it so nothing a sandbox step wrote under HOME can load.
157
+ */
158
+ includeUserScope?: boolean;
145
159
  }
146
160
  /**
147
161
  * All locally-present Claude Code plugin content relevant to `cwd`, split by
@@ -145,6 +145,13 @@ export function readEnabledPlugins(cwd, homeDir) {
145
145
  collectEnabled(join(cwd, ".claude", "settings.local.json"), "project", out);
146
146
  return out;
147
147
  }
148
+ /** Project-scope `enabledPlugins` only (no user `~/.claude/settings.json`). */
149
+ export function readProjectEnabledPlugins(cwd) {
150
+ const out = new Map();
151
+ collectEnabled(join(cwd, ".claude", "settings.json"), "project", out);
152
+ collectEnabled(join(cwd, ".claude", "settings.local.json"), "project", out);
153
+ return out;
154
+ }
148
155
  /** `~/.claude/plugins/known_marketplaces.json` → marketplace name → checkout dir. */
149
156
  export function readKnownMarketplaces(homeDir) {
150
157
  const out = new Map();
@@ -365,9 +372,10 @@ function resolveInstalledRoot(key, installed, marketplaces, realCwd) {
365
372
  */
366
373
  export function discoverClaudePlugins(deps) {
367
374
  try {
368
- const installed = readInstalledPlugins(deps.homeDir);
369
- const enabled = readEnabledPlugins(deps.cwd, deps.homeDir);
370
- const marketplaces = readKnownMarketplaces(deps.homeDir);
375
+ const userScope = deps.includeUserScope !== false;
376
+ const installed = userScope ? readInstalledPlugins(deps.homeDir) : new Map();
377
+ const enabled = userScope ? readEnabledPlugins(deps.cwd, deps.homeDir) : readProjectEnabledPlugins(deps.cwd);
378
+ const marketplaces = userScope ? readKnownMarketplaces(deps.homeDir) : new Map();
371
379
  const realCwd = realOrNull(deps.cwd) ?? resolve(deps.cwd);
372
380
  const user = [];
373
381
  const project = [];
@@ -375,7 +383,7 @@ export function discoverClaudePlugins(deps) {
375
383
  // YAGNI-installed plugins are the user's own explicit action (`yagni
376
384
  // plugin install`), so both scopes load without ceremony; a project-scope
377
385
  // install applies only inside its repo.
378
- for (const [key, entries] of Object.entries(deps.yagniLedger?.plugins ?? {})) {
386
+ for (const [key, entries] of Object.entries((userScope ? deps.yagniLedger?.plugins : undefined) ?? {})) {
379
387
  const applicable = entries.filter((e) => {
380
388
  if (!e.enabled || !isDirectory(e.installPath))
381
389
  return false;
@@ -60,6 +60,7 @@
60
60
  */
61
61
  import { type TreeSnapshotDeps } from "./treeSnapshot.js";
62
62
  import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
63
+ import { makeRunVerify } from "./verify.js";
63
64
  import { type RunBudget } from "./budget.js";
64
65
  import type { Finding, JsonEvent, ModelTier, PipelineStage, StageId, StageTag, StageUsage, StopReason } from "./types.js";
65
66
  /** One-line usage copy, shared by every argument error. */
@@ -143,10 +144,20 @@ export interface HeadlessGoDeps {
143
144
  env?: NodeJS.ProcessEnv;
144
145
  /** Environment for spawned stage children. Defaults to `env`. */
145
146
  childEnv?: NodeJS.ProcessEnv;
147
+ /**
148
+ * Extra pi flags for the build and fix stage children: the launcher's
149
+ * `--skill` / `--prompt-template` paths (YAG-784). See RunPipelineDeps.extraArgs.
150
+ */
151
+ extraArgs?: string[];
146
152
  /** Wall clock (epoch ms) the host's time left is anchored to. Defaults to `Date.now`; injected in tests. */
147
153
  now?: () => number;
148
154
  runPipeline?: typeof defaultRunPipeline;
149
155
  readFile?: (path: string) => string;
156
+ /**
157
+ * The verify gate a mission run uses, given its `checks.json` (null when
158
+ * there is none). Defaults to the real gate; injected in tests.
159
+ */
160
+ makeRunVerify?: typeof makeRunVerify;
150
161
  /** stdout sink; one call per NDJSON line, newline added by the caller. */
151
162
  write?: (line: string) => void;
152
163
  /** stderr sink for human-readable problems. */
@@ -59,12 +59,15 @@
59
59
  * ceilings (`SANDBOX_RUN_BUDGET`), not interactive /go's.
60
60
  */
61
61
  import { readFileSync } from "node:fs";
62
+ import { dirname, join } from "node:path";
62
63
  import { continuationResumePlan, parseContinuation } from "./continuation.js";
63
64
  import { resolveFanoutMode } from "./fanout.js";
64
65
  import { makeFanoutBeats } from "./fanoutBeats.js";
65
66
  import { normalizeMission } from "./mission.js";
67
+ import { MISSION_CHECKS_FILE_NAME, readMissionChecksFile } from "./missionChecks.js";
66
68
  import { startTreeSnapshots } from "./treeSnapshot.js";
67
69
  import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
70
+ import { makeRunVerify } from "./verify.js";
68
71
  import { loadGroundingEnabled } from "../grounding.js";
69
72
  import { codeStateHome } from "../stateHome.js";
70
73
  import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
@@ -319,9 +322,26 @@ export async function runHeadlessGo(argv, deps = {}) {
319
322
  // NDJSON stream as `pipeline_fanout` records, and the mission collector
320
323
  // folds them onto the Run.
321
324
  const fanoutBeats = makeFanoutBeats();
325
+ // A mission's approved checks and its pull request's CI jobs, written by the
326
+ // host beside the ticket: the verify gate runs them inside the loop. No file
327
+ // (a host that predates it) leaves the gate as it was.
328
+ let runVerify;
329
+ if (mission && args.ticketFile) {
330
+ const checksPath = join(dirname(args.ticketFile), MISSION_CHECKS_FILE_NAME);
331
+ const read = readMissionChecksFile(checksPath, readFile);
332
+ if (read.file) {
333
+ const checks = read.file;
334
+ runVerify = (deps.makeRunVerify ?? makeRunVerify)({ repoChecks: () => checks });
335
+ }
336
+ else if (read.problem !== "missing") {
337
+ const why = read.problem === "malformed" ? "not a version 1 checks file" : "it could not be read";
338
+ writeErr(`Ignoring ${checksPath}: ${why}. The verify gate runs its own checks only.`);
339
+ }
340
+ }
322
341
  const pipelineDeps = {
323
342
  cwd,
324
343
  childEnv,
344
+ ...(deps.extraArgs?.length ? { extraArgs: deps.extraArgs } : {}),
325
345
  // `go.fanout` rides the same env surface as the tier cap, so a benchmark lane
326
346
  // pins the implement diamond without a second flag (spec decision 8).
327
347
  fanout: resolveFanoutMode(env),
@@ -336,6 +356,7 @@ export async function runHeadlessGo(argv, deps = {}) {
336
356
  ...(deps.stages ? { stages: deps.stages } : {}),
337
357
  grounded: deps.grounded ?? grounding,
338
358
  ...(mission ? { mission } : {}),
359
+ ...(runVerify ? { runVerify } : {}),
339
360
  ...(continuation ? { continuation: { stageAtStop: continuation.stageAtStop } } : {}),
340
361
  ...(resumeFrom ? { resumeFrom } : {}),
341
362
  onEvent: (ev, tag) => {
@@ -73,5 +73,6 @@ export declare function buildStageInvocation(stage: PipelineStage, ctx: {
73
73
  export declare function groundedChildArgv(stageArgv: string[], opts: {
74
74
  piCli: string;
75
75
  extensionPath: string;
76
+ extraArgs?: string[];
76
77
  }): string[];
77
78
  //# sourceMappingURL=invocation.d.ts.map
@@ -89,12 +89,19 @@ export function buildStageInvocation(stage, ctx) {
89
89
  */
90
90
  export function groundedChildArgv(stageArgv, opts) {
91
91
  const userChoseProvider = stageArgv.includes("--provider");
92
+ // Extra flags (the launcher's `--skill` / `--prompt-template` paths for a
93
+ // build or fix stage) go before the positional `Task: …` prompt, which pi
94
+ // reads as the message only while it stays last.
95
+ const extra = opts.extraArgs ?? [];
96
+ const passthrough = extra.length > 0 && stageArgv.length > 0
97
+ ? [...stageArgv.slice(0, -1), ...extra, stageArgv[stageArgv.length - 1]]
98
+ : stageArgv;
92
99
  return [
93
100
  opts.piCli,
94
101
  "-e",
95
102
  opts.extensionPath,
96
103
  ...(userChoseProvider ? [] : ["--provider", "yagni"]),
97
- ...stageArgv,
104
+ ...passthrough,
98
105
  ];
99
106
  }
100
107
  //# sourceMappingURL=invocation.js.map
@@ -0,0 +1,66 @@
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
+ export declare const MISSION_CHECKS_FILE_NAME = "checks.json";
15
+ export type MissionCheckStatus = "runnable" | "fails_on_base" | "cannot_run";
16
+ export interface MissionCheck {
17
+ command: string;
18
+ status: MissionCheckStatus;
19
+ cause?: string;
20
+ }
21
+ export interface MissionCiStep {
22
+ name: string;
23
+ run: string;
24
+ workingDirectory?: string;
25
+ env?: Record<string, string>;
26
+ }
27
+ export interface MissionCiJob {
28
+ workflow: string;
29
+ job: string;
30
+ name: string;
31
+ paths?: string[];
32
+ pathsIgnore?: string[];
33
+ steps: MissionCiStep[];
34
+ cannotRun: Array<{
35
+ step: string;
36
+ reason: string;
37
+ }>;
38
+ }
39
+ export interface MissionChecksFile {
40
+ version: 1;
41
+ checks: MissionCheck[];
42
+ ciJobs: MissionCiJob[];
43
+ }
44
+ /** The file, or null when the text is not a well-formed version 1 file. */
45
+ export declare function parseMissionChecksFile(text: string): MissionChecksFile | null;
46
+ /**
47
+ * Read the file. `missing` (no such file: a host that predates it, or a run
48
+ * with no mission) is silent; `unreadable` and `malformed` are the caller's to
49
+ * report once.
50
+ */
51
+ export declare function readMissionChecksFile(path: string, readFile?: (path: string) => string): {
52
+ file: MissionChecksFile;
53
+ } | {
54
+ file: null;
55
+ problem: "missing" | "unreadable" | "malformed";
56
+ };
57
+ /** GitHub's filter glob: `*` within one path segment, `**` across segments, `?` one character. */
58
+ export declare function globToRegExp(pattern: string): RegExp;
59
+ /**
60
+ * Whether a change to `changedPaths` runs this job on its pull request, by
61
+ * GitHub's rules: with `paths`, when any changed path matches; with
62
+ * `paths-ignore`, unless every changed path is ignored (a new folder is never
63
+ * taken as ignored). No filter: always. No changed paths: never.
64
+ */
65
+ export declare function ciJobInScope(job: Pick<MissionCiJob, "paths" | "pathsIgnore">, changedPaths: readonly string[]): boolean;
66
+ //# sourceMappingURL=missionChecks.d.ts.map