@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.
- package/dist/agentSources.d.ts +78 -0
- package/dist/agentSources.js +194 -0
- package/dist/claudeCompat.d.ts +83 -0
- package/dist/claudeCompat.js +234 -14
- package/dist/claudePlugins.d.ts +15 -1
- package/dist/claudePlugins.js +12 -4
- package/dist/extension/pipeline/headlessGo.d.ts +11 -0
- package/dist/extension/pipeline/headlessGo.js +21 -0
- package/dist/extension/pipeline/invocation.d.ts +1 -0
- package/dist/extension/pipeline/invocation.js +8 -1
- package/dist/extension/pipeline/missionChecks.d.ts +66 -0
- package/dist/extension/pipeline/missionChecks.js +197 -0
- package/dist/extension/pipeline/orchestrator.d.ts +8 -0
- package/dist/extension/pipeline/orchestrator.js +9 -7
- package/dist/extension/pipeline/runner.d.ts +6 -0
- package/dist/extension/pipeline/runner.js +5 -1
- package/dist/extension/pipeline/verify.d.ts +60 -1
- package/dist/extension/pipeline/verify.js +320 -9
- package/dist/goHeadless.d.ts +24 -0
- package/dist/goHeadless.js +107 -1
- package/package.json +2 -2
package/dist/claudeCompat.js
CHANGED
|
@@ -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
|
|
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:
|
|
273
|
-
user:
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/claudePlugins.d.ts
CHANGED
|
@@ -38,7 +38,12 @@
|
|
|
38
38
|
* cannot reach outside its repo/plugin.
|
|
39
39
|
*/
|
|
40
40
|
import type { PluginLedger } from "./pluginStore.js";
|
|
41
|
-
|
|
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
|
package/dist/claudePlugins.js
CHANGED
|
@@ -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
|
|
369
|
-
const
|
|
370
|
-
const
|
|
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
|
-
...
|
|
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
|