storybook-addon-designbook 0.2.0 → 0.4.0

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/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import * as actualFS from 'fs';
3
- import { readFileSync, existsSync, readdirSync, mkdirSync, appendFileSync, renameSync, writeFileSync, unlinkSync, openSync, closeSync, statSync, realpathSync as realpathSync$1, readlinkSync, readdir as readdir$1, lstatSync } from 'fs';
3
+ import { readFileSync, existsSync, readdirSync, mkdirSync, appendFileSync, renameSync, writeFileSync, statSync, unlinkSync, openSync, closeSync, realpathSync as realpathSync$1, readlinkSync, readdir as readdir$1, lstatSync } from 'fs';
4
4
  import { load, dump } from 'js-yaml';
5
- import { resolve, dirname, basename, join, parse, relative, extname as extname$1, win32, posix } from 'path';
5
+ import { resolve, dirname, basename, join, parse, extname as extname$1, delimiter, relative, win32, posix } from 'path';
6
6
  import jsonata9 from 'jsonata';
7
7
  import { EventEmitter } from 'events';
8
8
  import Stream from 'stream';
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'url';
11
11
  import { realpath, readlink, readdir, lstat, readFile, mkdir, writeFile } from 'fs/promises';
12
12
  import { unescape, escape, Minimatch, GLOBSTAR } from 'minimatch';
13
13
  import { homedir } from 'os';
14
- import { execFileSync, spawn, execSync } from 'child_process';
14
+ import { spawn, execSync } from 'child_process';
15
15
  import * as http from 'http';
16
16
  import { createHash, randomBytes } from 'crypto';
17
17
  import { Command } from 'commander';
@@ -2366,10 +2366,10 @@ var init_esm2 = __esm({
2366
2366
  * Return a void Promise that resolves once the stream ends.
2367
2367
  */
2368
2368
  async promise() {
2369
- return new Promise((resolve19, reject) => {
2369
+ return new Promise((resolve20, reject) => {
2370
2370
  this.on(DESTROYED, () => reject(new Error("stream destroyed")));
2371
2371
  this.on("error", (er) => reject(er));
2372
- this.on("end", () => resolve19());
2372
+ this.on("end", () => resolve20());
2373
2373
  });
2374
2374
  }
2375
2375
  /**
@@ -2393,7 +2393,7 @@ var init_esm2 = __esm({
2393
2393
  return Promise.resolve({ done: false, value: res });
2394
2394
  if (this[EOF])
2395
2395
  return stop();
2396
- let resolve19;
2396
+ let resolve20;
2397
2397
  let reject;
2398
2398
  const onerr = (er) => {
2399
2399
  this.off("data", ondata);
@@ -2407,19 +2407,19 @@ var init_esm2 = __esm({
2407
2407
  this.off("end", onend);
2408
2408
  this.off(DESTROYED, ondestroy);
2409
2409
  this.pause();
2410
- resolve19({ value, done: !!this[EOF] });
2410
+ resolve20({ value, done: !!this[EOF] });
2411
2411
  };
2412
2412
  const onend = () => {
2413
2413
  this.off("error", onerr);
2414
2414
  this.off("data", ondata);
2415
2415
  this.off(DESTROYED, ondestroy);
2416
2416
  stop();
2417
- resolve19({ done: true, value: void 0 });
2417
+ resolve20({ done: true, value: void 0 });
2418
2418
  };
2419
2419
  const ondestroy = () => onerr(new Error("stream destroyed"));
2420
2420
  return new Promise((res2, rej) => {
2421
2421
  reject = rej;
2422
- resolve19 = res2;
2422
+ resolve20 = res2;
2423
2423
  this.once(DESTROYED, ondestroy);
2424
2424
  this.once("error", onerr);
2425
2425
  this.once("end", onend);
@@ -3288,7 +3288,7 @@ var init_esm3 = __esm({
3288
3288
  }
3289
3289
  }
3290
3290
  #applyStat(st) {
3291
- const { atime, atimeMs, birthtime, birthtimeMs, blksize, blocks, ctime, ctimeMs, dev, gid, ino, mode, mtime, mtimeMs, nlink, rdev, size, uid } = st;
3291
+ const { atime, atimeMs, birthtime, birthtimeMs, blksize, blocks, ctime, ctimeMs, dev, gid, ino, mode, mtime, mtimeMs: mtimeMs2, nlink, rdev, size, uid } = st;
3292
3292
  this.#atime = atime;
3293
3293
  this.#atimeMs = atimeMs;
3294
3294
  this.#birthtime = birthtime;
@@ -3302,7 +3302,7 @@ var init_esm3 = __esm({
3302
3302
  this.#ino = ino;
3303
3303
  this.#mode = mode;
3304
3304
  this.#mtime = mtime;
3305
- this.#mtimeMs = mtimeMs;
3305
+ this.#mtimeMs = mtimeMs2;
3306
3306
  this.#nlink = nlink;
3307
3307
  this.#rdev = rdev;
3308
3308
  this.#size = size;
@@ -3396,9 +3396,9 @@ var init_esm3 = __esm({
3396
3396
  if (this.#asyncReaddirInFlight) {
3397
3397
  await this.#asyncReaddirInFlight;
3398
3398
  } else {
3399
- let resolve19 = () => {
3399
+ let resolve20 = () => {
3400
3400
  };
3401
- this.#asyncReaddirInFlight = new Promise((res) => resolve19 = res);
3401
+ this.#asyncReaddirInFlight = new Promise((res) => resolve20 = res);
3402
3402
  try {
3403
3403
  for (const e of await this.#fs.promises.readdir(fullpath, {
3404
3404
  withFileTypes: true
@@ -3411,7 +3411,7 @@ var init_esm3 = __esm({
3411
3411
  children.provisional = 0;
3412
3412
  }
3413
3413
  this.#asyncReaddirInFlight = void 0;
3414
- resolve19();
3414
+ resolve20();
3415
3415
  }
3416
3416
  return children.slice(0, children.provisional);
3417
3417
  }
@@ -4480,10 +4480,10 @@ var init_ignore = __esm({
4480
4480
  ignored(p) {
4481
4481
  const fullpath = p.fullpath();
4482
4482
  const fullpaths = `${fullpath}/`;
4483
- const relative4 = p.relative() || ".";
4484
- const relatives = `${relative4}/`;
4483
+ const relative3 = p.relative() || ".";
4484
+ const relatives = `${relative3}/`;
4485
4485
  for (const m of this.relative) {
4486
- if (m.match(relative4) || m.match(relatives))
4486
+ if (m.match(relative3) || m.match(relatives))
4487
4487
  return true;
4488
4488
  }
4489
4489
  for (const m of this.absolute) {
@@ -4494,9 +4494,9 @@ var init_ignore = __esm({
4494
4494
  }
4495
4495
  childrenIgnored(p) {
4496
4496
  const fullpath = p.fullpath() + "/";
4497
- const relative4 = (p.relative() || ".") + "/";
4497
+ const relative3 = (p.relative() || ".") + "/";
4498
4498
  for (const m of this.relativeChildren) {
4499
- if (m.match(relative4))
4499
+ if (m.match(relative3))
4500
4500
  return true;
4501
4501
  }
4502
4502
  for (const m of this.absoluteChildren) {
@@ -5345,21 +5345,21 @@ var init_esm4 = __esm({
5345
5345
  }
5346
5346
  });
5347
5347
  function findFreePort() {
5348
- return new Promise((resolve19, reject) => {
5348
+ return new Promise((resolve20, reject) => {
5349
5349
  const server = http.createServer();
5350
5350
  server.listen(0, "127.0.0.1", () => {
5351
5351
  const addr = server.address();
5352
5352
  const port = typeof addr === "object" && addr ? addr.port : null;
5353
5353
  server.close((err) => {
5354
5354
  if (err || port === null) reject(err ?? new Error("Could not determine port"));
5355
- else resolve19(port);
5355
+ else resolve20(port);
5356
5356
  });
5357
5357
  });
5358
5358
  server.on("error", reject);
5359
5359
  });
5360
5360
  }
5361
5361
  function fetchJson(url2) {
5362
- return new Promise((resolve19, reject) => {
5362
+ return new Promise((resolve20, reject) => {
5363
5363
  http.get(url2, (res) => {
5364
5364
  if (res.statusCode !== 200) {
5365
5365
  res.resume();
@@ -5372,7 +5372,7 @@ function fetchJson(url2) {
5372
5372
  });
5373
5373
  res.on("end", () => {
5374
5374
  try {
5375
- resolve19(JSON.parse(body));
5375
+ resolve20(JSON.parse(body));
5376
5376
  } catch {
5377
5377
  reject(new Error("Invalid JSON"));
5378
5378
  }
@@ -6308,6 +6308,15 @@ function toKebab(input) {
6308
6308
  }
6309
6309
  function normaliseToSectionId(input) {
6310
6310
  const [withoutVariant = input] = input.split("--");
6311
+ if (!withoutVariant.includes("/")) {
6312
+ if (DESIGN_SYSTEM_STORY_ID.test(withoutVariant)) {
6313
+ return { id: "shell" };
6314
+ }
6315
+ const storyMatch = SECTION_STORY_ID.exec(withoutVariant);
6316
+ if (storyMatch?.[1]) {
6317
+ return { id: storyMatch[1] };
6318
+ }
6319
+ }
6311
6320
  const segments = withoutVariant.split("/").map((s) => s.trim()).filter(Boolean);
6312
6321
  if (segments.length === 0) {
6313
6322
  return null;
@@ -6327,9 +6336,11 @@ function normaliseToSectionId(input) {
6327
6336
  const id = toKebab(first);
6328
6337
  return id ? { id } : null;
6329
6338
  }
6330
- var scenePathResolver;
6339
+ var SECTION_STORY_ID, DESIGN_SYSTEM_STORY_ID, scenePathResolver;
6331
6340
  var init_scene_path = __esm({
6332
6341
  "src/resolvers/scene-path.ts"() {
6342
+ SECTION_STORY_ID = /^designbook-sections-(.+?)-scenes$/;
6343
+ DESIGN_SYSTEM_STORY_ID = /^designbook-design-system-scenes$/;
6333
6344
  scenePathResolver = {
6334
6345
  name: "scene_path",
6335
6346
  resolve(input, _config, _context) {
@@ -6391,6 +6402,9 @@ var init_components_index = __esm({
6391
6402
  init_story_patterns();
6392
6403
  componentsIndexResolver = {
6393
6404
  name: "components_index",
6405
+ // Producer: derives the inventory from the live Storybook index, with no
6406
+ // input param. Must run even though nothing supplies `components`.
6407
+ requiresInput: false,
6394
6408
  async resolve(_input, _config, context2) {
6395
6409
  const config = context2.config;
6396
6410
  const framework = String(config["frameworks.component"] ?? "");
@@ -6946,8 +6960,8 @@ function pickRegionLabel(params) {
6946
6960
  if (segments[0] === "design-system") {
6947
6961
  return "shell";
6948
6962
  }
6949
- const basename4 = segments[segments.length - 1] ?? "";
6950
- return basename4.replace(/(\.[^./]+)+$/, "");
6963
+ const basename5 = segments[segments.length - 1] ?? "";
6964
+ return basename5.replace(/(\.[^./]+)+$/, "");
6951
6965
  }
6952
6966
  return "";
6953
6967
  }
@@ -7166,7 +7180,7 @@ async function resolveParams(schema, context2) {
7166
7180
  return;
7167
7181
  }
7168
7182
  const input = outputParams[key];
7169
- if (input === void 0 && !decl.from) return;
7183
+ if (input === void 0 && !decl.from && resolver.requiresInput !== false) return;
7170
7184
  let effectiveInput;
7171
7185
  if (decl.from) {
7172
7186
  let fromValue;
@@ -8269,6 +8283,9 @@ __export(validation_registry_exports, {
8269
8283
  getValidatorKeys: () => getValidatorKeys,
8270
8284
  validateByKeys: () => validateByKeys
8271
8285
  });
8286
+ function shellQuote(value) {
8287
+ return `'${value.replace(/'/g, `'\\''`)}'`;
8288
+ }
8272
8289
  function toFileResult(result, file, type) {
8273
8290
  const ts = (/* @__PURE__ */ new Date()).toISOString();
8274
8291
  return {
@@ -8296,7 +8313,7 @@ async function validateByKeys(keys, file, config) {
8296
8313
  for (const key of keys) {
8297
8314
  if (key.startsWith("cmd:")) {
8298
8315
  const cmdTemplate = key.slice(4);
8299
- const cmd = cmdTemplate.replace(/\{\{\s*file\s*\}\}/g, file);
8316
+ const cmd = cmdTemplate.replace(/\{\{\s*file\s*\}\}/g, shellQuote(file));
8300
8317
  const ts = (/* @__PURE__ */ new Date()).toISOString();
8301
8318
  try {
8302
8319
  execSync(cmd, { timeout: 3e4, stdio: ["pipe", "pipe", "pipe"] });
@@ -8346,11 +8363,11 @@ var init_validation_registry = __esm({
8346
8363
  const buildResult = await validateSceneBuild2(file, config);
8347
8364
  if (!buildResult.valid) return buildResult;
8348
8365
  const { load: parseYaml16 } = await import('js-yaml');
8349
- const { readFileSync: readFileSync24, existsSync: existsSync26 } = await import('fs');
8350
- if (!existsSync26(file)) return buildResult;
8366
+ const { readFileSync: readFileSync23, existsSync: existsSync25 } = await import('fs');
8367
+ if (!existsSync25(file)) return buildResult;
8351
8368
  let raw;
8352
8369
  try {
8353
- raw = parseYaml16(readFileSync24(file, "utf-8"));
8370
+ raw = parseYaml16(readFileSync23(file, "utf-8"));
8354
8371
  } catch {
8355
8372
  return buildResult;
8356
8373
  }
@@ -8688,6 +8705,11 @@ function resolveSkillsRoot(configDir) {
8688
8705
  }
8689
8706
  return resolve(configDir, ".claude");
8690
8707
  }
8708
+ function expandTilde(p) {
8709
+ if (p === "~") return homedir();
8710
+ if (p.startsWith("~/") || p.startsWith("~\\")) return resolve(homedir(), p.slice(2));
8711
+ return p;
8712
+ }
8691
8713
  function assertNotRepoRoot(dataDir) {
8692
8714
  const parent = dirname(dataDir);
8693
8715
  const hasPnpmWorkspace = existsSync(resolve(parent, "pnpm-workspace.yaml"));
@@ -8752,6 +8774,9 @@ function loadConfig(startDir) {
8752
8774
  config[key] = resolve(configDir, config[key]);
8753
8775
  }
8754
8776
  }
8777
+ if (typeof config["skills"] === "string") {
8778
+ config["skills"] = resolve(configDir, expandTilde(config["skills"]));
8779
+ }
8755
8780
  return config;
8756
8781
  } catch (err) {
8757
8782
  throw new Error(`Failed to parse designbook.config.yml: ${err.message}`);
@@ -8761,6 +8786,165 @@ function loadConfig(startDir) {
8761
8786
  // src/cli.ts
8762
8787
  init_data();
8763
8788
  init_entity_mapping();
8789
+ var ARTIFACT_SUBDIRS = ["workflows", "tasks", "rules", "blueprints"];
8790
+ function isSkillContentRoot(dir) {
8791
+ if (!existsSync(dir)) return false;
8792
+ for (const sub of ARTIFACT_SUBDIRS) {
8793
+ if (existsSync(resolve(dir, sub))) return true;
8794
+ }
8795
+ let children;
8796
+ try {
8797
+ children = readdirSync(dir);
8798
+ } catch {
8799
+ return false;
8800
+ }
8801
+ for (const child of children) {
8802
+ const childDir = resolve(dir, child);
8803
+ if (!isDir(childDir)) continue;
8804
+ for (const sub of ARTIFACT_SUBDIRS) {
8805
+ if (existsSync(resolve(childDir, sub))) return true;
8806
+ }
8807
+ }
8808
+ return false;
8809
+ }
8810
+ function isDir(p) {
8811
+ try {
8812
+ return statSync(p).isDirectory();
8813
+ } catch {
8814
+ return false;
8815
+ }
8816
+ }
8817
+ function mtimeMs(p) {
8818
+ try {
8819
+ return statSync(p).mtimeMs;
8820
+ } catch {
8821
+ return 0;
8822
+ }
8823
+ }
8824
+ function listDirs(dir) {
8825
+ try {
8826
+ return readdirSync(dir).filter((c) => isDir(resolve(dir, c)));
8827
+ } catch {
8828
+ return [];
8829
+ }
8830
+ }
8831
+ function deriveSkillSourcesFromBase(base) {
8832
+ const B = resolve(base);
8833
+ if (!existsSync(B)) return [];
8834
+ const sources = [];
8835
+ for (const skillName of listDirs(B)) {
8836
+ const skillDir = resolve(B, skillName);
8837
+ const hashRoots = listDirs(skillDir).map((h) => resolve(skillDir, h)).filter(isSkillContentRoot);
8838
+ if (hashRoots.length > 0) {
8839
+ const best = hashRoots.reduce((a, b) => mtimeMs(b) > mtimeMs(a) ? b : a);
8840
+ sources.push({ name: skillName, root: best, origin: "plugin" });
8841
+ continue;
8842
+ }
8843
+ if (isSkillContentRoot(skillDir)) {
8844
+ sources.push({ name: skillName, root: skillDir, origin: "plugin" });
8845
+ }
8846
+ }
8847
+ if (sources.length > 0) return sources;
8848
+ if (isSkillContentRoot(B)) return [{ name: basename(B), root: B, origin: "plugin" }];
8849
+ return [];
8850
+ }
8851
+ function resolveProjectSkillSources(configDir) {
8852
+ const skillsRoot = resolveSkillsRoot(configDir);
8853
+ const skillsDir = resolve(skillsRoot, "skills");
8854
+ if (!existsSync(skillsDir)) return [];
8855
+ const sources = [];
8856
+ for (const child of listDirs(skillsDir)) {
8857
+ sources.push({ name: child, root: resolve(skillsDir, child), origin: "project" });
8858
+ }
8859
+ return sources;
8860
+ }
8861
+
8862
+ // src/skill-resolver.ts
8863
+ function pathScanBase(env) {
8864
+ const raw = env["PATH"] ?? "";
8865
+ for (const entry of raw.split(delimiter)) {
8866
+ if (!entry) continue;
8867
+ const cleaned = entry.replace(/[\\/]+$/, "");
8868
+ if (!/[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/]designbook(?:-[^\\/]+)?[\\/][^\\/]+[\\/]bin$/.test(cleaned)) {
8869
+ continue;
8870
+ }
8871
+ const contentRoot = dirname(cleaned);
8872
+ const skillDir = dirname(contentRoot);
8873
+ return dirname(skillDir);
8874
+ }
8875
+ return null;
8876
+ }
8877
+ function firstNonEmpty(bases) {
8878
+ for (const base of bases) {
8879
+ if (!base) continue;
8880
+ const sources = deriveSkillSourcesFromBase(base);
8881
+ if (sources.length > 0) return sources;
8882
+ }
8883
+ return [];
8884
+ }
8885
+ var configResolver = {
8886
+ name: "config",
8887
+ apply: (ctx) => typeof ctx.config.skills === "string" && ctx.config.skills ? firstNonEmpty([ctx.config.skills]) : []
8888
+ };
8889
+ var claudeCodeResolver = {
8890
+ name: "claude-code",
8891
+ apply: (ctx) => {
8892
+ const isClaudeCode = ctx.env["CLAUDECODE"] === "1" || /claude-code/.test(ctx.env["AI_AGENT"] ?? "");
8893
+ if (!isClaudeCode) return [];
8894
+ return firstNonEmpty([pathScanBase(ctx.env), join(ctx.home, ".claude", "plugins", "cache", "designbook")]);
8895
+ }
8896
+ };
8897
+ var codexResolver = {
8898
+ name: "codex",
8899
+ apply: (ctx) => {
8900
+ const codexHome = ctx.env["CODEX_HOME"] ? ctx.env["CODEX_HOME"] : join(ctx.home, ".codex");
8901
+ return firstNonEmpty([join(codexHome, "skills")]);
8902
+ }
8903
+ };
8904
+ var geminiResolver = {
8905
+ name: "gemini",
8906
+ apply: (ctx) => firstNonEmpty([join(ctx.home, ".gemini", "skills")])
8907
+ };
8908
+ var agentsResolver = {
8909
+ name: "agents",
8910
+ apply: (ctx) => firstNonEmpty([join(ctx.home, ".agents", "skills")])
8911
+ };
8912
+ var BUILT_IN_RESOLVERS = [
8913
+ configResolver,
8914
+ claudeCodeResolver,
8915
+ codexResolver,
8916
+ geminiResolver,
8917
+ agentsResolver
8918
+ ];
8919
+ function resolvePluginSkillSources(ctx) {
8920
+ const runtimeDisabled = ctx.env["DESIGNBOOK_DISABLE_AUTODETECT"] === "1";
8921
+ for (const resolver of BUILT_IN_RESOLVERS) {
8922
+ if (runtimeDisabled && resolver.name !== "config") continue;
8923
+ const sources = resolver.apply(ctx);
8924
+ if (sources.length > 0) return { runtime: resolver.name, sources };
8925
+ }
8926
+ return null;
8927
+ }
8928
+ function resolveSkillSources(configDir, overrides) {
8929
+ const ctx = {
8930
+ env: process.env,
8931
+ home: homedir(),
8932
+ config: loadConfig(configDir)
8933
+ };
8934
+ const projectSources = resolveProjectSkillSources(configDir);
8935
+ const pluginResult = resolvePluginSkillSources(ctx);
8936
+ if (pluginResult && process.env["DESIGNBOOK_DEBUG"]) {
8937
+ const base = pluginResult.sources[0]?.root ?? "?";
8938
+ process.stderr.write(`[designbook] skills: ${pluginResult.runtime} \u2192 ${base}
8939
+ `);
8940
+ }
8941
+ const byName = /* @__PURE__ */ new Map();
8942
+ for (const src of projectSources) byName.set(src.name, src);
8943
+ for (const src of pluginResult?.sources ?? []) {
8944
+ if (!byName.has(src.name)) byName.set(src.name, src);
8945
+ }
8946
+ return Array.from(byName.values());
8947
+ }
8764
8948
  var LONG_RUNNING_MS = 1e4;
8765
8949
  function digestLog(logPath2) {
8766
8950
  if (!existsSync(logPath2)) {
@@ -8831,50 +9015,125 @@ function evalAssertions(assertions, output) {
8831
9015
  }
8832
9016
  return { passed, total, failures };
8833
9017
  }
8834
- var MAX_RETRIES = 10;
8835
- var STALE_LOCK_MS = 3e4;
8836
- function acquireLock(filePath) {
8837
- const lockPath = `${filePath}.lock`;
8838
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
8839
- if (existsSync(lockPath)) {
9018
+ function deboSuffix(data) {
9019
+ if (!data.workflow_id) throw new Error("workflow_id is required for direct engine stash-at-target");
9020
+ return `.${data.workflow_id}.debo`;
9021
+ }
9022
+ function stashPath(data, fileEntry) {
9023
+ return fileEntry.path + deboSuffix(data);
9024
+ }
9025
+ var directEngine = {
9026
+ writeFile(data, task, key, content) {
9027
+ const fileEntry = (task.files ?? []).find((f) => f.key === key);
9028
+ if (!fileEntry) throw new Error(`No file entry with key '${key}' in task '${task.id}'`);
9029
+ const path = stashPath(data, fileEntry);
9030
+ mkdirSync(dirname(path), { recursive: true });
9031
+ writeFileSync(path, content);
9032
+ return { path };
9033
+ },
9034
+ getStagedPath(data, task, key) {
9035
+ const fileEntry = (task.files ?? []).find((f) => f.key === key);
9036
+ if (fileEntry) return stashPath(data, fileEntry);
9037
+ const resultEntry = task.result?.[key];
9038
+ if (resultEntry?.path) {
9039
+ return resultEntry.path;
9040
+ }
9041
+ throw new Error(`No file entry with key '${key}' in task '${task.id}'`);
9042
+ },
9043
+ flush(data, tasks) {
9044
+ const suffix = deboSuffix(data);
9045
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9046
+ for (const task of tasks) {
9047
+ for (const file of task.files ?? []) {
9048
+ if (!file.validation_result) continue;
9049
+ const src = file.path + suffix;
9050
+ if (!existsSync(src)) continue;
9051
+ mkdirSync(dirname(file.path), { recursive: true });
9052
+ renameSync(src, file.path);
9053
+ file.flushed_at = now;
9054
+ }
9055
+ }
9056
+ return Promise.resolve();
9057
+ },
9058
+ done() {
9059
+ return { archive: true };
9060
+ },
9061
+ cleanup(data) {
9062
+ const suffix = deboSuffix(data);
9063
+ const dirs = /* @__PURE__ */ new Set();
9064
+ for (const task of data.tasks) {
9065
+ for (const file of task.files ?? []) {
9066
+ dirs.add(dirname(file.path));
9067
+ }
9068
+ }
9069
+ for (const dir of dirs) {
9070
+ if (!existsSync(dir)) continue;
8840
9071
  try {
8841
- const content = readFileSync(lockPath, "utf-8");
8842
- const lockTime = parseInt(content, 10);
8843
- if (Date.now() - lockTime > STALE_LOCK_MS) {
8844
- try {
8845
- unlinkSync(lockPath);
8846
- } catch {
9072
+ const entries = readdirSync(dir);
9073
+ for (const entry of entries) {
9074
+ if (entry.endsWith(suffix)) {
9075
+ unlinkSync(resolve(dir, entry));
8847
9076
  }
8848
9077
  }
8849
9078
  } catch {
8850
9079
  }
8851
9080
  }
8852
- try {
8853
- writeFileSync(lockPath, String(Date.now()), { flag: "wx" });
8854
- return lockPath;
8855
- } catch (err) {
8856
- if (err.code !== "EEXIST") {
8857
- throw err;
9081
+ },
9082
+ async onTransition(from, to, ctx) {
9083
+ const declared = ctx.data.stages ? Object.keys(ctx.data.stages) : [];
9084
+ if (declared.includes(from)) {
9085
+ const stageTasks = ctx.data.tasks.filter((t) => t.stage === from);
9086
+ if (stageTasks.length > 0) {
9087
+ await directEngine.flush(ctx.data, stageTasks);
8858
9088
  }
8859
9089
  }
9090
+ if (from === "finalizing" && to === "done") {
9091
+ return { archive: true };
9092
+ }
9093
+ return {};
8860
9094
  }
8861
- throw new Error(
8862
- `Failed to acquire lock on ${filePath} after ${MAX_RETRIES} attempts. Another process may be writing to this file.`
8863
- );
9095
+ };
9096
+
9097
+ // src/engines/index.ts
9098
+ var engines = {
9099
+ direct: directEngine
9100
+ };
9101
+
9102
+ // src/workflow-lifecycle.ts
9103
+ var IMPLICIT_STAGES = {
9104
+ _after_declared: ["committed"],
9105
+ _before_done: ["finalizing", "done"]
9106
+ };
9107
+ function buildLifecycleOrder(stages) {
9108
+ const declared = Object.keys(stages).filter((s) => (stages[s].steps?.length ?? 0) > 0);
9109
+ return ["created", "planned", ...declared, ...IMPLICIT_STAGES._after_declared, ...IMPLICIT_STAGES._before_done];
8864
9110
  }
8865
- function releaseLock(lockPath) {
8866
- try {
8867
- unlinkSync(lockPath);
8868
- } catch {
8869
- }
9111
+ function getNextStage(current, stages) {
9112
+ const order = buildLifecycleOrder(stages);
9113
+ const currentIdx = order.indexOf(current);
9114
+ if (currentIdx === -1 || currentIdx >= order.length - 1) return null;
9115
+ return order[currentIdx + 1] ?? null;
8870
9116
  }
8871
- async function withLockAsync(filePath, fn) {
8872
- const lockPath = acquireLock(filePath);
8873
- try {
8874
- return await fn();
8875
- } finally {
8876
- releaseLock(lockPath);
9117
+ function getNextStep(currentStage, _completedStep, tasks) {
9118
+ const pending = tasks.find((t) => t.stage === currentStage && t.status !== "done");
9119
+ return pending?.step ?? null;
9120
+ }
9121
+ function checkStageParams(stage, stages, provided) {
9122
+ const def = stages[stage];
9123
+ if (!def?.params) return null;
9124
+ const unfulfilled = {};
9125
+ for (const [key, param] of Object.entries(def.params)) {
9126
+ if (provided[key] === void 0) {
9127
+ unfulfilled[key] = param;
9128
+ }
8877
9129
  }
9130
+ return Object.keys(unfulfilled).length > 0 ? unfulfilled : null;
9131
+ }
9132
+ function interpolatePrompt(prompt, state) {
9133
+ return prompt.replace(/\{(\w+)\}/g, (_match, key) => {
9134
+ const value = state[key];
9135
+ return value !== void 0 ? String(value) : `{${key}}`;
9136
+ });
8878
9137
  }
8879
9138
 
8880
9139
  // src/workflow-resolve.ts
@@ -8970,7 +9229,7 @@ function pullType(ref2, baseFilePath, baseFileSchemas, definitions, input, visit
8970
9229
  srcFile = baseFilePath;
8971
9230
  srcSchemas = baseFileSchemas;
8972
9231
  } else {
8973
- const r = resolveSchemaRef(ref2, baseFilePath, input.skillsRoot);
9232
+ const r = resolveSchemaRef(ref2, baseFilePath, input.skillsRoot, input.sources);
8974
9233
  typeName = r.typeName;
8975
9234
  schema = r.schema;
8976
9235
  srcFile = r.schemaFilePath;
@@ -9084,10 +9343,10 @@ function parseSchemaExtension(filePath) {
9084
9343
  }
9085
9344
  return hasAny ? ext : null;
9086
9345
  }
9087
- function resolveRefsInExtension(obj, sourceFilePath, skillsRoot, schemas) {
9346
+ function resolveRefsInExtension(obj, sourceFilePath, skillsRoot, schemas, sources) {
9088
9347
  for (const [key, value] of Object.entries(obj)) {
9089
9348
  if (key === "$ref" && typeof value === "string") {
9090
- const { typeName, schema } = resolveSchemaRef(value, sourceFilePath, skillsRoot);
9349
+ const { typeName, schema } = resolveSchemaRef(value, sourceFilePath, skillsRoot, sources);
9091
9350
  schemas[typeName] = schema;
9092
9351
  delete obj.$ref;
9093
9352
  Object.assign(obj, schema);
@@ -9096,11 +9355,11 @@ function resolveRefsInExtension(obj, sourceFilePath, skillsRoot, schemas) {
9096
9355
  if (Array.isArray(value)) {
9097
9356
  for (const item of value) {
9098
9357
  if (item && typeof item === "object") {
9099
- resolveRefsInExtension(item, sourceFilePath, skillsRoot, schemas);
9358
+ resolveRefsInExtension(item, sourceFilePath, skillsRoot, schemas, sources);
9100
9359
  }
9101
9360
  }
9102
9361
  } else if (value && typeof value === "object") {
9103
- resolveRefsInExtension(value, sourceFilePath, skillsRoot, schemas);
9362
+ resolveRefsInExtension(value, sourceFilePath, skillsRoot, schemas, sources);
9104
9363
  }
9105
9364
  }
9106
9365
  }
@@ -9184,9 +9443,9 @@ function computeMergedSchema(baseResult, input) {
9184
9443
  if (ext.constrains) {
9185
9444
  throw new Error(`Blueprint '${bp2}' uses constrains: \u2014 only rules may constrain schemas`);
9186
9445
  }
9187
- if (ext.extends) resolveRefsInExtension(ext.extends, bp2, input.skillsRoot, input.schemas);
9446
+ if (ext.extends) resolveRefsInExtension(ext.extends, bp2, input.skillsRoot, input.schemas, input.sources);
9188
9447
  if (ext.provides && typeof ext.provides === "object") {
9189
- resolveRefsInExtension(ext.provides, bp2, input.skillsRoot, input.schemas);
9448
+ resolveRefsInExtension(ext.provides, bp2, input.skillsRoot, input.schemas, input.sources);
9190
9449
  }
9191
9450
  blueprintExts.push({ path: bp2, ext });
9192
9451
  }
@@ -9197,11 +9456,17 @@ function computeMergedSchema(baseResult, input) {
9197
9456
  if (typeof ext.provides === "string") {
9198
9457
  ext.provides = void 0;
9199
9458
  }
9200
- if (ext.extends) resolveRefsInExtension(ext.extends, rule, input.skillsRoot, input.schemas);
9459
+ if (ext.extends) resolveRefsInExtension(ext.extends, rule, input.skillsRoot, input.schemas, input.sources);
9201
9460
  if (ext.provides && typeof ext.provides === "object") {
9202
- resolveRefsInExtension(ext.provides, rule, input.skillsRoot, input.schemas);
9461
+ resolveRefsInExtension(
9462
+ ext.provides,
9463
+ rule,
9464
+ input.skillsRoot,
9465
+ input.schemas,
9466
+ input.sources
9467
+ );
9203
9468
  }
9204
- if (ext.constrains) resolveRefsInExtension(ext.constrains, rule, input.skillsRoot, input.schemas);
9469
+ if (ext.constrains) resolveRefsInExtension(ext.constrains, rule, input.skillsRoot, input.schemas, input.sources);
9205
9470
  ruleExts.push({ path: rule, ext });
9206
9471
  }
9207
9472
  }
@@ -9270,6 +9535,22 @@ function computeMergedSchema(baseResult, input) {
9270
9535
  }
9271
9536
 
9272
9537
  // src/workflow-resolve.ts
9538
+ function pluginSources(sources) {
9539
+ return (sources ?? []).filter((s) => s.origin === "plugin");
9540
+ }
9541
+ function toSourcePattern(globPattern) {
9542
+ return globPattern.replace(/^skills\/\*\*\//, "**/");
9543
+ }
9544
+ function derivePluginArtifactName(source2, filePath) {
9545
+ const rel = relative(source2.root, filePath).replace(/\\/g, "/");
9546
+ const parts = rel.split("/");
9547
+ const artifact = (parts[parts.length - 1] ?? "").replace(/\.md$/, "");
9548
+ if (parts.length >= 3) {
9549
+ const concern = parts[0];
9550
+ return `${source2.name}:${concern}:${artifact}`;
9551
+ }
9552
+ return `${source2.name}:${artifact}`;
9553
+ }
9273
9554
  function loadSchemaFile(schemaFilePath) {
9274
9555
  if (!existsSync(schemaFilePath)) {
9275
9556
  throw new Error(`Schema file not found: ${schemaFilePath}`);
@@ -9291,7 +9572,21 @@ function loadSchemaFile(schemaFilePath) {
9291
9572
  }
9292
9573
  return schemas;
9293
9574
  }
9294
- function resolveSchemaRef(ref2, taskFilePath, skillsRoot) {
9575
+ function reanchorRelativeRefToPluginSource(resolvedPath, sources) {
9576
+ const plugins = pluginSources(sources);
9577
+ if (plugins.length === 0) return void 0;
9578
+ const byName = new Map(plugins.map((s) => [s.name, s]));
9579
+ const segments = resolvedPath.replace(/\\/g, "/").split("/");
9580
+ for (let i = segments.length - 2; i >= 0; i--) {
9581
+ const source2 = byName.get(segments[i]);
9582
+ if (!source2) continue;
9583
+ const rest = segments.slice(i + 1).join("/");
9584
+ const candidate = resolve(source2.root, rest);
9585
+ if (existsSync(candidate)) return candidate;
9586
+ }
9587
+ return void 0;
9588
+ }
9589
+ function resolveSchemaRef(ref2, taskFilePath, skillsRoot, sources) {
9295
9590
  const hashIdx = ref2.indexOf("#/");
9296
9591
  if (hashIdx === -1) {
9297
9592
  throw new Error(`Invalid $ref '${ref2}' \u2014 must contain '#/' fragment (e.g. ../schemas.yml#/TypeName)`);
@@ -9301,8 +9596,19 @@ function resolveSchemaRef(ref2, taskFilePath, skillsRoot) {
9301
9596
  let schemaFilePath;
9302
9597
  if (filePart.startsWith(".") || filePart.startsWith("/")) {
9303
9598
  schemaFilePath = resolve(dirname(taskFilePath), filePart);
9599
+ if (!existsSync(schemaFilePath)) {
9600
+ const reanchored = reanchorRelativeRefToPluginSource(schemaFilePath, sources);
9601
+ if (reanchored) schemaFilePath = reanchored;
9602
+ }
9304
9603
  } else {
9305
- schemaFilePath = resolve(skillsRoot, filePart);
9604
+ const skillName = filePart.split("/")[0] ?? "";
9605
+ const pluginSource = pluginSources(sources).find((s) => s.name === skillName);
9606
+ if (pluginSource) {
9607
+ const rest = filePart.slice(skillName.length + 1);
9608
+ schemaFilePath = resolve(pluginSource.root, rest);
9609
+ } else {
9610
+ schemaFilePath = resolve(skillsRoot, filePart);
9611
+ }
9306
9612
  }
9307
9613
  const fileSchemas = loadSchemaFile(schemaFilePath);
9308
9614
  if (!(typeName in fileSchemas)) {
@@ -9311,10 +9617,10 @@ function resolveSchemaRef(ref2, taskFilePath, skillsRoot) {
9311
9617
  }
9312
9618
  return { typeName, schema: fileSchemas[typeName], schemaFilePath, fileSchemas };
9313
9619
  }
9314
- function collectLocalRefsFromSchema(node, fileSchemas, schemas, visited, schemaFilePath, skillsRoot) {
9620
+ function collectLocalRefsFromSchema(node, fileSchemas, schemas, visited, schemaFilePath, skillsRoot, sources) {
9315
9621
  if (Array.isArray(node)) {
9316
9622
  for (const item of node)
9317
- collectLocalRefsFromSchema(item, fileSchemas, schemas, visited, schemaFilePath, skillsRoot);
9623
+ collectLocalRefsFromSchema(item, fileSchemas, schemas, visited, schemaFilePath, skillsRoot, sources);
9318
9624
  return;
9319
9625
  }
9320
9626
  if (!node || typeof node !== "object") return;
@@ -9326,10 +9632,18 @@ function collectLocalRefsFromSchema(node, fileSchemas, schemas, visited, schemaF
9326
9632
  if (typeName && !(typeName in schemas) && typeName in fileSchemas && !visited.has(typeName)) {
9327
9633
  visited.add(typeName);
9328
9634
  schemas[typeName] = fileSchemas[typeName];
9329
- collectLocalRefsFromSchema(fileSchemas[typeName], fileSchemas, schemas, visited, schemaFilePath, skillsRoot);
9635
+ collectLocalRefsFromSchema(
9636
+ fileSchemas[typeName],
9637
+ fileSchemas,
9638
+ schemas,
9639
+ visited,
9640
+ schemaFilePath,
9641
+ skillsRoot,
9642
+ sources
9643
+ );
9330
9644
  }
9331
9645
  } else if (ref2.includes("#/")) {
9332
- const resolved = resolveSchemaRef(ref2, schemaFilePath, skillsRoot);
9646
+ const resolved = resolveSchemaRef(ref2, schemaFilePath, skillsRoot, sources);
9333
9647
  obj.$ref = `#/${resolved.typeName}`;
9334
9648
  if (!(resolved.typeName in schemas) && !visited.has(resolved.typeName)) {
9335
9649
  visited.add(resolved.typeName);
@@ -9340,16 +9654,17 @@ function collectLocalRefsFromSchema(node, fileSchemas, schemas, visited, schemaF
9340
9654
  schemas,
9341
9655
  visited,
9342
9656
  resolved.schemaFilePath,
9343
- skillsRoot
9657
+ skillsRoot,
9658
+ sources
9344
9659
  );
9345
9660
  }
9346
9661
  }
9347
9662
  }
9348
9663
  for (const value of Object.values(obj)) {
9349
- collectLocalRefsFromSchema(value, fileSchemas, schemas, visited, schemaFilePath, skillsRoot);
9664
+ collectLocalRefsFromSchema(value, fileSchemas, schemas, visited, schemaFilePath, skillsRoot, sources);
9350
9665
  }
9351
9666
  }
9352
- function resolveSchemasForTasks(tasks, skillsRoot, schemas) {
9667
+ function resolveSchemasForTasks(tasks, skillsRoot, schemas, sources) {
9353
9668
  for (const task of tasks) {
9354
9669
  if (!task.result || !task.task_file) continue;
9355
9670
  const taskFilePath = task.task_file;
@@ -9359,25 +9674,40 @@ function resolveSchemasForTasks(tasks, skillsRoot, schemas) {
9359
9674
  if (!resultProperties) continue;
9360
9675
  for (const [key, resultDecl] of Object.entries(resultProperties)) {
9361
9676
  const originalRef = resultDecl.$ref;
9362
- resolveRefsInDeclaration(resultDecl, taskFilePath, skillsRoot, schemas);
9677
+ resolveRefsInDeclaration(resultDecl, taskFilePath, skillsRoot, schemas, sources);
9363
9678
  if (task.result[key] && originalRef) {
9364
9679
  const { typeName, schema, fileSchemas, schemaFilePath } = resolveSchemaRef(
9365
9680
  originalRef,
9366
9681
  taskFilePath,
9367
- skillsRoot
9682
+ skillsRoot,
9683
+ sources
9368
9684
  );
9369
9685
  schemas[typeName] = schema;
9370
- collectLocalRefsFromSchema(schema, fileSchemas, schemas, /* @__PURE__ */ new Set([typeName]), schemaFilePath, skillsRoot);
9686
+ collectLocalRefsFromSchema(
9687
+ schema,
9688
+ fileSchemas,
9689
+ schemas,
9690
+ /* @__PURE__ */ new Set([typeName]),
9691
+ schemaFilePath,
9692
+ skillsRoot,
9693
+ sources
9694
+ );
9371
9695
  task.result[key].schema = schema;
9372
9696
  }
9373
9697
  if (task.result[key]?.schema && typeof task.result[key].schema === "object") {
9374
- rewriteRefsInSchema(task.result[key].schema, taskFilePath, skillsRoot, schemas);
9698
+ rewriteRefsInSchema(
9699
+ task.result[key].schema,
9700
+ taskFilePath,
9701
+ skillsRoot,
9702
+ schemas,
9703
+ sources
9704
+ );
9375
9705
  }
9376
9706
  }
9377
9707
  if (taskFm.each) {
9378
9708
  for (const eachValue of Object.values(taskFm.each)) {
9379
9709
  if (eachValue && typeof eachValue === "object") {
9380
- resolveRefsInDeclaration(eachValue, taskFilePath, skillsRoot, schemas);
9710
+ resolveRefsInDeclaration(eachValue, taskFilePath, skillsRoot, schemas, sources);
9381
9711
  }
9382
9712
  }
9383
9713
  }
@@ -9388,9 +9718,17 @@ function resolveSchemasForTasks(tasks, skillsRoot, schemas) {
9388
9718
  schema: refSchema,
9389
9719
  fileSchemas,
9390
9720
  schemaFilePath
9391
- } = resolveSchemaRef(ref2, taskFilePath, skillsRoot);
9721
+ } = resolveSchemaRef(ref2, taskFilePath, skillsRoot, sources);
9392
9722
  schemas[typeName] = refSchema;
9393
- collectLocalRefsFromSchema(refSchema, fileSchemas, schemas, /* @__PURE__ */ new Set([typeName]), schemaFilePath, skillsRoot);
9723
+ collectLocalRefsFromSchema(
9724
+ refSchema,
9725
+ fileSchemas,
9726
+ schemas,
9727
+ /* @__PURE__ */ new Set([typeName]),
9728
+ schemaFilePath,
9729
+ skillsRoot,
9730
+ sources
9731
+ );
9394
9732
  }
9395
9733
  }
9396
9734
  let inventory;
@@ -9415,26 +9753,31 @@ function deriveSkillsRootFromTaskFile(taskFilePath) {
9415
9753
  if (idx === -1) return void 0;
9416
9754
  return taskFilePath.slice(0, idx + marker.length - 1);
9417
9755
  }
9418
- function resolveRefsInDeclaration(obj, taskFilePath, skillsRoot, schemas) {
9756
+ function resolveRefsInDeclaration(obj, taskFilePath, skillsRoot, schemas, sources) {
9419
9757
  if (obj.$ref && typeof obj.$ref === "string") {
9420
- const { typeName, schema, fileSchemas, schemaFilePath } = resolveSchemaRef(obj.$ref, taskFilePath, skillsRoot);
9758
+ const { typeName, schema, fileSchemas, schemaFilePath } = resolveSchemaRef(
9759
+ obj.$ref,
9760
+ taskFilePath,
9761
+ skillsRoot,
9762
+ sources
9763
+ );
9421
9764
  schemas[typeName] = schema;
9422
- collectLocalRefsFromSchema(schema, fileSchemas, schemas, /* @__PURE__ */ new Set([typeName]), schemaFilePath, skillsRoot);
9765
+ collectLocalRefsFromSchema(schema, fileSchemas, schemas, /* @__PURE__ */ new Set([typeName]), schemaFilePath, skillsRoot, sources);
9423
9766
  obj.$ref = `#/${typeName}`;
9424
9767
  }
9425
9768
  for (const value of Object.values(obj)) {
9426
9769
  if (Array.isArray(value)) {
9427
9770
  for (const item of value) {
9428
9771
  if (item && typeof item === "object") {
9429
- resolveRefsInDeclaration(item, taskFilePath, skillsRoot, schemas);
9772
+ resolveRefsInDeclaration(item, taskFilePath, skillsRoot, schemas, sources);
9430
9773
  }
9431
9774
  }
9432
9775
  } else if (value && typeof value === "object") {
9433
- resolveRefsInDeclaration(value, taskFilePath, skillsRoot, schemas);
9776
+ resolveRefsInDeclaration(value, taskFilePath, skillsRoot, schemas, sources);
9434
9777
  }
9435
9778
  }
9436
9779
  }
9437
- function rewriteRefsInSchema(obj, taskFilePath, skillsRoot, schemas) {
9780
+ function rewriteRefsInSchema(obj, taskFilePath, skillsRoot, schemas, sources) {
9438
9781
  if (obj.$ref && typeof obj.$ref === "string") {
9439
9782
  const ref2 = obj.$ref;
9440
9783
  if (ref2.includes("#/") && !ref2.startsWith("#/")) {
@@ -9443,9 +9786,9 @@ function rewriteRefsInSchema(obj, taskFilePath, skillsRoot, schemas) {
9443
9786
  schema,
9444
9787
  fileSchemas,
9445
9788
  schemaFilePath: refFilePath
9446
- } = resolveSchemaRef(ref2, taskFilePath, skillsRoot);
9789
+ } = resolveSchemaRef(ref2, taskFilePath, skillsRoot, sources);
9447
9790
  schemas[typeName] = schema;
9448
- collectLocalRefsFromSchema(schema, fileSchemas, schemas, /* @__PURE__ */ new Set([typeName]), refFilePath, skillsRoot);
9791
+ collectLocalRefsFromSchema(schema, fileSchemas, schemas, /* @__PURE__ */ new Set([typeName]), refFilePath, skillsRoot, sources);
9449
9792
  obj.$ref = `#/${typeName}`;
9450
9793
  }
9451
9794
  }
@@ -9453,11 +9796,11 @@ function rewriteRefsInSchema(obj, taskFilePath, skillsRoot, schemas) {
9453
9796
  if (Array.isArray(value)) {
9454
9797
  for (const item of value) {
9455
9798
  if (item && typeof item === "object") {
9456
- rewriteRefsInSchema(item, taskFilePath, skillsRoot, schemas);
9799
+ rewriteRefsInSchema(item, taskFilePath, skillsRoot, schemas, sources);
9457
9800
  }
9458
9801
  }
9459
9802
  } else if (value && typeof value === "object") {
9460
- rewriteRefsInSchema(value, taskFilePath, skillsRoot, schemas);
9803
+ rewriteRefsInSchema(value, taskFilePath, skillsRoot, schemas, sources);
9461
9804
  }
9462
9805
  }
9463
9806
  }
@@ -9478,19 +9821,21 @@ function getWorkflowStageDefinitions(fm3) {
9478
9821
  function getWorkflowTitle(fm3) {
9479
9822
  return fm3.title ?? fm3.workflow?.title ?? "";
9480
9823
  }
9481
- function deriveArtifactName(filePath, agentsDir, frontmatter) {
9824
+ function deriveArtifactName(filePath, agentsDir, frontmatter, source2) {
9482
9825
  if (frontmatter?.type && typeof frontmatter.type === "string") {
9483
9826
  if (frontmatter.name && typeof frontmatter.name === "string" && frontmatter.name.includes(":")) {
9484
9827
  return frontmatter.name;
9485
9828
  }
9486
9829
  const bpName = frontmatter.name ?? filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/, "") ?? "";
9487
- const rel2 = relative(resolve(agentsDir, "skills"), filePath).replace(/\\/g, "/");
9488
- const skill2 = rel2.split("/")[0] ?? "";
9830
+ const skill2 = source2 ? source2.name : relative(resolve(agentsDir, "skills"), filePath).replace(/\\/g, "/").split("/")[0] ?? "";
9489
9831
  return `${skill2}:blueprints:${frontmatter.type}/${bpName}`;
9490
9832
  }
9491
9833
  if (frontmatter?.name && typeof frontmatter.name === "string") {
9492
9834
  return frontmatter.name;
9493
9835
  }
9836
+ if (source2) {
9837
+ return derivePluginArtifactName(source2, filePath);
9838
+ }
9494
9839
  const rel = relative(resolve(agentsDir, "skills"), filePath).replace(/\\/g, "/");
9495
9840
  const parts = rel.split("/");
9496
9841
  const skill = parts[0] ?? "";
@@ -9603,30 +9948,23 @@ function buildEnvMap(config) {
9603
9948
  env["DESIGNBOOK_EXTENSION_SKILLS"] = getExtensionSkillIds(extensions);
9604
9949
  return env;
9605
9950
  }
9606
- function buildWorktreeEnvMap(envMap, worktreePath, rootDir) {
9607
- const remapped = { ...envMap };
9608
- remapped["DESIGNBOOK_WORKSPACE"] = worktreePath;
9609
- for (const [key, value] of Object.entries(envMap)) {
9610
- if (!key.startsWith("DESIGNBOOK_DIRS_")) continue;
9611
- const relPath = relative(rootDir, value);
9612
- remapped[key] = resolve(worktreePath, relPath);
9613
- }
9614
- if (envMap["DESIGNBOOK_HOME"]) {
9615
- remapped["DESIGNBOOK_HOME"] = resolve(worktreePath, relative(rootDir, envMap["DESIGNBOOK_HOME"]));
9951
+ function resolveFiles(globPattern, context2, config, agentsDir, requireWhen = true, sources) {
9952
+ const results = [];
9953
+ const candidates = [];
9954
+ for (const filePath of globSync(globPattern, { cwd: agentsDir, absolute: true })) {
9955
+ candidates.push({ filePath });
9616
9956
  }
9617
- if (envMap["DESIGNBOOK_DATA"]) {
9618
- remapped["DESIGNBOOK_DATA"] = resolve(worktreePath, relative(rootDir, envMap["DESIGNBOOK_DATA"]));
9957
+ const sourcePattern = toSourcePattern(globPattern);
9958
+ for (const source2 of pluginSources(sources)) {
9959
+ for (const filePath of globSync(sourcePattern, { cwd: source2.root, absolute: true })) {
9960
+ candidates.push({ filePath, source: source2 });
9961
+ }
9619
9962
  }
9620
- return remapped;
9621
- }
9622
- function resolveFiles(globPattern, context2, config, agentsDir, requireWhen = true) {
9623
- const results = [];
9624
- const paths = globSync(globPattern, { cwd: agentsDir, absolute: true });
9625
- for (const filePath of paths) {
9963
+ for (const { filePath, source: source2 } of candidates) {
9626
9964
  const frontmatter = parseFrontmatter(filePath);
9627
9965
  const trigger = frontmatter?.trigger;
9628
9966
  const filter = frontmatter?.filter;
9629
- const name = deriveArtifactName(filePath, agentsDir, frontmatter);
9967
+ const name = deriveArtifactName(filePath, agentsDir, frontmatter, source2);
9630
9968
  const triggerCount = trigger ? Object.keys(trigger).length : 0;
9631
9969
  const filterCount = filter ? Object.keys(filter).length : 0;
9632
9970
  if (triggerCount + filterCount === 0) {
@@ -9702,10 +10040,19 @@ function deduplicateByNameAs(files, agentsDir, warnings = []) {
9702
10040
  });
9703
10041
  return standalone;
9704
10042
  }
9705
- function resolveTaskFilesRich(stage, config, agentsDir) {
10043
+ function resolveExplicitTaskInPluginSources(skillName, taskName, sources) {
10044
+ const source2 = pluginSources(sources).find((s) => s.name === skillName);
10045
+ if (!source2) return void 0;
10046
+ const flat = resolve(source2.root, "tasks", `${taskName}.md`);
10047
+ if (existsSync(flat)) return flat;
10048
+ const nested = globSync(`**/tasks/${taskName}.md`, { cwd: source2.root, absolute: true });
10049
+ if (nested.length > 0) return nested[0];
10050
+ return void 0;
10051
+ }
10052
+ function resolveTaskFilesRich(stage, config, agentsDir, sources) {
9706
10053
  const context2 = buildRuntimeContext(stage);
9707
10054
  const enrichedConfig = buildEnrichedConfig(config);
9708
- const broadMatches = resolveFiles("skills/**/tasks/*.md", context2, enrichedConfig, agentsDir, true);
10055
+ const broadMatches = resolveFiles("skills/**/tasks/*.md", context2, enrichedConfig, agentsDir, true, sources);
9709
10056
  if (stage.includes(":")) {
9710
10057
  if (broadMatches.length > 0) {
9711
10058
  broadMatches.sort((a, b) => b.specificity - a.specificity);
@@ -9723,12 +10070,29 @@ function resolveTaskFilesRich(stage, config, agentsDir) {
9723
10070
  const name = deriveArtifactName(taskPath, agentsDir, frontmatter);
9724
10071
  return [{ path: taskPath, name, specificity: 0, frontmatter }];
9725
10072
  }
10073
+ const pluginTaskPath = resolveExplicitTaskInPluginSources(skillName, taskName, sources);
10074
+ if (pluginTaskPath) {
10075
+ console.warn(
10076
+ `[designbook] task "${pluginTaskPath}" resolved by filename \u2014 add trigger.steps: [${stage}] to frontmatter`
10077
+ );
10078
+ const frontmatter = parseFrontmatter(pluginTaskPath);
10079
+ const pluginSource = pluginSources(sources).find((s) => s.name === skillName);
10080
+ const name = deriveArtifactName(pluginTaskPath, agentsDir, frontmatter, pluginSource);
10081
+ return [{ path: pluginTaskPath, name, specificity: 0, frontmatter }];
10082
+ }
9726
10083
  return [];
9727
10084
  }
9728
10085
  if (broadMatches.length > 0) {
9729
10086
  return deduplicateByNameAs(broadMatches, agentsDir);
9730
10087
  }
9731
- const filenameMatches = resolveFiles(`skills/**/tasks/${stage}.md`, context2, enrichedConfig, agentsDir, false);
10088
+ const filenameMatches = resolveFiles(
10089
+ `skills/**/tasks/${stage}.md`,
10090
+ context2,
10091
+ enrichedConfig,
10092
+ agentsDir,
10093
+ false,
10094
+ sources
10095
+ );
9732
10096
  if (filenameMatches.length > 0) {
9733
10097
  for (const m of filenameMatches) {
9734
10098
  console.warn(`[designbook] task "${m.path}" resolved by filename \u2014 add trigger.steps: [${stage}] to frontmatter`);
@@ -9737,22 +10101,22 @@ function resolveTaskFilesRich(stage, config, agentsDir) {
9737
10101
  }
9738
10102
  return [];
9739
10103
  }
9740
- function matchRuleFiles(stage, config, agentsDir, extraConditions, effectiveDomains) {
10104
+ function matchRuleFiles(stage, config, agentsDir, extraConditions, effectiveDomains, sources) {
9741
10105
  const context2 = buildRuntimeContext(stage);
9742
10106
  if (effectiveDomains && effectiveDomains.length > 0) {
9743
10107
  context2["domain"] = effectiveDomains;
9744
10108
  }
9745
10109
  const enrichedConfig = buildEnrichedConfig(config);
9746
- const matches = resolveFiles("skills/**/rules/*.md", context2, enrichedConfig, agentsDir, true);
10110
+ const matches = resolveFiles("skills/**/rules/*.md", context2, enrichedConfig, agentsDir, true, sources);
9747
10111
  return matches.map((m) => m.path);
9748
10112
  }
9749
- function matchBlueprintFiles(stage, config, agentsDir, extraConditions, effectiveDomains) {
10113
+ function matchBlueprintFiles(stage, config, agentsDir, extraConditions, effectiveDomains, sources) {
9750
10114
  const context2 = buildRuntimeContext(stage);
9751
10115
  if (effectiveDomains && effectiveDomains.length > 0) {
9752
10116
  context2["domain"] = effectiveDomains;
9753
10117
  }
9754
10118
  const enrichedConfig = buildEnrichedConfig(config);
9755
- const matches = resolveFiles("skills/**/blueprints/*.md", context2, enrichedConfig, agentsDir, true);
10119
+ const matches = resolveFiles("skills/**/blueprints/*.md", context2, enrichedConfig, agentsDir, true, sources);
9756
10120
  const byKey = /* @__PURE__ */ new Map();
9757
10121
  for (const m of matches) {
9758
10122
  const type = m.frontmatter?.["type"];
@@ -9908,9 +10272,9 @@ function resolveConfigForStep(stage, rawConfig) {
9908
10272
  config_instructions: [...explicitInstructions, ...extensionSkills]
9909
10273
  };
9910
10274
  }
9911
- function resolveParamsRef(params, taskFilePath, skillsRoot) {
10275
+ function resolveParamsRef(params, taskFilePath, skillsRoot, sources) {
9912
10276
  const ref2 = params["$ref"];
9913
- const { schema } = resolveSchemaRef(ref2, taskFilePath, skillsRoot);
10277
+ const { schema } = resolveSchemaRef(ref2, taskFilePath, skillsRoot, sources);
9914
10278
  const schemaObj = schema;
9915
10279
  const schemaProps = schemaObj.properties;
9916
10280
  if (!schemaProps) {
@@ -9997,7 +10361,7 @@ function generateTaskId(stage, params, _schemaParams, index = 0) {
9997
10361
  const hash = createHash("sha256").update(stage + JSON.stringify(params) + index).digest("hex").slice(0, 6);
9998
10362
  return `${baseName}-${hash}`;
9999
10363
  }
10000
- async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir) {
10364
+ async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir, sources) {
10001
10365
  const wfFm = parseFrontmatter(workflowFilePath);
10002
10366
  const stageDefs = wfFm ? getWorkflowStageDefinitions(wfFm) : void 0;
10003
10367
  const allSteps = wfFm ? getWorkflowSteps(wfFm) : void 0;
@@ -10010,9 +10374,9 @@ async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir)
10010
10374
  const expectedParams = {};
10011
10375
  const collectedSchemas = {};
10012
10376
  for (const step of allSteps ?? []) {
10013
- let resolvedTaskFiles = resolveTaskFilesRich(step, config, agentsDir);
10377
+ let resolvedTaskFiles = resolveTaskFilesRich(step, config, agentsDir, sources);
10014
10378
  if (resolvedTaskFiles.length === 0 && !step.includes(":") && workflowId) {
10015
- resolvedTaskFiles = resolveTaskFilesRich(`${workflowId}:${step}`, config, agentsDir);
10379
+ resolvedTaskFiles = resolveTaskFilesRich(`${workflowId}:${step}`, config, agentsDir, sources);
10016
10380
  }
10017
10381
  if (resolvedTaskFiles.length === 0) {
10018
10382
  console.debug(`[Designbook] workflow: step "${step}" skipped \u2014 no matching task file`);
@@ -10051,13 +10415,13 @@ async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir)
10051
10415
  const effectiveDomainsArg = effectiveDomains.length > 0 ? effectiveDomains : void 0;
10052
10416
  const ruleFiles = [];
10053
10417
  for (const s of stepsToMatch) {
10054
- for (const r of matchRuleFiles(s, config, agentsDir, void 0, effectiveDomainsArg)) {
10418
+ for (const r of matchRuleFiles(s, config, agentsDir, void 0, effectiveDomainsArg, sources)) {
10055
10419
  if (!ruleFiles.includes(r)) ruleFiles.push(r);
10056
10420
  }
10057
10421
  }
10058
10422
  const blueprintFiles = [];
10059
10423
  for (const s of stepsToMatch) {
10060
- for (const b of matchBlueprintFiles(s, config, agentsDir, void 0, effectiveDomainsArg)) {
10424
+ for (const b of matchBlueprintFiles(s, config, agentsDir, void 0, effectiveDomainsArg, sources)) {
10061
10425
  if (!blueprintFiles.includes(b)) blueprintFiles.push(b);
10062
10426
  }
10063
10427
  }
@@ -10086,7 +10450,8 @@ async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir)
10086
10450
  ruleFiles,
10087
10451
  skillsRoot: resolve(agentsDir, "skills"),
10088
10452
  schemas: collectedSchemas,
10089
- refMap
10453
+ refMap,
10454
+ sources
10090
10455
  });
10091
10456
  }
10092
10457
  }
@@ -10097,7 +10462,8 @@ async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir)
10097
10462
  result: taskFmForSchema?.result,
10098
10463
  taskFilePath: primaryTaskFile,
10099
10464
  skillsRoot: resolve(agentsDir, "skills"),
10100
- envMap
10465
+ envMap,
10466
+ sources
10101
10467
  });
10102
10468
  if (mergedSchema) {
10103
10469
  for (const [resultKey, composedSchema] of Object.entries(mergedSchema)) {
@@ -10141,7 +10507,7 @@ async function resolveAllStages(workflowFilePath, config, rawConfig, agentsDir)
10141
10507
  let params = taskFm?.params;
10142
10508
  if (!params) continue;
10143
10509
  if ("$ref" in params) {
10144
- params = resolveParamsRef(params, taskFile, skillsRoot);
10510
+ params = resolveParamsRef(params, taskFile, skillsRoot, sources);
10145
10511
  }
10146
10512
  validateParamFormats(params, taskFile);
10147
10513
  const properties = params.properties ?? {};
@@ -10240,292 +10606,30 @@ async function generateTaskTitle(stage, params, schemaParams, explicitTitle) {
10240
10606
  }
10241
10607
  return words;
10242
10608
  }
10243
-
10244
- // src/engines/git-worktree.ts
10245
- function checkPreflightClean(rootDir, outputsRoot) {
10246
- try {
10247
- const relOutputsRoot = relative(rootDir, outputsRoot);
10248
- const output = execFileSync("git", ["-C", rootDir, "status", "--porcelain", "--", relOutputsRoot], {
10249
- encoding: "utf-8"
10250
- });
10251
- if (!output.trim()) return { clean: true, files: [] };
10252
- const files = output.split("\n").filter((line) => line.length >= 4).map((line) => line.slice(3).trim()).filter(Boolean);
10253
- return { clean: false, files };
10254
- } catch {
10255
- return { clean: true, files: [] };
10256
- }
10609
+ function getExpr(value) {
10610
+ return typeof value === "string" ? value : value.expr;
10257
10611
  }
10258
- function createGitWorktree(worktreePath, branchName, rootDir) {
10259
- execFileSync("git", ["worktree", "add", worktreePath, "-b", branchName], { cwd: rootDir });
10260
- }
10261
- var gitWorktreeEngine = {
10262
- writeFile(_data, task, key, content) {
10263
- const fileEntry = (task.files ?? []).find((f) => f.key === key);
10264
- if (!fileEntry) throw new Error(`No file entry with key '${key}' in task '${task.id}'`);
10265
- mkdirSync(dirname(fileEntry.path), { recursive: true });
10266
- writeFileSync(fileEntry.path, content);
10267
- return { path: fileEntry.path };
10268
- },
10269
- getStagedPath(_data, task, key) {
10270
- const fileEntry = (task.files ?? []).find((f) => f.key === key);
10271
- if (!fileEntry) throw new Error(`No file entry with key '${key}' in task '${task.id}'`);
10272
- return fileEntry.path;
10273
- },
10274
- flush() {
10275
- },
10276
- setup(ctx) {
10277
- if (ctx.dryRun) {
10278
- return { envMap: buildWorktreeEnvMap(ctx.envMap, ctx.worktreePath, ctx.rootDir) };
10279
- }
10280
- const branchName = `workflow/${ctx.workflowName}`;
10281
- const preflight = checkPreflightClean(ctx.rootDir, ctx.workspace);
10282
- if (!preflight.clean) {
10283
- const files = preflight.files.map((f) => ` \xB7 ${f}`).join("\n");
10284
- throw new Error(`Uncommitted changes in workspace \u2014 commit these files before running workflow plan:
10285
- ${files}`);
10286
- }
10287
- createGitWorktree(ctx.worktreePath, branchName, ctx.rootDir);
10288
- return {
10289
- envMap: buildWorktreeEnvMap(ctx.envMap, ctx.worktreePath, ctx.rootDir),
10290
- write_root: ctx.worktreePath,
10291
- worktree_branch: branchName
10292
- };
10293
- },
10294
- commit(data) {
10295
- if (!data.write_root || !data.worktree_branch || !data.workspace_root) return;
10296
- if (!existsSync(data.write_root)) return;
10297
- const outputTasks = data.tasks.filter((t) => t.type !== "test");
10298
- const outputPaths = outputTasks.flatMap((t) => (t.files ?? []).map((f) => f.path));
10299
- if (outputPaths.length > 0) {
10300
- const relPaths = outputPaths.map((p) => relative(data.write_root, p)).filter((r) => !r.startsWith(".."));
10301
- if (relPaths.length > 0) {
10302
- execFileSync("git", ["-C", data.write_root, "add", ...relPaths]);
10303
- }
10304
- }
10305
- execFileSync("git", ["-C", data.write_root, "commit", "--allow-empty", "-m", `workflow: ${data.worktree_branch}`]);
10306
- execFileSync("git", ["worktree", "remove", data.write_root, "--force"], { cwd: data.workspace_root });
10307
- },
10308
- merge(data) {
10309
- const branch = data.worktree_branch;
10310
- const rootDir = data.workspace_root;
10311
- if (!branch) throw new Error("Workflow has no worktree_branch \u2014 nothing to merge");
10312
- if (!rootDir) throw new Error("Workflow has no workspace_root");
10313
- execFileSync("git", ["-C", rootDir, "merge", "--squash", branch]);
10314
- execFileSync("git", ["-C", rootDir, "commit", "-m", `workflow: ${data.workflow}`]);
10315
- execFileSync("git", ["-C", rootDir, "branch", "-D", branch]);
10316
- return { branch, workspace_root: rootDir };
10317
- },
10318
- done() {
10319
- return { archive: false };
10320
- },
10321
- cleanup(data) {
10322
- if (data.write_root && existsSync(data.write_root)) {
10323
- try {
10324
- execFileSync("git", ["worktree", "remove", data.write_root, "--force"], { cwd: data.workspace_root });
10325
- } catch {
10326
- }
10327
- }
10328
- if (data.worktree_branch && data.workspace_root) {
10329
- try {
10330
- execFileSync("git", ["-C", data.workspace_root, "branch", "-D", data.worktree_branch]);
10331
- } catch {
10332
- }
10333
- }
10334
- },
10335
- onTransition(from, to, ctx) {
10336
- const { data } = ctx;
10337
- switch (`${from}\u2192${to}`) {
10338
- case "planned\u2192execute": {
10339
- if (!ctx.setupCtx) return {};
10340
- const result = gitWorktreeEngine.setup(ctx.setupCtx);
10341
- return {
10342
- envMap: result.envMap,
10343
- write_root: result.write_root,
10344
- worktree_branch: result.worktree_branch
10345
- };
10346
- }
10347
- case "execute\u2192committed": {
10348
- gitWorktreeEngine.commit(data);
10349
- return {};
10350
- }
10351
- case "committed\u2192test": {
10352
- return {};
10353
- }
10354
- case "finalizing\u2192done": {
10355
- if (!ctx.params?.merge_approved) {
10356
- return {
10357
- requires: {
10358
- merge_approved: {
10359
- type: "boolean",
10360
- prompt: "Branch {branch} mergen?"
10361
- }
10362
- }
10363
- };
10364
- }
10365
- const mergeResult = gitWorktreeEngine.merge(data);
10366
- return { archive: true, branch: mergeResult.branch };
10367
- }
10368
- default:
10369
- if (to === "abandoned") {
10370
- gitWorktreeEngine.cleanup(data);
10371
- }
10372
- return {};
10373
- }
10374
- }
10375
- };
10376
- function deboSuffix(data) {
10377
- if (!data.workflow_id) throw new Error("workflow_id is required for direct engine stash-at-target");
10378
- return `.${data.workflow_id}.debo`;
10379
- }
10380
- function stashPath(data, fileEntry) {
10381
- return fileEntry.path + deboSuffix(data);
10382
- }
10383
- var directEngine = {
10384
- setup(ctx) {
10385
- return { envMap: ctx.envMap };
10386
- },
10387
- writeFile(data, task, key, content) {
10388
- const fileEntry = (task.files ?? []).find((f) => f.key === key);
10389
- if (!fileEntry) throw new Error(`No file entry with key '${key}' in task '${task.id}'`);
10390
- const path = stashPath(data, fileEntry);
10391
- mkdirSync(dirname(path), { recursive: true });
10392
- writeFileSync(path, content);
10393
- return { path };
10394
- },
10395
- getStagedPath(data, task, key) {
10396
- const fileEntry = (task.files ?? []).find((f) => f.key === key);
10397
- if (fileEntry) return stashPath(data, fileEntry);
10398
- const resultEntry = task.result?.[key];
10399
- if (resultEntry?.path) {
10400
- return resultEntry.path;
10401
- }
10402
- throw new Error(`No file entry with key '${key}' in task '${task.id}'`);
10403
- },
10404
- flush(data, tasks) {
10405
- const suffix = deboSuffix(data);
10406
- const now = (/* @__PURE__ */ new Date()).toISOString();
10407
- for (const task of tasks) {
10408
- for (const file of task.files ?? []) {
10409
- if (!file.validation_result) continue;
10410
- const src = file.path + suffix;
10411
- if (!existsSync(src)) continue;
10412
- mkdirSync(dirname(file.path), { recursive: true });
10413
- renameSync(src, file.path);
10414
- file.flushed_at = now;
10415
- }
10416
- }
10417
- return Promise.resolve();
10418
- },
10419
- commit() {
10420
- },
10421
- merge() {
10422
- throw new Error('Engine "direct" does not support merge \u2014 files are already written to real paths');
10423
- },
10424
- done() {
10425
- return { archive: true };
10426
- },
10427
- cleanup(data) {
10428
- const suffix = deboSuffix(data);
10429
- const dirs = /* @__PURE__ */ new Set();
10430
- for (const task of data.tasks) {
10431
- for (const file of task.files ?? []) {
10432
- dirs.add(dirname(file.path));
10433
- }
10434
- }
10435
- for (const dir of dirs) {
10436
- if (!existsSync(dir)) continue;
10437
- try {
10438
- const entries = readdirSync(dir);
10439
- for (const entry of entries) {
10440
- if (entry.endsWith(suffix)) {
10441
- unlinkSync(resolve(dir, entry));
10442
- }
10443
- }
10444
- } catch {
10445
- }
10446
- }
10447
- },
10448
- async onTransition(from, to, ctx) {
10449
- const declared = ctx.data.stages ? Object.keys(ctx.data.stages) : [];
10450
- if (declared.includes(from)) {
10451
- const stageTasks = ctx.data.tasks.filter((t) => t.stage === from);
10452
- if (stageTasks.length > 0) {
10453
- await directEngine.flush(ctx.data, stageTasks);
10454
- }
10455
- }
10456
- if (from === "finalizing" && to === "done") {
10457
- return { archive: true };
10458
- }
10459
- return {};
10460
- }
10461
- };
10462
-
10463
- // src/engines/index.ts
10464
- var engines = {
10465
- "git-worktree": gitWorktreeEngine,
10466
- direct: directEngine
10467
- };
10468
-
10469
- // src/workflow-lifecycle.ts
10470
- var IMPLICIT_STAGES = {
10471
- _after_declared: ["committed"],
10472
- _before_done: ["finalizing", "done"]
10473
- };
10474
- function buildLifecycleOrder(stages) {
10475
- const declared = Object.keys(stages).filter((s) => (stages[s].steps?.length ?? 0) > 0);
10476
- return ["created", "planned", ...declared, ...IMPLICIT_STAGES._after_declared, ...IMPLICIT_STAGES._before_done];
10477
- }
10478
- function getNextStage(current, stages) {
10479
- const order = buildLifecycleOrder(stages);
10480
- const currentIdx = order.indexOf(current);
10481
- if (currentIdx === -1 || currentIdx >= order.length - 1) return null;
10482
- return order[currentIdx + 1] ?? null;
10483
- }
10484
- function getNextStep(currentStage, _completedStep, tasks) {
10485
- const pending = tasks.find((t) => t.stage === currentStage && t.status !== "done");
10486
- return pending?.step ?? null;
10487
- }
10488
- function checkStageParams(stage, stages, provided) {
10489
- const def = stages[stage];
10490
- if (!def?.params) return null;
10491
- const unfulfilled = {};
10492
- for (const [key, param] of Object.entries(def.params)) {
10493
- if (provided[key] === void 0) {
10494
- unfulfilled[key] = param;
10495
- }
10496
- }
10497
- return Object.keys(unfulfilled).length > 0 ? unfulfilled : null;
10498
- }
10499
- function interpolatePrompt(prompt, state) {
10500
- return prompt.replace(/\{(\w+)\}/g, (_match, key) => {
10501
- const value = state[key];
10502
- return value !== void 0 ? String(value) : `{${key}}`;
10503
- });
10504
- }
10505
- function getExpr(value) {
10506
- return typeof value === "string" ? value : value.expr;
10507
- }
10508
- async function resolveEach(each, scope) {
10509
- const entries = Object.entries(each);
10510
- if (entries.length === 0) return [];
10511
- let combos = [{}];
10512
- for (const [binding, raw] of entries) {
10513
- const expr = getExpr(raw);
10514
- const next = [];
10515
- for (const combo of combos) {
10516
- const innerScope = { ...scope, ...combo };
10517
- const value = await jsonata9(expr).evaluate(innerScope);
10518
- if (value === void 0 || value === null) continue;
10519
- const items = Array.isArray(value) ? value : [value];
10520
- for (const item of items) {
10521
- next.push({ ...combo, [binding]: item });
10522
- }
10523
- }
10524
- combos = next;
10525
- if (combos.length === 0) break;
10526
- }
10527
- const total = combos.length;
10528
- return combos.map((c, i) => ({ ...c, $i: i, $total: total }));
10612
+ async function resolveEach(each, scope) {
10613
+ const entries = Object.entries(each);
10614
+ if (entries.length === 0) return [];
10615
+ let combos = [{}];
10616
+ for (const [binding, raw] of entries) {
10617
+ const expr = getExpr(raw);
10618
+ const next = [];
10619
+ for (const combo of combos) {
10620
+ const innerScope = { ...scope, ...combo };
10621
+ const value = await jsonata9(expr).evaluate(innerScope);
10622
+ if (value === void 0 || value === null) continue;
10623
+ const items = Array.isArray(value) ? value : [value];
10624
+ for (const item of items) {
10625
+ next.push({ ...combo, [binding]: item });
10626
+ }
10627
+ }
10628
+ combos = next;
10629
+ if (combos.length === 0) break;
10630
+ }
10631
+ const total = combos.length;
10632
+ return combos.map((c, i) => ({ ...c, $i: i, $total: total }));
10529
10633
  }
10530
10634
 
10531
10635
  // src/workflow.ts
@@ -10581,9 +10685,8 @@ function renderSubmitResultsHint(taskId, results) {
10581
10685
  }
10582
10686
 
10583
10687
  // src/workflow.ts
10584
- function resolveWorkflowEngine(data) {
10585
- const name = data.engine ?? (data.worktree_branch ? "git-worktree" : "direct");
10586
- return engines[name];
10688
+ function resolveWorkflowEngine(_data) {
10689
+ return engines["direct"];
10587
10690
  }
10588
10691
  function timestamp() {
10589
10692
  const d = /* @__PURE__ */ new Date();
@@ -10613,11 +10716,6 @@ function readWorkflow(filePath) {
10613
10716
  throw new Error(`Workflow not found: ${name}`);
10614
10717
  }
10615
10718
  const data = load(readFileSync(filePath, "utf-8"));
10616
- const legacy = data;
10617
- if (!data.workspace_root && legacy.root_dir) {
10618
- data.workspace_root = legacy.root_dir;
10619
- }
10620
- delete legacy.root_dir;
10621
10719
  resolveWorkflowPaths(data);
10622
10720
  return data;
10623
10721
  }
@@ -10709,7 +10807,7 @@ function workflowAbandon(dataDir, name) {
10709
10807
  const changesDir = resolve(dataDir, "workflows", "changes", name);
10710
10808
  const filePath = resolve(changesDir, "tasks.yml");
10711
10809
  const data = readWorkflow(filePath);
10712
- const engine = resolveWorkflowEngine(data);
10810
+ const engine = resolveWorkflowEngine();
10713
10811
  if (engine) engine.cleanup(data);
10714
10812
  data.status = "incomplete";
10715
10813
  data.completed_at = timestamp();
@@ -10804,16 +10902,6 @@ function resolveWorkflowPaths(data) {
10804
10902
  function relativizeWorkflowPaths(data) {
10805
10903
  return transformWorkflowPaths(data, relativizePath);
10806
10904
  }
10807
- function workflowMerge(dataDir, name) {
10808
- const changesDir = resolve(dataDir, "workflows", "changes", name);
10809
- const filePath = resolve(changesDir, "tasks.yml");
10810
- const data = readWorkflow(filePath);
10811
- const engine = resolveWorkflowEngine(data);
10812
- if (!engine) throw new Error(`Unknown engine: "${data.engine}"`);
10813
- const result = engine.merge(data);
10814
- archiveAndCascade(dataDir, name, data);
10815
- return result;
10816
- }
10817
10905
  function workflowList(dataDir, workflowId, includeArchived) {
10818
10906
  const changesDir = resolve(dataDir, "workflows", "changes");
10819
10907
  const active = existsSync(changesDir) ? readdirSync(changesDir).filter((name) => name.startsWith(`${workflowId}-`)) : [];
@@ -11055,455 +11143,439 @@ async function expandTasksFromParams(stageLoaded, stages, params, existingTasks,
11055
11143
  async function workflowDone(dataDir, name, taskId, loaded, options) {
11056
11144
  const changesDir = resolve(dataDir, "workflows", "changes", name);
11057
11145
  const filePath = resolve(changesDir, "tasks.yml");
11058
- return withLockAsync(filePath, async () => {
11059
- const data = readWorkflow(filePath);
11060
- data._changesDir = changesDir;
11061
- const task = data.tasks.find((t) => t.id === taskId);
11062
- if (!task) {
11063
- throw new Error(`Task not found: ${taskId} (available: ${data.tasks.map((t) => t.id).join(", ")})`);
11064
- }
11065
- if (task.status === "done") {
11066
- throw new Error(`Task '${taskId}' is already done`);
11067
- }
11068
- if (options?.data && task.result) {
11069
- const dataPayload = options.data;
11070
- const declaredKeys = new Set(Object.keys(task.result));
11071
- const unknownKeys = Object.keys(dataPayload).filter((k) => !declaredKeys.has(k));
11072
- if (unknownKeys.length > 0) {
11073
- throw new Error(
11074
- `Unknown result key(s) in --data: ${unknownKeys.join(", ")}. Valid keys: ${[...declaredKeys].join(", ")}`
11146
+ const data = readWorkflow(filePath);
11147
+ data._changesDir = changesDir;
11148
+ const task = data.tasks.find((t) => t.id === taskId);
11149
+ if (!task) {
11150
+ throw new Error(`Task not found: ${taskId} (available: ${data.tasks.map((t) => t.id).join(", ")})`);
11151
+ }
11152
+ if (task.status === "done") {
11153
+ throw new Error(`Task '${taskId}' is already done`);
11154
+ }
11155
+ if (options?.data && task.result) {
11156
+ const dataPayload = options.data;
11157
+ const declaredKeys = new Set(Object.keys(task.result));
11158
+ const unknownKeys = Object.keys(dataPayload).filter((k) => !declaredKeys.has(k));
11159
+ if (unknownKeys.length > 0) {
11160
+ throw new Error(
11161
+ `Unknown result key(s) in --data: ${unknownKeys.join(", ")}. Valid keys: ${[...declaredKeys].join(", ")}`
11162
+ );
11163
+ }
11164
+ const validationErrors = [];
11165
+ for (const [key, rawValue] of Object.entries(dataPayload)) {
11166
+ const value = rawValue;
11167
+ const resultEntry = task.result[key];
11168
+ if (!resultEntry) continue;
11169
+ if (resultEntry.path) {
11170
+ if (/\{\{[^}]*\}\}/.test(resultEntry.path)) {
11171
+ throw new Error(`result path template unresolved: ${resultEntry.path}`);
11172
+ }
11173
+ const { serializeForPath: serializeForPath2 } = await Promise.resolve().then(() => (init_workflow_serialize(), workflow_serialize_exports));
11174
+ const serialized = serializeForPath2(
11175
+ resultEntry.path,
11176
+ value,
11177
+ resultEntry.schema
11075
11178
  );
11076
- }
11077
- const validationErrors = [];
11078
- for (const [key, rawValue] of Object.entries(dataPayload)) {
11079
- const value = rawValue;
11080
- const resultEntry = task.result[key];
11081
- if (!resultEntry) continue;
11082
- if (resultEntry.path) {
11083
- if (/\{\{[^}]*\}\}/.test(resultEntry.path)) {
11084
- throw new Error(`result path template unresolved: ${resultEntry.path}`);
11085
- }
11086
- const { serializeForPath: serializeForPath2 } = await Promise.resolve().then(() => (init_workflow_serialize(), workflow_serialize_exports));
11087
- const serialized = serializeForPath2(
11088
- resultEntry.path,
11089
- value,
11090
- resultEntry.schema
11091
- );
11092
- const engine2 = resolveWorkflowEngine(data);
11093
- const dataWithDir = data;
11094
- dataWithDir._changesDir = changesDir;
11095
- if (!task.files?.some((f) => f.key === key)) {
11096
- task.files = task.files ?? [];
11097
- task.files.push({ path: resultEntry.path, key, validators: resultEntry.validators ?? [] });
11098
- }
11099
- let writtenPath;
11100
- if (engine2?.writeFile) {
11101
- const result = engine2.writeFile(dataWithDir, task, key, serialized);
11102
- writtenPath = result.path;
11103
- } else {
11104
- const dir = dirname(resultEntry.path);
11105
- mkdirSync(dir, { recursive: true });
11106
- writeFileSync(resultEntry.path, serialized);
11107
- writtenPath = resultEntry.path;
11108
- }
11109
- const config = options.config ?? { data: dataDir, technology: "html", extensions: [] };
11110
- const schemaErrors = await validateResultEntry(resultEntry, value, data.schemas, config, "data");
11111
- const semanticErrors = resultEntry.validators && resultEntry.validators.length > 0 ? await validateResultEntry(
11112
- { ...resultEntry, schema: void 0 },
11113
- writtenPath,
11114
- data.schemas,
11115
- config,
11116
- "file"
11117
- ) : [];
11118
- const errors = [...schemaErrors, ...semanticErrors];
11119
- if (errors.length > 0) {
11120
- validationErrors.push(...errors.map((e) => `${key}: ${e}`));
11121
- resultEntry.valid = false;
11122
- resultEntry.error = errors.join("; ");
11123
- } else {
11124
- resultEntry.valid = true;
11125
- delete resultEntry.error;
11126
- }
11127
- resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11128
- const fileEntry = task.files?.find((f) => f.key === key);
11129
- if (fileEntry) {
11130
- fileEntry.validation_result = {
11131
- file: resultEntry.path,
11132
- type: key,
11133
- valid: resultEntry.valid,
11134
- error: resultEntry.error,
11135
- last_validated: resultEntry.last_validated,
11136
- last_passed: resultEntry.valid ? resultEntry.last_validated : void 0,
11137
- last_failed: !resultEntry.valid ? resultEntry.last_validated : void 0
11138
- };
11139
- }
11140
- if (resultEntry.flush === "immediate" && writtenPath !== resultEntry.path) {
11141
- mkdirSync(dirname(resultEntry.path), { recursive: true });
11142
- renameSync(writtenPath, resultEntry.path);
11143
- resultEntry.flushed_at = (/* @__PURE__ */ new Date()).toISOString();
11144
- if (fileEntry) fileEntry.flushed_at = resultEntry.flushed_at;
11145
- }
11179
+ const engine2 = resolveWorkflowEngine();
11180
+ const dataWithDir = data;
11181
+ dataWithDir._changesDir = changesDir;
11182
+ if (!task.files?.some((f) => f.key === key)) {
11183
+ task.files = task.files ?? [];
11184
+ task.files.push({ path: resultEntry.path, key, validators: resultEntry.validators ?? [] });
11185
+ }
11186
+ let writtenPath;
11187
+ if (engine2?.writeFile) {
11188
+ const result = engine2.writeFile(dataWithDir, task, key, serialized);
11189
+ writtenPath = result.path;
11146
11190
  } else {
11147
- const config = options.config ?? { data: dataDir, technology: "html", extensions: [] };
11148
- const errors = await validateResultEntry(resultEntry, value, data.schemas, config, "data");
11149
- if (errors.length > 0) {
11150
- validationErrors.push(...errors.map((e) => `${key}: ${e}`));
11151
- resultEntry.valid = false;
11152
- resultEntry.error = errors.join("; ");
11153
- } else {
11154
- resultEntry.value = value;
11155
- resultEntry.valid = true;
11156
- delete resultEntry.error;
11157
- }
11158
- resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11191
+ const dir = dirname(resultEntry.path);
11192
+ mkdirSync(dir, { recursive: true });
11193
+ writeFileSync(resultEntry.path, serialized);
11194
+ writtenPath = resultEntry.path;
11195
+ }
11196
+ const config = options.config ?? { data: dataDir, technology: "html", extensions: [] };
11197
+ const schemaErrors = await validateResultEntry(resultEntry, value, data.schemas, config, "data");
11198
+ const semanticErrors = resultEntry.validators && resultEntry.validators.length > 0 ? await validateResultEntry(
11199
+ { ...resultEntry, schema: void 0 },
11200
+ writtenPath,
11201
+ data.schemas,
11202
+ config,
11203
+ "file"
11204
+ ) : [];
11205
+ const errors = [...schemaErrors, ...semanticErrors];
11206
+ if (errors.length > 0) {
11207
+ validationErrors.push(...errors.map((e) => `${key}: ${e}`));
11208
+ resultEntry.valid = false;
11209
+ resultEntry.error = errors.join("; ");
11210
+ } else {
11211
+ resultEntry.valid = true;
11212
+ delete resultEntry.error;
11213
+ }
11214
+ resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11215
+ const fileEntry = task.files?.find((f) => f.key === key);
11216
+ if (fileEntry) {
11217
+ fileEntry.validation_result = {
11218
+ file: resultEntry.path,
11219
+ type: key,
11220
+ valid: resultEntry.valid,
11221
+ error: resultEntry.error,
11222
+ last_validated: resultEntry.last_validated,
11223
+ last_passed: resultEntry.valid ? resultEntry.last_validated : void 0,
11224
+ last_failed: !resultEntry.valid ? resultEntry.last_validated : void 0
11225
+ };
11159
11226
  }
11160
- }
11161
- if (validationErrors.length > 0) {
11162
- writeWorkflowAtomic(filePath, data);
11163
- return {
11164
- archived: false,
11165
- data,
11166
- response: {
11167
- stage: data.current_stage ?? "unknown",
11168
- validation_errors: validationErrors
11169
- }
11170
- };
11171
- }
11172
- }
11173
- if (task.result) {
11174
- const directWrites = [];
11175
- for (const [key, resultEntry] of Object.entries(task.result)) {
11176
- if (!resultEntry.path) continue;
11177
- if (resultEntry.valid !== void 0) continue;
11178
- if (resultEntry.submission === "direct") continue;
11179
- if (/\{[a-zA-Z]\w*\}/.test(resultEntry.path)) continue;
11180
- if (existsSync(resultEntry.path)) {
11181
- directWrites.push(` \xB7 result \`${key}\` at \`${resultEntry.path}\``);
11227
+ if (resultEntry.flush === "immediate" && writtenPath !== resultEntry.path) {
11228
+ mkdirSync(dirname(resultEntry.path), { recursive: true });
11229
+ renameSync(writtenPath, resultEntry.path);
11230
+ resultEntry.flushed_at = (/* @__PURE__ */ new Date()).toISOString();
11231
+ if (fileEntry) fileEntry.flushed_at = resultEntry.flushed_at;
11182
11232
  }
11183
- }
11184
- if (directWrites.length > 0) {
11185
- const hintResults = Object.fromEntries(
11186
- Object.entries(task.result).map(([k, v]) => {
11187
- const schema = v.schema;
11188
- return [
11189
- k,
11190
- {
11191
- path: v.path,
11192
- submission: v.submission,
11193
- flush: v.flush,
11194
- $ref: typeof schema?.$ref === "string" ? schema.$ref : void 0,
11195
- type: typeof schema?.type === "string" ? schema.type : void 0
11196
- }
11197
- ];
11198
- })
11199
- );
11200
- const hint = renderSubmitResultsHint(taskId, hintResults) ?? "";
11201
- throw new Error(
11202
- `Cannot mark '${taskId}' as done \u2014 ${directWrites.length} file result(s) were written directly instead of via \`workflow done --data\`:
11203
- ` + directWrites.join("\n") + "\n\n" + hint
11204
- );
11233
+ } else {
11234
+ const config = options.config ?? { data: dataDir, technology: "html", extensions: [] };
11235
+ const errors = await validateResultEntry(resultEntry, value, data.schemas, config, "data");
11236
+ if (errors.length > 0) {
11237
+ validationErrors.push(...errors.map((e) => `${key}: ${e}`));
11238
+ resultEntry.valid = false;
11239
+ resultEntry.error = errors.join("; ");
11240
+ } else {
11241
+ resultEntry.value = value;
11242
+ resultEntry.valid = true;
11243
+ delete resultEntry.error;
11244
+ }
11245
+ resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11205
11246
  }
11206
11247
  }
11207
- if (task.result) {
11208
- for (const [, resultEntry] of Object.entries(task.result)) {
11209
- if (resultEntry.valid !== void 0) continue;
11210
- if (resultEntry.path) continue;
11211
- if (resultEntry.schema && typeof resultEntry.schema === "object" && "default" in resultEntry.schema) {
11212
- resultEntry.value = resultEntry.schema.default;
11213
- resultEntry.valid = true;
11214
- resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11248
+ if (validationErrors.length > 0) {
11249
+ writeWorkflowAtomic(filePath, data);
11250
+ return {
11251
+ archived: false,
11252
+ data,
11253
+ response: {
11254
+ stage: data.current_stage ?? "unknown",
11255
+ validation_errors: validationErrors
11215
11256
  }
11257
+ };
11258
+ }
11259
+ }
11260
+ if (task.result) {
11261
+ const directWrites = [];
11262
+ for (const [key, resultEntry] of Object.entries(task.result)) {
11263
+ if (!resultEntry.path) continue;
11264
+ if (resultEntry.valid !== void 0) continue;
11265
+ if (resultEntry.submission === "direct") continue;
11266
+ if (/\{[a-zA-Z]\w*\}/.test(resultEntry.path)) continue;
11267
+ if (existsSync(resultEntry.path)) {
11268
+ directWrites.push(` \xB7 result \`${key}\` at \`${resultEntry.path}\``);
11216
11269
  }
11217
11270
  }
11218
- const hasUnresolvedPlaceholder = (f) => /\{[a-zA-Z]\w*\}/.test(f.path);
11219
- const taskFiles = task.files ?? [];
11220
- if (taskFiles.length > 0 && taskFiles.every((f) => hasUnresolvedPlaceholder(f)) && !taskFiles.some((f) => f.validation_result)) {
11271
+ if (directWrites.length > 0) {
11272
+ const hintResults = Object.fromEntries(
11273
+ Object.entries(task.result).map(([k, v]) => {
11274
+ const schema = v.schema;
11275
+ return [
11276
+ k,
11277
+ {
11278
+ path: v.path,
11279
+ submission: v.submission,
11280
+ flush: v.flush,
11281
+ $ref: typeof schema?.$ref === "string" ? schema.$ref : void 0,
11282
+ type: typeof schema?.type === "string" ? schema.type : void 0
11283
+ }
11284
+ ];
11285
+ })
11286
+ );
11287
+ const hint = renderSubmitResultsHint(taskId, hintResults) ?? "";
11221
11288
  throw new Error(
11222
- `Cannot mark '${taskId}' as done \u2014 all file paths have unresolved placeholders and no files were written:
11223
- ` + taskFiles.map((f) => ` \xB7 \`${f.path}\``).join("\n")
11289
+ `Cannot mark '${taskId}' as done \u2014 ${directWrites.length} file result(s) were written directly instead of via \`workflow done --data\`:
11290
+ ` + directWrites.join("\n") + "\n\n" + hint
11224
11291
  );
11225
11292
  }
11226
- const notWritten = taskFiles.filter((f) => !f.validation_result && !hasUnresolvedPlaceholder(f));
11227
- if (notWritten.length > 0) {
11228
- throw new Error(
11229
- `Cannot mark '${taskId}' as done \u2014 ${notWritten.length} file(s) not yet written:
11293
+ }
11294
+ if (task.result) {
11295
+ for (const [, resultEntry] of Object.entries(task.result)) {
11296
+ if (resultEntry.valid !== void 0) continue;
11297
+ if (resultEntry.path) continue;
11298
+ if (resultEntry.schema && typeof resultEntry.schema === "object" && "default" in resultEntry.schema) {
11299
+ resultEntry.value = resultEntry.schema.default;
11300
+ resultEntry.valid = true;
11301
+ resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11302
+ }
11303
+ }
11304
+ }
11305
+ const hasUnresolvedPlaceholder = (f) => /\{[a-zA-Z]\w*\}/.test(f.path);
11306
+ const taskFiles = task.files ?? [];
11307
+ if (taskFiles.length > 0 && taskFiles.every((f) => hasUnresolvedPlaceholder(f)) && !taskFiles.some((f) => f.validation_result)) {
11308
+ throw new Error(
11309
+ `Cannot mark '${taskId}' as done \u2014 all file paths have unresolved placeholders and no files were written:
11310
+ ` + taskFiles.map((f) => ` \xB7 \`${f.path}\``).join("\n")
11311
+ );
11312
+ }
11313
+ const notWritten = taskFiles.filter((f) => !f.validation_result && !hasUnresolvedPlaceholder(f));
11314
+ if (notWritten.length > 0) {
11315
+ throw new Error(
11316
+ `Cannot mark '${taskId}' as done \u2014 ${notWritten.length} file(s) not yet written:
11230
11317
  ` + notWritten.map((f) => ` \xB7 file \`${f.key}\` not yet written`).join("\n")
11318
+ );
11319
+ }
11320
+ const failed = (task.files ?? []).filter((f) => f.validation_result?.valid === false);
11321
+ if (failed.length > 0) {
11322
+ throw new Error(
11323
+ `Cannot mark '${taskId}' as done \u2014 ${failed.length} file(s) have errors:
11324
+ ` + failed.map((f) => ` \xB7 file \`${f.key}\` has errors: ${f.validation_result?.error ?? "invalid"}`).join("\n")
11325
+ );
11326
+ }
11327
+ if (task.result) {
11328
+ const missingResults = Object.entries(task.result).filter(
11329
+ ([, r]) => r.valid === void 0 && r.path !== void 0 && r.required !== false
11330
+ );
11331
+ if (missingResults.length > 0) {
11332
+ throw new Error(
11333
+ `Cannot mark '${taskId}' as done \u2014 ${missingResults.length} result(s) not yet written:
11334
+ ` + missingResults.map(([k]) => ` \xB7 result \`${k}\` not yet written`).join("\n")
11231
11335
  );
11232
11336
  }
11233
- const failed = (task.files ?? []).filter((f) => f.validation_result?.valid === false);
11234
- if (failed.length > 0) {
11337
+ const missingDataResults = Object.entries(task.result).filter(
11338
+ ([, r]) => r.valid === void 0 && r.path === void 0 && r.required !== false
11339
+ );
11340
+ if (missingDataResults.length > 0) {
11235
11341
  throw new Error(
11236
- `Cannot mark '${taskId}' as done \u2014 ${failed.length} file(s) have errors:
11237
- ` + failed.map((f) => ` \xB7 file \`${f.key}\` has errors: ${f.validation_result?.error ?? "invalid"}`).join("\n")
11342
+ `Cannot mark '${taskId}' as done \u2014 ${missingDataResults.length} data result(s) not yet written:
11343
+ ` + missingDataResults.map(([k]) => ` \xB7 result \`${k}\` not yet written`).join("\n")
11238
11344
  );
11239
11345
  }
11240
- if (task.result) {
11241
- const missingResults = Object.entries(task.result).filter(
11242
- ([, r]) => r.valid === void 0 && r.path !== void 0 && r.required !== false
11243
- );
11244
- if (missingResults.length > 0) {
11245
- throw new Error(
11246
- `Cannot mark '${taskId}' as done \u2014 ${missingResults.length} result(s) not yet written:
11247
- ` + missingResults.map(([k]) => ` \xB7 result \`${k}\` not yet written`).join("\n")
11248
- );
11249
- }
11250
- const missingDataResults = Object.entries(task.result).filter(
11251
- ([, r]) => r.valid === void 0 && r.path === void 0 && r.required !== false
11252
- );
11253
- if (missingDataResults.length > 0) {
11254
- throw new Error(
11255
- `Cannot mark '${taskId}' as done \u2014 ${missingDataResults.length} data result(s) not yet written:
11256
- ` + missingDataResults.map(([k]) => ` \xB7 result \`${k}\` not yet written`).join("\n")
11257
- );
11258
- }
11259
- const failedResults = Object.entries(task.result).filter(([, r]) => r.valid === false);
11260
- if (failedResults.length > 0) {
11261
- throw new Error(
11262
- `Cannot mark '${taskId}' as done \u2014 ${failedResults.length} result(s) have errors:
11346
+ const failedResults = Object.entries(task.result).filter(([, r]) => r.valid === false);
11347
+ if (failedResults.length > 0) {
11348
+ throw new Error(
11349
+ `Cannot mark '${taskId}' as done \u2014 ${failedResults.length} result(s) have errors:
11263
11350
  ` + failedResults.map(([k, r]) => ` \xB7 result \`${k}\` has errors: ${r.error ?? "invalid"}`).join("\n")
11264
- );
11265
- }
11351
+ );
11266
11352
  }
11267
- task.status = "done";
11268
- if (!task.started_at) task.started_at = timestamp();
11269
- task.completed_at = timestamp();
11270
- if (options?.summary) task.summary = options.summary;
11271
- if (loaded) {
11272
- const stepName = task.step;
11273
- if (stepName) {
11274
- data.stage_loaded = data.stage_loaded ?? {};
11275
- if (!data.stage_loaded[stepName]) {
11276
- data.stage_loaded[stepName] = {
11277
- task_file: loaded.task_file ?? "",
11278
- rules: loaded.rules ?? [],
11279
- blueprints: loaded.blueprints ?? [],
11280
- config_rules: loaded.config_rules ?? [],
11281
- config_instructions: loaded.config_instructions ?? [],
11282
- ...loaded.isolate ? { isolate: true } : {}
11283
- };
11284
- }
11353
+ }
11354
+ task.status = "done";
11355
+ if (!task.started_at) task.started_at = timestamp();
11356
+ task.completed_at = timestamp();
11357
+ if (options?.summary) task.summary = options.summary;
11358
+ if (loaded) {
11359
+ const stepName = task.step;
11360
+ if (stepName) {
11361
+ data.stage_loaded = data.stage_loaded ?? {};
11362
+ if (!data.stage_loaded[stepName]) {
11363
+ data.stage_loaded[stepName] = {
11364
+ task_file: loaded.task_file ?? "",
11365
+ rules: loaded.rules ?? [],
11366
+ blueprints: loaded.blueprints ?? [],
11367
+ config_rules: loaded.config_rules ?? [],
11368
+ config_instructions: loaded.config_instructions ?? [],
11369
+ ...loaded.isolate ? { isolate: true } : {}
11370
+ };
11285
11371
  }
11286
11372
  }
11287
- const engine = resolveWorkflowEngine(data);
11288
- const hasGroupedStages = data.stages && typeof data.stages === "object" && !Array.isArray(data.stages);
11289
- if (hasGroupedStages && data.current_stage) {
11290
- const stages = data.stages;
11291
- const currentStage = data.current_stage;
11292
- const configForExpansion = options?.config ?? { data: dataDir, technology: "html", extensions: [] };
11293
- const getMissingStageSteps = (stageName) => {
11294
- const stageDef = stages[stageName];
11295
- if (!stageDef?.steps) return [];
11296
- const stageHasTrackedSteps = data.tasks.some((t) => t.stage === stageName && typeof t.step === "string" && t.step.length > 0) || stageDef.steps.some((step) => Boolean(data.stage_loaded?.[step]));
11297
- if (!stageHasTrackedSteps) return [];
11298
- return stageDef.steps.filter((step) => !data.tasks.some((t) => t.stage === stageName && t.step === step));
11299
- };
11300
- const expandStageIfNeeded = async (stageName) => {
11301
- const missingBefore = getMissingStageSteps(stageName);
11302
- if (missingBefore.length === 0) return {};
11303
- if (!data.stage_loaded) return {};
11304
- const stageLoaded = data.stage_loaded;
11305
- const scopeEnvMap = data.env_map ?? {};
11306
- const stageDef = stages[stageName];
11307
- if (!stageDef) return {};
11308
- let expandedFromScope;
11309
- let resolverErrors;
11310
- const resolveResult = await resolveStageTaskParams(
11311
- stageLoaded,
11312
- stageDef,
11313
- data.params ?? {},
11314
- configForExpansion
11373
+ }
11374
+ const engine = resolveWorkflowEngine();
11375
+ const hasGroupedStages = data.stages && typeof data.stages === "object" && !Array.isArray(data.stages);
11376
+ if (hasGroupedStages && data.current_stage) {
11377
+ const stages = data.stages;
11378
+ const currentStage = data.current_stage;
11379
+ const configForExpansion = options?.config ?? { data: dataDir, technology: "html", extensions: [] };
11380
+ const getMissingStageSteps = (stageName) => {
11381
+ const stageDef = stages[stageName];
11382
+ if (!stageDef?.steps) return [];
11383
+ const stageHasTrackedSteps = data.tasks.some((t) => t.stage === stageName && typeof t.step === "string" && t.step.length > 0) || stageDef.steps.some((step) => Boolean(data.stage_loaded?.[step]));
11384
+ if (!stageHasTrackedSteps) return [];
11385
+ return stageDef.steps.filter((step) => !data.tasks.some((t) => t.stage === stageName && t.step === step));
11386
+ };
11387
+ const expandStageIfNeeded = async (stageName) => {
11388
+ const missingBefore = getMissingStageSteps(stageName);
11389
+ if (missingBefore.length === 0) return {};
11390
+ if (!data.stage_loaded) return {};
11391
+ const stageLoaded = data.stage_loaded;
11392
+ const scopeEnvMap = data.env_map ?? {};
11393
+ const stageDef = stages[stageName];
11394
+ if (!stageDef) return {};
11395
+ let expandedFromScope;
11396
+ let resolverErrors;
11397
+ const resolveResult = await resolveStageTaskParams(stageLoaded, stageDef, data.params ?? {}, configForExpansion);
11398
+ data.params = resolveResult.params;
11399
+ if (Object.keys(resolveResult.unresolved).length > 0) {
11400
+ resolverErrors = Object.fromEntries(
11401
+ Object.entries(resolveResult.unresolved).map(([k, v]) => [k, { input: v.input, error: v.error }])
11315
11402
  );
11316
- data.params = resolveResult.params;
11317
- if (Object.keys(resolveResult.unresolved).length > 0) {
11318
- resolverErrors = Object.fromEntries(
11319
- Object.entries(resolveResult.unresolved).map(([k, v]) => [k, { input: v.input, error: v.error }])
11403
+ }
11404
+ const expanded = await expandTasksFromParams(
11405
+ stageLoaded,
11406
+ { [stageName]: stageDef },
11407
+ resolveResult.params,
11408
+ data.tasks,
11409
+ scopeEnvMap,
11410
+ data.scope,
11411
+ options?.config
11412
+ );
11413
+ if (expanded.length > 0) {
11414
+ data.tasks.push(...expanded);
11415
+ const pluginSkillSources = options?.config ? resolvePluginSkillSources({ env: process.env, home: homedir(), config: options.config })?.sources ?? [] : [];
11416
+ const skillsRoot = deriveSkillsRootFromTaskFile(expanded[0]?.task_file) ?? (expanded[0]?.task_file ? dirname(expanded[0].task_file) : void 0);
11417
+ if (skillsRoot) {
11418
+ const mergedSchemas = resolveSchemasForTasks(
11419
+ expanded,
11420
+ skillsRoot,
11421
+ { ...data.schemas ?? {} },
11422
+ pluginSkillSources
11320
11423
  );
11321
- }
11322
- const expanded = await expandTasksFromParams(
11323
- stageLoaded,
11324
- { [stageName]: stageDef },
11325
- resolveResult.params,
11326
- data.tasks,
11327
- scopeEnvMap,
11328
- data.scope,
11329
- options?.config
11330
- );
11331
- if (expanded.length > 0) {
11332
- data.tasks.push(...expanded);
11333
- const skillsRoot = deriveSkillsRootFromTaskFile(expanded[0]?.task_file);
11334
- if (skillsRoot) {
11335
- const mergedSchemas = resolveSchemasForTasks(expanded, skillsRoot, { ...data.schemas ?? {} });
11336
- if (Object.keys(mergedSchemas).length > 0) {
11337
- data.schemas = mergedSchemas;
11338
- }
11424
+ if (Object.keys(mergedSchemas).length > 0) {
11425
+ data.schemas = mergedSchemas;
11339
11426
  }
11340
- expandedFromScope = expanded.map((t) => ({
11341
- id: t.id,
11342
- step: t.step,
11343
- stage: t.stage,
11344
- title: t.title
11345
- }));
11346
11427
  }
11347
- return { expandedFromScope, resolverErrors };
11348
- };
11349
- const formatResolverErrors = (resolverErrors) => {
11350
- if (!resolverErrors || Object.keys(resolverErrors).length === 0) return "";
11351
- const details = Object.entries(resolverErrors).map(([key, value]) => ` \xB7 ${key}: ${value.error ?? "unresolved"}`).join("\n");
11352
- return `
11428
+ expandedFromScope = expanded.map((t) => ({
11429
+ id: t.id,
11430
+ step: t.step,
11431
+ stage: t.stage,
11432
+ title: t.title
11433
+ }));
11434
+ }
11435
+ return { expandedFromScope, resolverErrors };
11436
+ };
11437
+ const formatResolverErrors = (resolverErrors) => {
11438
+ if (!resolverErrors || Object.keys(resolverErrors).length === 0) return "";
11439
+ const details = Object.entries(resolverErrors).map(([key, value]) => ` \xB7 ${key}: ${value.error ?? "unresolved"}`).join("\n");
11440
+ return `
11353
11441
  Resolver errors:
11354
11442
  ${details}`;
11443
+ };
11444
+ const nextStepInStage = getNextStep(currentStage, task.step ?? "", data.tasks);
11445
+ const stageTasks = data.tasks.filter((t) => t.stage === currentStage);
11446
+ const stageDone = stageTasks.filter((t) => t.status === "done").length;
11447
+ const stageTotal = stageTasks.length;
11448
+ let response;
11449
+ if (nextStepInStage) {
11450
+ const nextTask = data.tasks.find((t) => t.step === nextStepInStage && t.status === "pending");
11451
+ if (nextTask) {
11452
+ nextTask.status = "in-progress";
11453
+ nextTask.started_at = timestamp();
11454
+ }
11455
+ response = {
11456
+ stage: currentStage,
11457
+ step_completed: task.step,
11458
+ next_step: nextStepInStage,
11459
+ stage_progress: `${stageDone}/${stageTotal}`,
11460
+ stage_complete: false
11461
+ };
11462
+ writeWorkflowAtomic(filePath, data);
11463
+ return { archived: false, data, response };
11464
+ }
11465
+ const currentStageExpansion = await expandStageIfNeeded(currentStage);
11466
+ const nextTaskInCurrentStage = data.tasks.find((t) => t.stage === currentStage && t.status !== "done");
11467
+ if (nextTaskInCurrentStage) {
11468
+ if (nextTaskInCurrentStage.status === "pending") {
11469
+ nextTaskInCurrentStage.status = "in-progress";
11470
+ nextTaskInCurrentStage.started_at = timestamp();
11471
+ }
11472
+ response = {
11473
+ stage: currentStage,
11474
+ step_completed: task.step,
11475
+ next_step: nextTaskInCurrentStage.step ?? null,
11476
+ stage_progress: `${stageDone}/${stageTotal}`,
11477
+ stage_complete: false,
11478
+ ...currentStageExpansion.expandedFromScope && { expanded_tasks: currentStageExpansion.expandedFromScope },
11479
+ ...currentStageExpansion.resolverErrors && { resolver_errors: currentStageExpansion.resolverErrors }
11355
11480
  };
11356
- const nextStepInStage = getNextStep(currentStage, task.step ?? "", data.tasks);
11357
- const stageTasks = data.tasks.filter((t) => t.stage === currentStage);
11358
- const stageDone = stageTasks.filter((t) => t.status === "done").length;
11359
- const stageTotal = stageTasks.length;
11360
- let response;
11361
- if (nextStepInStage) {
11362
- const nextTask = data.tasks.find((t) => t.step === nextStepInStage && t.status === "pending");
11363
- if (nextTask) {
11364
- nextTask.status = "in-progress";
11365
- nextTask.started_at = timestamp();
11481
+ writeWorkflowAtomic(filePath, data);
11482
+ return { archived: false, data, response };
11483
+ }
11484
+ const missingCurrentSteps = getMissingStageSteps(currentStage);
11485
+ if (missingCurrentSteps.length > 0) {
11486
+ throw new Error(
11487
+ `Cannot complete stage '${currentStage}' \u2014 step(s) not materialized: ${missingCurrentSteps.join(", ")}` + formatResolverErrors(currentStageExpansion.resolverErrors)
11488
+ );
11489
+ }
11490
+ const scopeUpdate = collectStageResults(stageTasks);
11491
+ if (Object.keys(scopeUpdate).length > 0) {
11492
+ data.scope = { ...data.scope ?? {}, ...scopeUpdate };
11493
+ }
11494
+ let fromStage = currentStage;
11495
+ let nextStage = getNextStage(fromStage, stages);
11496
+ while (nextStage) {
11497
+ if (engine) {
11498
+ const transitionResult = await engine.onTransition(fromStage, nextStage, { data });
11499
+ if (transitionResult.archive) {
11500
+ data.current_stage = "done";
11501
+ const transitionResponse = {
11502
+ stage: "done",
11503
+ stage_progress: `${stageDone}/${stageTotal}`,
11504
+ stage_complete: true,
11505
+ ...Object.keys(scopeUpdate).length > 0 && { scope_update: scopeUpdate }
11506
+ };
11507
+ const held2 = holdForAfter(filePath, data, options?.after, transitionResponse);
11508
+ if (held2) return held2;
11509
+ archiveAndCascade(dataDir, name, data);
11510
+ return { archived: true, data, response: transitionResponse };
11511
+ }
11512
+ }
11513
+ const unfulfilledParams = checkStageParams(nextStage, stages, data.params ?? {});
11514
+ if (unfulfilledParams) {
11515
+ data.current_stage = nextStage;
11516
+ const promptState = {};
11517
+ const interpolated = {};
11518
+ for (const [key, param] of Object.entries(unfulfilledParams)) {
11519
+ interpolated[key] = { ...param, prompt: interpolatePrompt(param.prompt, promptState) };
11366
11520
  }
11367
- response = {
11368
- stage: currentStage,
11369
- step_completed: task.step,
11370
- next_step: nextStepInStage,
11371
- stage_progress: `${stageDone}/${stageTotal}`,
11372
- stage_complete: false
11373
- };
11374
11521
  writeWorkflowAtomic(filePath, data);
11375
- return { archived: false, data, response };
11522
+ return { archived: false, data, response: { stage: nextStage, waiting_for: interpolated } };
11376
11523
  }
11377
- const currentStageExpansion = await expandStageIfNeeded(currentStage);
11378
- const nextTaskInCurrentStage = data.tasks.find((t) => t.stage === currentStage && t.status !== "done");
11379
- if (nextTaskInCurrentStage) {
11380
- if (nextTaskInCurrentStage.status === "pending") {
11381
- nextTaskInCurrentStage.status = "in-progress";
11382
- nextTaskInCurrentStage.started_at = timestamp();
11524
+ const { expandedFromScope, resolverErrors } = await expandStageIfNeeded(nextStage);
11525
+ const nextStepInNewStage = data.tasks.find((t) => t.stage === nextStage && t.status !== "done");
11526
+ if (nextStepInNewStage) {
11527
+ if (nextStepInNewStage.status === "pending") {
11528
+ nextStepInNewStage.status = "in-progress";
11529
+ nextStepInNewStage.started_at = timestamp();
11383
11530
  }
11531
+ data.current_stage = nextStage;
11384
11532
  response = {
11385
- stage: currentStage,
11386
- step_completed: task.step,
11387
- next_step: nextTaskInCurrentStage.step ?? null,
11533
+ stage: nextStage,
11534
+ transition_from: currentStage,
11535
+ next_stage: nextStage,
11536
+ next_step: nextStepInNewStage.step ?? null,
11388
11537
  stage_progress: `${stageDone}/${stageTotal}`,
11389
- stage_complete: false,
11390
- ...currentStageExpansion.expandedFromScope && { expanded_tasks: currentStageExpansion.expandedFromScope },
11391
- ...currentStageExpansion.resolverErrors && { resolver_errors: currentStageExpansion.resolverErrors }
11538
+ stage_complete: true,
11539
+ ...Object.keys(scopeUpdate).length > 0 && { scope_update: scopeUpdate },
11540
+ ...expandedFromScope && { expanded_tasks: expandedFromScope },
11541
+ ...resolverErrors && { resolver_errors: resolverErrors }
11392
11542
  };
11393
11543
  writeWorkflowAtomic(filePath, data);
11394
11544
  return { archived: false, data, response };
11395
11545
  }
11396
- const missingCurrentSteps = getMissingStageSteps(currentStage);
11397
- if (missingCurrentSteps.length > 0) {
11546
+ const missingNextSteps = getMissingStageSteps(nextStage);
11547
+ if (missingNextSteps.length > 0) {
11398
11548
  throw new Error(
11399
- `Cannot complete stage '${currentStage}' \u2014 step(s) not materialized: ${missingCurrentSteps.join(", ")}` + formatResolverErrors(currentStageExpansion.resolverErrors)
11549
+ `Cannot enter stage '${nextStage}' \u2014 step(s) not materialized: ${missingNextSteps.join(", ")}` + formatResolverErrors(resolverErrors)
11400
11550
  );
11401
11551
  }
11402
- const scopeUpdate = collectStageResults(stageTasks);
11403
- if (Object.keys(scopeUpdate).length > 0) {
11404
- data.scope = { ...data.scope ?? {}, ...scopeUpdate };
11405
- }
11406
- let fromStage = currentStage;
11407
- let nextStage = getNextStage(fromStage, stages);
11408
- while (nextStage) {
11409
- if (engine) {
11410
- const transitionResult = await engine.onTransition(fromStage, nextStage, { data });
11411
- if (transitionResult.requires) {
11412
- const promptState = {
11413
- branch: data.worktree_branch
11414
- };
11415
- const interpolated = {};
11416
- for (const [key, param] of Object.entries(transitionResult.requires)) {
11417
- interpolated[key] = { ...param, prompt: interpolatePrompt(param.prompt, promptState) };
11418
- }
11419
- data.current_stage = fromStage;
11420
- writeWorkflowAtomic(filePath, data);
11421
- return { archived: false, data, response: { stage: fromStage, waiting_for: interpolated } };
11422
- }
11423
- if (transitionResult.archive) {
11424
- data.current_stage = "done";
11425
- const transitionResponse = {
11426
- stage: "done",
11427
- stage_progress: `${stageDone}/${stageTotal}`,
11428
- stage_complete: true,
11429
- ...Object.keys(scopeUpdate).length > 0 && { scope_update: scopeUpdate }
11430
- };
11431
- const held2 = holdForAfter(filePath, data, options?.after, transitionResponse);
11432
- if (held2) return held2;
11433
- archiveAndCascade(dataDir, name, data);
11434
- return { archived: true, data, response: transitionResponse };
11435
- }
11436
- }
11437
- const unfulfilledParams = checkStageParams(nextStage, stages, data.params ?? {});
11438
- if (unfulfilledParams) {
11439
- data.current_stage = nextStage;
11440
- const promptState = {
11441
- branch: data.worktree_branch
11442
- };
11443
- const interpolated = {};
11444
- for (const [key, param] of Object.entries(unfulfilledParams)) {
11445
- interpolated[key] = { ...param, prompt: interpolatePrompt(param.prompt, promptState) };
11446
- }
11447
- writeWorkflowAtomic(filePath, data);
11448
- return { archived: false, data, response: { stage: nextStage, waiting_for: interpolated } };
11449
- }
11450
- const { expandedFromScope, resolverErrors } = await expandStageIfNeeded(nextStage);
11451
- const nextStepInNewStage = data.tasks.find((t) => t.stage === nextStage && t.status !== "done");
11452
- if (nextStepInNewStage) {
11453
- if (nextStepInNewStage.status === "pending") {
11454
- nextStepInNewStage.status = "in-progress";
11455
- nextStepInNewStage.started_at = timestamp();
11456
- }
11457
- data.current_stage = nextStage;
11458
- response = {
11459
- stage: nextStage,
11460
- transition_from: currentStage,
11461
- next_stage: nextStage,
11462
- next_step: nextStepInNewStage.step ?? null,
11463
- stage_progress: `${stageDone}/${stageTotal}`,
11464
- stage_complete: true,
11465
- ...Object.keys(scopeUpdate).length > 0 && { scope_update: scopeUpdate },
11466
- ...expandedFromScope && { expanded_tasks: expandedFromScope },
11467
- ...resolverErrors && { resolver_errors: resolverErrors }
11468
- };
11469
- writeWorkflowAtomic(filePath, data);
11470
- return { archived: false, data, response };
11471
- }
11472
- const missingNextSteps = getMissingStageSteps(nextStage);
11473
- if (missingNextSteps.length > 0) {
11474
- throw new Error(
11475
- `Cannot enter stage '${nextStage}' \u2014 step(s) not materialized: ${missingNextSteps.join(", ")}` + formatResolverErrors(resolverErrors)
11476
- );
11477
- }
11478
- fromStage = nextStage;
11479
- nextStage = getNextStage(fromStage, stages);
11552
+ fromStage = nextStage;
11553
+ nextStage = getNextStage(fromStage, stages);
11554
+ }
11555
+ data.current_stage = "done";
11556
+ const completionResponse = {
11557
+ stage: "done",
11558
+ stage_progress: `${stageDone}/${stageTotal}`,
11559
+ stage_complete: true,
11560
+ ...Object.keys(scopeUpdate).length > 0 && { scope_update: scopeUpdate }
11561
+ };
11562
+ if (engine) {
11563
+ if (engine.flush) {
11564
+ await engine.flush(data, data.tasks);
11480
11565
  }
11481
- data.current_stage = "done";
11482
- const completionResponse = {
11483
- stage: "done",
11484
- stage_progress: `${stageDone}/${stageTotal}`,
11485
- stage_complete: true,
11486
- ...Object.keys(scopeUpdate).length > 0 && { scope_update: scopeUpdate }
11487
- };
11488
- if (engine) {
11489
- if (engine.flush) {
11490
- await engine.flush(data, data.tasks);
11491
- }
11492
- const doneResult = engine.done(data);
11493
- if (doneResult.archive) {
11494
- const held2 = holdForAfter(filePath, data, options?.after, completionResponse);
11495
- if (held2) return held2;
11496
- archiveAndCascade(dataDir, name, data);
11497
- return { archived: true, data, response: completionResponse };
11498
- }
11566
+ {
11567
+ const held2 = holdForAfter(filePath, data, options?.after, completionResponse);
11568
+ if (held2) return held2;
11569
+ archiveAndCascade(dataDir, name, data);
11570
+ return { archived: true, data, response: completionResponse };
11499
11571
  }
11500
- const held = holdForAfter(filePath, data, options?.after, completionResponse);
11501
- if (held) return held;
11502
- writeWorkflowAtomic(filePath, data);
11503
- return { archived: false, data, response: completionResponse };
11504
11572
  }
11505
- throw new Error("Workflow has no grouped stages \u2014 all workflows require stages in frontmatter");
11506
- });
11573
+ const held = holdForAfter(filePath, data, options?.after, completionResponse);
11574
+ if (held) return held;
11575
+ writeWorkflowAtomic(filePath, data);
11576
+ return { archived: false, data, response: completionResponse };
11577
+ }
11578
+ throw new Error("Workflow has no grouped stages \u2014 all workflows require stages in frontmatter");
11507
11579
  }
11508
11580
  function workflowGetFile(dataDir, name, taskId, key) {
11509
11581
  const changesDir = resolve(dataDir, "workflows", "changes", name);
@@ -11524,7 +11596,7 @@ function workflowGetFile(dataDir, name, taskId, key) {
11524
11596
  const validKeys = [...fileKeys, ...resultKeys].join(", ");
11525
11597
  throw new Error(`Unknown key '${key}' for task '${taskId}'. Valid keys: ${validKeys}`);
11526
11598
  }
11527
- const engine = resolveWorkflowEngine(data);
11599
+ const engine = resolveWorkflowEngine();
11528
11600
  const dataWithDir = data;
11529
11601
  dataWithDir._changesDir = changesDir;
11530
11602
  const staged_path = engine?.getStagedPath ? engine.getStagedPath(dataWithDir, task, key) : fileEntry.path;
@@ -11554,112 +11626,110 @@ function collectStageResults(stageTasks) {
11554
11626
  async function workflowResult(dataDir, name, taskId, key, content, config) {
11555
11627
  const changesDir = resolve(dataDir, "workflows", "changes", name);
11556
11628
  const filePath = resolve(changesDir, "tasks.yml");
11557
- return withLockAsync(filePath, async () => {
11558
- const data = readWorkflow(filePath);
11559
- const task = data.tasks.find((t) => t.id === taskId);
11560
- if (!task) {
11561
- throw new Error(`Task not found: ${taskId} (available: ${data.tasks.map((t) => t.id).join(", ")})`);
11562
- }
11563
- if (!task.result) {
11564
- throw new Error(`Task '${taskId}' has no result declarations`);
11565
- }
11566
- const resultEntry = task.result[key];
11567
- if (!resultEntry) {
11568
- const validKeys = Object.keys(task.result).join(", ");
11569
- throw new Error(`Unknown result key '${key}' for task '${taskId}'. Valid keys: ${validKeys}`);
11570
- }
11571
- const isFileResult = !!resultEntry.path;
11572
- const errors = [];
11573
- if (isFileResult) {
11574
- if (resultEntry.submission !== "direct") {
11575
- throw new Error(
11576
- `Result '${key}' is not declared as \`submission: direct\`. Submit file results via \`workflow done --data '{"` + key + "\": ...}'` instead."
11577
- );
11578
- }
11579
- const engine = resolveWorkflowEngine(data);
11580
- const dataWithDir = data;
11581
- dataWithDir._changesDir = changesDir;
11582
- if (!task.files?.some((f) => f.key === key)) {
11583
- task.files = task.files ?? [];
11584
- task.files.push({
11585
- path: resultEntry.path,
11586
- key,
11587
- validators: resultEntry.validators ?? []
11588
- });
11589
- }
11590
- let writtenPath;
11591
- if (engine?.getStagedPath) {
11592
- const staged = engine.getStagedPath(dataWithDir, task, key);
11593
- writtenPath = existsSync(staged) ? staged : resultEntry.path;
11594
- } else {
11595
- writtenPath = resultEntry.path;
11596
- }
11597
- if (!existsSync(writtenPath)) {
11598
- throw new Error(`External file not found at path: ${writtenPath}`);
11599
- }
11600
- const validationErrors2 = await validateResultEntry(resultEntry, writtenPath, data.schemas, config, "file");
11601
- if (validationErrors2.length > 0) {
11602
- errors.push(...validationErrors2);
11603
- resultEntry.valid = false;
11604
- resultEntry.error = validationErrors2.join("; ");
11605
- } else {
11606
- resultEntry.valid = true;
11607
- delete resultEntry.error;
11608
- }
11609
- resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11610
- const fileEntry = task.files?.find((f) => f.key === key);
11611
- if (fileEntry) {
11612
- fileEntry.validation_result = {
11613
- file: resultEntry.path,
11614
- type: key,
11615
- valid: resultEntry.valid,
11616
- error: resultEntry.error,
11617
- last_validated: resultEntry.last_validated,
11618
- last_passed: resultEntry.valid ? resultEntry.last_validated : void 0,
11619
- last_failed: !resultEntry.valid ? resultEntry.last_validated : void 0
11620
- };
11621
- }
11622
- if (resultEntry.flush === "immediate" && writtenPath !== resultEntry.path) {
11623
- mkdirSync(dirname(resultEntry.path), { recursive: true });
11624
- renameSync(writtenPath, resultEntry.path);
11625
- resultEntry.flushed_at = (/* @__PURE__ */ new Date()).toISOString();
11626
- if (fileEntry) fileEntry.flushed_at = resultEntry.flushed_at;
11627
- writtenPath = resultEntry.path;
11628
- }
11629
- writeWorkflowAtomic(filePath, data);
11630
- return {
11631
- valid: errors.length === 0,
11632
- errors,
11633
- file_path: writtenPath
11634
- };
11629
+ const data = readWorkflow(filePath);
11630
+ const task = data.tasks.find((t) => t.id === taskId);
11631
+ if (!task) {
11632
+ throw new Error(`Task not found: ${taskId} (available: ${data.tasks.map((t) => t.id).join(", ")})`);
11633
+ }
11634
+ if (!task.result) {
11635
+ throw new Error(`Task '${taskId}' has no result declarations`);
11636
+ }
11637
+ const resultEntry = task.result[key];
11638
+ if (!resultEntry) {
11639
+ const validKeys = Object.keys(task.result).join(", ");
11640
+ throw new Error(`Unknown result key '${key}' for task '${taskId}'. Valid keys: ${validKeys}`);
11641
+ }
11642
+ const isFileResult = !!resultEntry.path;
11643
+ const errors = [];
11644
+ if (isFileResult) {
11645
+ if (resultEntry.submission !== "direct") {
11646
+ throw new Error(
11647
+ `Result '${key}' is not declared as \`submission: direct\`. Submit file results via \`workflow done --data '{"` + key + "\": ...}'` instead."
11648
+ );
11635
11649
  }
11636
- let dataValue;
11637
- if (typeof content === "string") {
11638
- try {
11639
- dataValue = JSON.parse(content);
11640
- } catch {
11641
- throw new Error(`Data result '${key}' requires valid JSON, got: ${content.slice(0, 100)}`);
11642
- }
11650
+ const engine = resolveWorkflowEngine();
11651
+ const dataWithDir = data;
11652
+ dataWithDir._changesDir = changesDir;
11653
+ if (!task.files?.some((f) => f.key === key)) {
11654
+ task.files = task.files ?? [];
11655
+ task.files.push({
11656
+ path: resultEntry.path,
11657
+ key,
11658
+ validators: resultEntry.validators ?? []
11659
+ });
11660
+ }
11661
+ let writtenPath;
11662
+ if (engine?.getStagedPath) {
11663
+ const staged = engine.getStagedPath(dataWithDir, task, key);
11664
+ writtenPath = existsSync(staged) ? staged : resultEntry.path;
11643
11665
  } else {
11644
- dataValue = content;
11666
+ writtenPath = resultEntry.path;
11645
11667
  }
11646
- const validationErrors = await validateResultEntry(resultEntry, dataValue, data.schemas, config, "data");
11647
- if (validationErrors.length > 0) {
11648
- errors.push(...validationErrors);
11668
+ if (!existsSync(writtenPath)) {
11669
+ throw new Error(`External file not found at path: ${writtenPath}`);
11670
+ }
11671
+ const validationErrors2 = await validateResultEntry(resultEntry, writtenPath, data.schemas, config, "file");
11672
+ if (validationErrors2.length > 0) {
11673
+ errors.push(...validationErrors2);
11649
11674
  resultEntry.valid = false;
11650
- resultEntry.error = validationErrors.join("; ");
11675
+ resultEntry.error = validationErrors2.join("; ");
11651
11676
  } else {
11652
- resultEntry.value = dataValue;
11653
11677
  resultEntry.valid = true;
11654
11678
  delete resultEntry.error;
11655
11679
  }
11656
11680
  resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11681
+ const fileEntry = task.files?.find((f) => f.key === key);
11682
+ if (fileEntry) {
11683
+ fileEntry.validation_result = {
11684
+ file: resultEntry.path,
11685
+ type: key,
11686
+ valid: resultEntry.valid,
11687
+ error: resultEntry.error,
11688
+ last_validated: resultEntry.last_validated,
11689
+ last_passed: resultEntry.valid ? resultEntry.last_validated : void 0,
11690
+ last_failed: !resultEntry.valid ? resultEntry.last_validated : void 0
11691
+ };
11692
+ }
11693
+ if (resultEntry.flush === "immediate" && writtenPath !== resultEntry.path) {
11694
+ mkdirSync(dirname(resultEntry.path), { recursive: true });
11695
+ renameSync(writtenPath, resultEntry.path);
11696
+ resultEntry.flushed_at = (/* @__PURE__ */ new Date()).toISOString();
11697
+ if (fileEntry) fileEntry.flushed_at = resultEntry.flushed_at;
11698
+ writtenPath = resultEntry.path;
11699
+ }
11657
11700
  writeWorkflowAtomic(filePath, data);
11658
11701
  return {
11659
11702
  valid: errors.length === 0,
11660
- errors
11703
+ errors,
11704
+ file_path: writtenPath
11661
11705
  };
11662
- });
11706
+ }
11707
+ let dataValue;
11708
+ if (typeof content === "string") {
11709
+ try {
11710
+ dataValue = JSON.parse(content);
11711
+ } catch {
11712
+ throw new Error(`Data result '${key}' requires valid JSON, got: ${content.slice(0, 100)}`);
11713
+ }
11714
+ } else {
11715
+ dataValue = content;
11716
+ }
11717
+ const validationErrors = await validateResultEntry(resultEntry, dataValue, data.schemas, config, "data");
11718
+ if (validationErrors.length > 0) {
11719
+ errors.push(...validationErrors);
11720
+ resultEntry.valid = false;
11721
+ resultEntry.error = validationErrors.join("; ");
11722
+ } else {
11723
+ resultEntry.value = dataValue;
11724
+ resultEntry.valid = true;
11725
+ delete resultEntry.error;
11726
+ }
11727
+ resultEntry.last_validated = (/* @__PURE__ */ new Date()).toISOString();
11728
+ writeWorkflowAtomic(filePath, data);
11729
+ return {
11730
+ valid: errors.length === 0,
11731
+ errors
11732
+ };
11663
11733
  }
11664
11734
  async function validateResultEntry(entry, content, schemas, config, mode) {
11665
11735
  const errors = [];
@@ -11856,20 +11926,30 @@ function register2(workflow) {
11856
11926
 
11857
11927
  // src/cli/workflow-discovery.ts
11858
11928
  init_esm4();
11859
- function resolveWorkflowFile(workflowId, agentsDir) {
11929
+ function pluginSources2(sources) {
11930
+ return (sources ?? []).filter((s) => s.origin === "plugin");
11931
+ }
11932
+ function resolveWorkflowFile(workflowId, agentsDir, sources) {
11860
11933
  const matches = globSync(`skills/**/workflows/${workflowId}.md`, { cwd: agentsDir, absolute: true });
11861
- if (matches.length === 0) {
11862
- throw new Error(`Workflow file not found for "${workflowId}". No match for skills/**/workflows/${workflowId}.md`);
11934
+ if (matches.length > 0) return matches[0];
11935
+ for (const source2 of pluginSources2(sources)) {
11936
+ const found = globSync(`**/workflows/${workflowId}.md`, { cwd: source2.root, absolute: true });
11937
+ if (found.length > 0) return found[0];
11863
11938
  }
11864
- return matches[0];
11939
+ throw new Error(`Workflow file not found for "${workflowId}". No match for skills/**/workflows/${workflowId}.md`);
11865
11940
  }
11866
- function listWorkflowDefinitions(agentsDir) {
11941
+ function listWorkflowDefinitions(agentsDir, sources) {
11867
11942
  const matches = globSync(`skills/**/workflows/*.md`, { cwd: agentsDir, absolute: true });
11868
11943
  const ids = matches.map((p) => basename(p, ".md"));
11944
+ for (const source2 of pluginSources2(sources)) {
11945
+ for (const p of globSync(`**/workflows/*.md`, { cwd: source2.root, absolute: true })) {
11946
+ ids.push(basename(p, ".md"));
11947
+ }
11948
+ }
11869
11949
  return Array.from(new Set(ids)).sort();
11870
11950
  }
11871
- function loadWorkflowDefinition(workflowId, agentsDir) {
11872
- const file = resolveWorkflowFile(workflowId, agentsDir);
11951
+ function loadWorkflowDefinition(workflowId, agentsDir, sources) {
11952
+ const file = resolveWorkflowFile(workflowId, agentsDir, sources);
11873
11953
  const raw = readFileSync(file, "utf8");
11874
11954
  const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
11875
11955
  if (!fmMatch) {
@@ -11936,8 +12016,9 @@ async function runWorkflowCreate(opts, config) {
11936
12016
  const rawConfig = configPath ? load(readFileSync(configPath, "utf-8")) ?? {} : {};
11937
12017
  const configDir = configPath ? dirname(configPath) : process.cwd();
11938
12018
  const agentsDir = resolveSkillsRoot(configDir);
11939
- const workflowFilePath = resolveWorkflowFile(opts.workflow, agentsDir);
11940
- const resolved = await resolveAllStages(workflowFilePath, config, rawConfig, agentsDir);
12019
+ const sources = resolveSkillSources(configDir);
12020
+ const workflowFilePath = resolveWorkflowFile(opts.workflow, agentsDir, sources);
12021
+ const resolved = await resolveAllStages(workflowFilePath, config, rawConfig, agentsDir, sources);
11941
12022
  const wfFm = parseFrontmatter(workflowFilePath);
11942
12023
  const wfParamsRaw = wfFm?.params;
11943
12024
  const wfParams = wfParamsRaw ? { ...wfParamsRaw } : Object.values(resolved.expected_params).some((p) => p.resolve) ? {} : void 0;
@@ -12007,7 +12088,7 @@ async function runWorkflowCreate(opts, config) {
12007
12088
  const skillsRoot = resolve(agentsDir, "skills");
12008
12089
  for (const [key, decl] of Object.entries(resultDeclProperties)) {
12009
12090
  if (decl.$ref && firstResult[key]) {
12010
- const { typeName, schema } = resolveSchemaRef(decl.$ref, firstResolved.task_file, skillsRoot);
12091
+ const { typeName, schema } = resolveSchemaRef(decl.$ref, firstResolved.task_file, skillsRoot, sources);
12011
12092
  firstSchemas[typeName] = schema;
12012
12093
  firstResult[key].schema = schema;
12013
12094
  }
@@ -12016,7 +12097,8 @@ async function runWorkflowCreate(opts, config) {
12016
12097
  firstResult[key].schema,
12017
12098
  firstResolved.task_file,
12018
12099
  skillsRoot,
12019
- firstSchemas
12100
+ firstSchemas,
12101
+ sources
12020
12102
  );
12021
12103
  }
12022
12104
  }
@@ -12169,13 +12251,14 @@ function register3(program2) {
12169
12251
  const configPath = findConfig();
12170
12252
  const configDir = configPath ? dirname(configPath) : process.cwd();
12171
12253
  const agentsDir = resolveSkillsRoot(configDir);
12254
+ const sources = resolveSkillSources(configDir);
12172
12255
  if (!workflowId) {
12173
- const ids = listWorkflowDefinitions(agentsDir);
12256
+ const ids = listWorkflowDefinitions(agentsDir, sources);
12174
12257
  for (const id of ids) console.log(id);
12175
12258
  return;
12176
12259
  }
12177
12260
  try {
12178
- const def = loadWorkflowDefinition(workflowId, agentsDir);
12261
+ const def = loadWorkflowDefinition(workflowId, agentsDir, sources);
12179
12262
  console.log(JSON.stringify(def, null, 2));
12180
12263
  } catch (err) {
12181
12264
  console.error(`Error: ${err.message}`);
@@ -12319,7 +12402,8 @@ function register3(program2) {
12319
12402
  const configPath = findConfig();
12320
12403
  const configDir = configPath ? dirname(configPath) : process.cwd();
12321
12404
  const agentsDir = resolveSkillsRoot(configDir);
12322
- allAfter = loadWorkflowDefinition(workflowId, agentsDir).after;
12405
+ const sources = resolveSkillSources(configDir);
12406
+ allAfter = loadWorkflowDefinition(workflowId, agentsDir, sources).after;
12323
12407
  parentParams = parentMeta.params ?? {};
12324
12408
  } catch (lookupErr) {
12325
12409
  console.warn(`Could not load after: declarations \u2014 proceeding without: ${lookupErr.message}`);
@@ -12425,19 +12509,6 @@ RESPONSE: ${JSON.stringify(response)}`);
12425
12509
  process.exitCode = 1;
12426
12510
  }
12427
12511
  });
12428
- workflow.command("merge").description("Squash-merge a workflow branch, kill preview, and archive the workflow").requiredOption("--workflow <name>", "Workflow name (e.g., debo-vision-2026-03-17-a3f7)").action((opts) => {
12429
- const config = loadConfig();
12430
- try {
12431
- const result = workflowMerge(config.data, opts.workflow);
12432
- log({ cmd: "workflow merge", args: { workflow: opts.workflow }, result: { branch: result.branch } });
12433
- console.log(`\u2713 Workflow ${opts.workflow} merged and archived`);
12434
- console.log(` Branch: ${result.branch} (deleted)`);
12435
- console.log(` Commit: workflow: ${opts.workflow}`);
12436
- } catch (err) {
12437
- console.error(`Error: ${err.message}`);
12438
- process.exitCode = 1;
12439
- }
12440
- });
12441
12512
  register2(workflow);
12442
12513
  }
12443
12514
 
@@ -12697,13 +12768,13 @@ function collectRefNamesFromSchema(schema) {
12697
12768
  Object.values(obj).forEach(walk2);
12698
12769
  }
12699
12770
  }
12700
- async function buildRenderContext(workflowFilePath, agentsDir) {
12771
+ async function buildRenderContext(workflowFilePath, agentsDir, sources) {
12701
12772
  const config = loadConfig();
12702
12773
  const configPath = findConfig();
12703
12774
  const rawConfig = configPath ? load(readFileSync(configPath, "utf-8")) ?? {} : {};
12704
12775
  const skillsRoot = resolve(agentsDir, "skills");
12705
12776
  const envMap = buildEnvMap(config);
12706
- const resolved = await resolveAllStages(workflowFilePath, config, rawConfig, agentsDir);
12777
+ const resolved = await resolveAllStages(workflowFilePath, config, rawConfig, agentsDir, sources);
12707
12778
  const wfFm = parseFrontmatter(workflowFilePath) ?? {};
12708
12779
  const workflowId = workflowFilePath.split("/").pop().replace(/\.md$/, "");
12709
12780
  const taskBodies = /* @__PURE__ */ new Map();
@@ -12752,7 +12823,8 @@ async function buildRenderContext(workflowFilePath, agentsDir) {
12752
12823
  result: resultFm,
12753
12824
  taskFilePath: rs.task_file,
12754
12825
  skillsRoot,
12755
- envMap
12826
+ envMap,
12827
+ sources
12756
12828
  });
12757
12829
  const schemaObj = buildResultSchemaObject(schemaBlock.result);
12758
12830
  outputSchemas.set(step, schemaObj);
@@ -13006,8 +13078,9 @@ function register5(program2) {
13006
13078
  const configPath = findConfig();
13007
13079
  const configDir = configPath ? dirname(configPath) : process.cwd();
13008
13080
  const agentsDir = resolveSkillsRoot(configDir);
13009
- const workflowFile = resolveWorkflowFile(workflowId, agentsDir);
13010
- const ctx = await buildRenderContext(workflowFile, agentsDir);
13081
+ const sources = resolveSkillSources(configDir);
13082
+ const workflowFile = resolveWorkflowFile(workflowId, agentsDir, sources);
13083
+ const ctx = await buildRenderContext(workflowFile, agentsDir, sources);
13011
13084
  const md = renderPlan(ctx);
13012
13085
  console.log(md);
13013
13086
  } catch (err) {
@@ -13188,4 +13261,7 @@ register3(program);
13188
13261
  register4(program);
13189
13262
  register5(program);
13190
13263
  register6(program);
13191
- program.parse();
13264
+ program.parseAsync(process.argv).catch((err) => {
13265
+ console.error(err instanceof Error ? err.message : String(err));
13266
+ process.exitCode = 1;
13267
+ });