@openplaybooks/converge 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ {
2
+ "version": 1,
3
+ "examples": {
4
+ "acp-demo": {
5
+ "description": "ACP (Claude Agent SDK) provider demo — programmatic agent invocation",
6
+ "playbookName": "default",
7
+ "hasApiKeys": true
8
+ },
9
+ "data-pipeline": {
10
+ "description": "Sequential pipeline: fetch → transform → validate",
11
+ "playbookName": "default",
12
+ "hasApiKeys": false
13
+ },
14
+ "deep-research": {
15
+ "description": "Layered iterative-deepening research with quality-gated progression",
16
+ "playbookName": "deep-research",
17
+ "hasApiKeys": true
18
+ },
19
+ "evolutionary-optimization": {
20
+ "description": "Fitness-landscape search for prompt tuning and hyperparameter sweeps",
21
+ "playbookName": "default",
22
+ "hasApiKeys": false
23
+ },
24
+ "flutter-app": {
25
+ "description": "Autonomous Flutter mobile app generation — design system → screens → wiring",
26
+ "playbookName": "default",
27
+ "hasApiKeys": true
28
+ },
29
+ "frontier-research": {
30
+ "description": "Beam-search frontier research with parallel beams and convergence tracking",
31
+ "playbookName": "default",
32
+ "hasApiKeys": false
33
+ },
34
+ "fullstack-app": {
35
+ "description": "Seed-driven dynamic backend + frontend generation",
36
+ "playbookName": "default",
37
+ "hasApiKeys": false
38
+ },
39
+ "hello-world": {
40
+ "description": "Simplest possible playbook — one task, two checks",
41
+ "playbookName": "default",
42
+ "hasApiKeys": false
43
+ },
44
+ "scientific-research": {
45
+ "description": "Bayesian reasoning, GRADE evidence, meta-analysis — 8-phase epoch loop",
46
+ "playbookName": "default",
47
+ "hasApiKeys": false
48
+ },
49
+ "social-sim": {
50
+ "description": "Persona-driven OASIS-style social simulation with tick-based iteration",
51
+ "playbookName": "social-sim",
52
+ "hasApiKeys": true
53
+ }
54
+ }
55
+ }
package/dist/index.js CHANGED
@@ -13877,19 +13877,33 @@ function createStoragePaths(convergeDir = ".converge") {
13877
13877
  taskLog: (playbookId, taskId) => `${convergeDir}/playbooks/${playbookId}/tasks/${taskId}.log.md`
13878
13878
  };
13879
13879
  }
13880
- function interpolateEnv(content, sourcePath) {
13881
- return content.replace(
13882
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
13883
- (_match, dollar, name, fallback) => {
13884
- if (dollar) return "$";
13885
- const value = process.env[name];
13886
- if (value !== void 0) return value;
13887
- if (fallback !== void 0) return fallback;
13888
- throw new Error(
13889
- `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
13890
- );
13880
+ function interpolateEnv(value, sourcePath, allowMissing) {
13881
+ if (typeof value === "string") {
13882
+ return value.replace(
13883
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
13884
+ (_match, dollar, name, fallback) => {
13885
+ if (dollar) return "$";
13886
+ const envValue = process.env[name];
13887
+ if (envValue !== void 0) return envValue;
13888
+ if (fallback !== void 0) return fallback;
13889
+ if (allowMissing) return "";
13890
+ throw new Error(
13891
+ `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
13892
+ );
13893
+ }
13894
+ );
13895
+ }
13896
+ if (Array.isArray(value)) {
13897
+ return value.map((v22) => interpolateEnv(v22, sourcePath, allowMissing));
13898
+ }
13899
+ if (value && typeof value === "object") {
13900
+ const out = {};
13901
+ for (const [k102, v22] of Object.entries(value)) {
13902
+ out[k102] = interpolateEnv(v22, sourcePath, allowMissing);
13891
13903
  }
13892
- );
13904
+ return out;
13905
+ }
13906
+ return value;
13893
13907
  }
13894
13908
  function createFilesystemStorage(convergeDir = ".converge") {
13895
13909
  return new FilesystemStorage(convergeDir);
@@ -13914,7 +13928,7 @@ async function findConvergeConfig(startDir) {
13914
13928
  dir = parent;
13915
13929
  }
13916
13930
  }
13917
- async function loadProjectMdConfig(configPath) {
13931
+ async function loadProjectMdConfig(configPath, opts = {}) {
13918
13932
  let raw;
13919
13933
  try {
13920
13934
  raw = await readFile(configPath, "utf-8");
@@ -13944,11 +13958,16 @@ See PROJECT.md format reference.`
13944
13958
  See PROJECT.md format reference.`
13945
13959
  );
13946
13960
  }
13961
+ frontmatter = substituteEnvVarsInTree(
13962
+ frontmatter,
13963
+ configPath,
13964
+ opts.allowMissingEnv ?? false
13965
+ );
13947
13966
  const body = raw.slice(match2[0].length).trim();
13948
13967
  const projectDir = dirname(dirname(configPath));
13949
13968
  return buildConfigFromData(frontmatter, projectDir, body);
13950
13969
  }
13951
- async function loadProjectYamlConfig(configPath) {
13970
+ async function loadProjectYamlConfig(configPath, opts = {}) {
13952
13971
  let raw;
13953
13972
  try {
13954
13973
  raw = await readFile(configPath, "utf-8");
@@ -13958,18 +13977,6 @@ async function loadProjectYamlConfig(configPath) {
13958
13977
  ${err.message}`
13959
13978
  );
13960
13979
  }
13961
- raw = raw.replace(
13962
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
13963
- (_m, dollar, name, fallback) => {
13964
- if (dollar) return "$";
13965
- const value = process.env[name];
13966
- if (value !== void 0) return value;
13967
- if (fallback !== void 0) return fallback;
13968
- throw new Error(
13969
- `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
13970
- );
13971
- }
13972
- );
13973
13980
  let data;
13974
13981
  try {
13975
13982
  data = parse(raw);
@@ -13983,9 +13990,44 @@ async function loadProjectYamlConfig(configPath) {
13983
13990
  See project.yaml format reference.`
13984
13991
  );
13985
13992
  }
13993
+ data = substituteEnvVarsInTree(
13994
+ data,
13995
+ configPath,
13996
+ opts.allowMissingEnv ?? false
13997
+ );
13986
13998
  const projectDir = dirname(dirname(configPath));
13987
13999
  return buildConfigFromData(data, projectDir);
13988
14000
  }
14001
+ function substituteEnvVarsInTree(value, configPath, allowMissingEnv) {
14002
+ if (typeof value === "string") {
14003
+ return value.replace(
14004
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
14005
+ (_m, dollar, name, fallback) => {
14006
+ if (dollar) return "$";
14007
+ const envValue = process.env[name];
14008
+ if (envValue !== void 0) return envValue;
14009
+ if (fallback !== void 0) return fallback;
14010
+ if (allowMissingEnv) return "";
14011
+ throw new Error(
14012
+ `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
14013
+ );
14014
+ }
14015
+ );
14016
+ }
14017
+ if (Array.isArray(value)) {
14018
+ return value.map(
14019
+ (v22) => substituteEnvVarsInTree(v22, configPath, allowMissingEnv)
14020
+ );
14021
+ }
14022
+ if (value && typeof value === "object") {
14023
+ const out = {};
14024
+ for (const [k102, v22] of Object.entries(value)) {
14025
+ out[k102] = substituteEnvVarsInTree(v22, configPath, allowMissingEnv);
14026
+ }
14027
+ return out;
14028
+ }
14029
+ return value;
14030
+ }
13989
14031
  function buildConfigFromData(data, projectDir, body) {
13990
14032
  const config = {
13991
14033
  name: data.name,
@@ -14012,12 +14054,12 @@ function buildConfigFromData(data, projectDir, body) {
14012
14054
  }
14013
14055
  return config;
14014
14056
  }
14015
- async function loadConvergeConfig(configPath, type2) {
14057
+ async function loadConvergeConfig(configPath, type2, opts = {}) {
14016
14058
  const configType = type2 || (configPath.endsWith("project.yaml") || configPath.endsWith("project.yml") ? "project.yaml" : "PROJECT.md");
14017
14059
  if (configType === "project.yaml") {
14018
- return loadProjectYamlConfig(configPath);
14060
+ return loadProjectYamlConfig(configPath, opts);
14019
14061
  } else {
14020
- return loadProjectMdConfig(configPath);
14062
+ return loadProjectMdConfig(configPath, opts);
14021
14063
  }
14022
14064
  }
14023
14065
  function convertScriptHooks(hooks, projectDir) {
@@ -14074,10 +14116,13 @@ function buildSafePayload(event, payload) {
14074
14116
  }
14075
14117
  return safe;
14076
14118
  }
14077
- async function resolveConvergeConfig(startDir) {
14119
+ async function resolveConvergeConfig(startDir, opts = {}) {
14078
14120
  const result = await findConvergeConfig(startDir);
14079
14121
  if (!result) return null;
14080
- const config = await loadConvergeConfig(result.path, result.type);
14122
+ const effective = {
14123
+ allowMissingEnv: opts.allowMissingEnv ?? process.env.CONVERGE_ALLOW_MISSING_ENV === "1"
14124
+ };
14125
+ const config = await loadConvergeConfig(result.path, result.type, effective);
14081
14126
  return { config, configPath: result.path, type: result.type };
14082
14127
  }
14083
14128
  function readEnvValue(varName) {
@@ -44069,13 +44114,22 @@ checkpoints/*.yaml
44069
44114
  /* Project Configuration */
44070
44115
  /* ────────────────────────────────────────────────────────────── */
44071
44116
  /**
44072
- * Read project configuration
44117
+ * Read project configuration.
44118
+ *
44119
+ * `allowMissingEnv` skips throwing on unset ${VAR} placeholders — used by
44120
+ * `--dry` and `converge doctor`, which validate config shape without
44121
+ * requiring credentials.
44073
44122
  */
44074
- readProject() {
44123
+ readProject(opts = {}) {
44075
44124
  const content = readFileSync(this.paths.project, "utf8");
44076
- const interpolated = interpolateEnv(content, this.paths.project);
44077
- const data = parse(interpolated);
44078
- return ProjectConfigSchema.parse(data);
44125
+ const parsed = parse(content);
44126
+ const allowMissing = opts.allowMissingEnv ?? process.env.CONVERGE_ALLOW_MISSING_ENV === "1";
44127
+ const interpolated = interpolateEnv(
44128
+ parsed,
44129
+ this.paths.project,
44130
+ allowMissing
44131
+ );
44132
+ return ProjectConfigSchema.parse(interpolated);
44079
44133
  }
44080
44134
  /**
44081
44135
  * Write project configuration
@@ -73936,19 +73990,33 @@ function createStoragePaths2(convergeDir = ".converge") {
73936
73990
  taskLog: (playbookId, taskId) => `${convergeDir}/playbooks/${playbookId}/tasks/${taskId}.log.md`
73937
73991
  };
73938
73992
  }
73939
- function interpolateEnv2(content, sourcePath) {
73940
- return content.replace(
73941
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
73942
- (_match, dollar, name, fallback) => {
73943
- if (dollar) return "$";
73944
- const value = process.env[name];
73945
- if (value !== void 0) return value;
73946
- if (fallback !== void 0) return fallback;
73947
- throw new Error(
73948
- `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
73949
- );
73993
+ function interpolateEnv2(value, sourcePath, allowMissing) {
73994
+ if (typeof value === "string") {
73995
+ return value.replace(
73996
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
73997
+ (_match, dollar, name, fallback) => {
73998
+ if (dollar) return "$";
73999
+ const envValue = process.env[name];
74000
+ if (envValue !== void 0) return envValue;
74001
+ if (fallback !== void 0) return fallback;
74002
+ if (allowMissing) return "";
74003
+ throw new Error(
74004
+ `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
74005
+ );
74006
+ }
74007
+ );
74008
+ }
74009
+ if (Array.isArray(value)) {
74010
+ return value.map((v18) => interpolateEnv2(v18, sourcePath, allowMissing));
74011
+ }
74012
+ if (value && typeof value === "object") {
74013
+ const out = {};
74014
+ for (const [k19, v18] of Object.entries(value)) {
74015
+ out[k19] = interpolateEnv2(v18, sourcePath, allowMissing);
73950
74016
  }
73951
- );
74017
+ return out;
74018
+ }
74019
+ return value;
73952
74020
  }
73953
74021
  var dynamicImport2 = new Function(
73954
74022
  "p",
@@ -74005,13 +74073,22 @@ checkpoints/*.yaml
74005
74073
  /* Project Configuration */
74006
74074
  /* ────────────────────────────────────────────────────────────── */
74007
74075
  /**
74008
- * Read project configuration
74076
+ * Read project configuration.
74077
+ *
74078
+ * `allowMissingEnv` skips throwing on unset ${VAR} placeholders — used by
74079
+ * `--dry` and `converge doctor`, which validate config shape without
74080
+ * requiring credentials.
74009
74081
  */
74010
- readProject() {
74082
+ readProject(opts = {}) {
74011
74083
  const content = readFileSync(this.paths.project, "utf8");
74012
- const interpolated = interpolateEnv2(content, this.paths.project);
74013
- const data = parse(interpolated);
74014
- return ProjectConfigSchema2.parse(data);
74084
+ const parsed = parse(content);
74085
+ const allowMissing = opts.allowMissingEnv ?? process.env.CONVERGE_ALLOW_MISSING_ENV === "1";
74086
+ const interpolated = interpolateEnv2(
74087
+ parsed,
74088
+ this.paths.project,
74089
+ allowMissing
74090
+ );
74091
+ return ProjectConfigSchema2.parse(interpolated);
74015
74092
  }
74016
74093
  /**
74017
74094
  * Write project configuration
@@ -98108,19 +98185,33 @@ var init_types23 = __esm3({
98108
98185
  });
98109
98186
  }
98110
98187
  });
98111
- function interpolateEnv3(content, sourcePath) {
98112
- return content.replace(
98113
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
98114
- (_match, dollar, name, fallback) => {
98115
- if (dollar) return "$";
98116
- const value = process.env[name];
98117
- if (value !== void 0) return value;
98118
- if (fallback !== void 0) return fallback;
98119
- throw new Error(
98120
- `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
98121
- );
98188
+ function interpolateEnv3(value, sourcePath, allowMissing) {
98189
+ if (typeof value === "string") {
98190
+ return value.replace(
98191
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
98192
+ (_match, dollar, name, fallback) => {
98193
+ if (dollar) return "$";
98194
+ const envValue = process.env[name];
98195
+ if (envValue !== void 0) return envValue;
98196
+ if (fallback !== void 0) return fallback;
98197
+ if (allowMissing) return "";
98198
+ throw new Error(
98199
+ `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
98200
+ );
98201
+ }
98202
+ );
98203
+ }
98204
+ if (Array.isArray(value)) {
98205
+ return value.map((v22) => interpolateEnv3(v22, sourcePath, allowMissing));
98206
+ }
98207
+ if (value && typeof value === "object") {
98208
+ const out = {};
98209
+ for (const [k102, v22] of Object.entries(value)) {
98210
+ out[k102] = interpolateEnv3(v22, sourcePath, allowMissing);
98122
98211
  }
98123
- );
98212
+ return out;
98213
+ }
98214
+ return value;
98124
98215
  }
98125
98216
  var dynamicImport3;
98126
98217
  var FilesystemStorage3;
@@ -98183,13 +98274,22 @@ checkpoints/*.yaml
98183
98274
  /* Project Configuration */
98184
98275
  /* ────────────────────────────────────────────────────────────── */
98185
98276
  /**
98186
- * Read project configuration
98277
+ * Read project configuration.
98278
+ *
98279
+ * `allowMissingEnv` skips throwing on unset ${VAR} placeholders — used by
98280
+ * `--dry` and `converge doctor`, which validate config shape without
98281
+ * requiring credentials.
98187
98282
  */
98188
- readProject() {
98283
+ readProject(opts = {}) {
98189
98284
  const content = readFileSync(this.paths.project, "utf8");
98190
- const interpolated = interpolateEnv3(content, this.paths.project);
98191
- const data = parse(interpolated);
98192
- return ProjectConfigSchema3.parse(data);
98285
+ const parsed = parse(content);
98286
+ const allowMissing = opts.allowMissingEnv ?? process.env.CONVERGE_ALLOW_MISSING_ENV === "1";
98287
+ const interpolated = interpolateEnv3(
98288
+ parsed,
98289
+ this.paths.project,
98290
+ allowMissing
98291
+ );
98292
+ return ProjectConfigSchema3.parse(interpolated);
98193
98293
  }
98194
98294
  /**
98195
98295
  * Write project configuration
@@ -98505,7 +98605,7 @@ ${message}
98505
98605
  };
98506
98606
  }
98507
98607
  });
98508
- async function loadProjectMdConfig2(configPath) {
98608
+ async function loadProjectMdConfig2(configPath, opts = {}) {
98509
98609
  let raw;
98510
98610
  try {
98511
98611
  raw = await readFile(configPath, "utf-8");
@@ -98535,11 +98635,16 @@ See PROJECT.md format reference.`
98535
98635
  See PROJECT.md format reference.`
98536
98636
  );
98537
98637
  }
98638
+ frontmatter = substituteEnvVarsInTree2(
98639
+ frontmatter,
98640
+ configPath,
98641
+ opts.allowMissingEnv ?? false
98642
+ );
98538
98643
  const body = raw.slice(match2[0].length).trim();
98539
98644
  const projectDir = dirname(dirname(configPath));
98540
98645
  return buildConfigFromData2(frontmatter, projectDir, body);
98541
98646
  }
98542
- async function loadProjectYamlConfig2(configPath) {
98647
+ async function loadProjectYamlConfig2(configPath, opts = {}) {
98543
98648
  let raw;
98544
98649
  try {
98545
98650
  raw = await readFile(configPath, "utf-8");
@@ -98549,18 +98654,6 @@ async function loadProjectYamlConfig2(configPath) {
98549
98654
  ${err.message}`
98550
98655
  );
98551
98656
  }
98552
- raw = raw.replace(
98553
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
98554
- (_m, dollar, name, fallback) => {
98555
- if (dollar) return "$";
98556
- const value = process.env[name];
98557
- if (value !== void 0) return value;
98558
- if (fallback !== void 0) return fallback;
98559
- throw new Error(
98560
- `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
98561
- );
98562
- }
98563
- );
98564
98657
  let data;
98565
98658
  try {
98566
98659
  data = parse(raw);
@@ -98574,9 +98667,44 @@ async function loadProjectYamlConfig2(configPath) {
98574
98667
  See project.yaml format reference.`
98575
98668
  );
98576
98669
  }
98670
+ data = substituteEnvVarsInTree2(
98671
+ data,
98672
+ configPath,
98673
+ opts.allowMissingEnv ?? false
98674
+ );
98577
98675
  const projectDir = dirname(dirname(configPath));
98578
98676
  return buildConfigFromData2(data, projectDir);
98579
98677
  }
98678
+ function substituteEnvVarsInTree2(value, configPath, allowMissingEnv) {
98679
+ if (typeof value === "string") {
98680
+ return value.replace(
98681
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
98682
+ (_m, dollar, name, fallback) => {
98683
+ if (dollar) return "$";
98684
+ const envValue = process.env[name];
98685
+ if (envValue !== void 0) return envValue;
98686
+ if (fallback !== void 0) return fallback;
98687
+ if (allowMissingEnv) return "";
98688
+ throw new Error(
98689
+ `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
98690
+ );
98691
+ }
98692
+ );
98693
+ }
98694
+ if (Array.isArray(value)) {
98695
+ return value.map(
98696
+ (v22) => substituteEnvVarsInTree2(v22, configPath, allowMissingEnv)
98697
+ );
98698
+ }
98699
+ if (value && typeof value === "object") {
98700
+ const out = {};
98701
+ for (const [k102, v22] of Object.entries(value)) {
98702
+ out[k102] = substituteEnvVarsInTree2(v22, configPath, allowMissingEnv);
98703
+ }
98704
+ return out;
98705
+ }
98706
+ return value;
98707
+ }
98580
98708
  function buildConfigFromData2(data, projectDir, body) {
98581
98709
  const config = {
98582
98710
  name: data.name,
@@ -98603,12 +98731,12 @@ function buildConfigFromData2(data, projectDir, body) {
98603
98731
  }
98604
98732
  return config;
98605
98733
  }
98606
- async function loadConvergeConfig2(configPath, type2) {
98734
+ async function loadConvergeConfig2(configPath, type2, opts = {}) {
98607
98735
  const configType = configPath.endsWith("project.yaml") || configPath.endsWith("project.yml") ? "project.yaml" : "PROJECT.md";
98608
98736
  if (configType === "project.yaml") {
98609
- return loadProjectYamlConfig2(configPath);
98737
+ return loadProjectYamlConfig2(configPath, opts);
98610
98738
  } else {
98611
- return loadProjectMdConfig2(configPath);
98739
+ return loadProjectMdConfig2(configPath, opts);
98612
98740
  }
98613
98741
  }
98614
98742
  function convertScriptHooks2(hooks, projectDir) {
@@ -139658,19 +139786,33 @@ var init_types24 = __esm4({
139658
139786
  });
139659
139787
  }
139660
139788
  });
139661
- function interpolateEnv4(content, sourcePath) {
139662
- return content.replace(
139663
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
139664
- (_match, dollar, name, fallback) => {
139665
- if (dollar) return "$";
139666
- const value = process.env[name];
139667
- if (value !== void 0) return value;
139668
- if (fallback !== void 0) return fallback;
139669
- throw new Error(
139670
- `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
139671
- );
139789
+ function interpolateEnv4(value, sourcePath, allowMissing) {
139790
+ if (typeof value === "string") {
139791
+ return value.replace(
139792
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
139793
+ (_match, dollar, name, fallback) => {
139794
+ if (dollar) return "$";
139795
+ const envValue = process.env[name];
139796
+ if (envValue !== void 0) return envValue;
139797
+ if (fallback !== void 0) return fallback;
139798
+ if (allowMissing) return "";
139799
+ throw new Error(
139800
+ `${sourcePath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
139801
+ );
139802
+ }
139803
+ );
139804
+ }
139805
+ if (Array.isArray(value)) {
139806
+ return value.map((v22) => interpolateEnv4(v22, sourcePath, allowMissing));
139807
+ }
139808
+ if (value && typeof value === "object") {
139809
+ const out = {};
139810
+ for (const [k102, v22] of Object.entries(value)) {
139811
+ out[k102] = interpolateEnv4(v22, sourcePath, allowMissing);
139672
139812
  }
139673
- );
139813
+ return out;
139814
+ }
139815
+ return value;
139674
139816
  }
139675
139817
  var dynamicImport4;
139676
139818
  var FilesystemStorage4;
@@ -139733,13 +139875,22 @@ checkpoints/*.yaml
139733
139875
  /* Project Configuration */
139734
139876
  /* ────────────────────────────────────────────────────────────── */
139735
139877
  /**
139736
- * Read project configuration
139878
+ * Read project configuration.
139879
+ *
139880
+ * `allowMissingEnv` skips throwing on unset ${VAR} placeholders — used by
139881
+ * `--dry` and `converge doctor`, which validate config shape without
139882
+ * requiring credentials.
139737
139883
  */
139738
- readProject() {
139884
+ readProject(opts = {}) {
139739
139885
  const content = readFileSync(this.paths.project, "utf8");
139740
- const interpolated = interpolateEnv4(content, this.paths.project);
139741
- const data = parse(interpolated);
139742
- return ProjectConfigSchema4.parse(data);
139886
+ const parsed = parse(content);
139887
+ const allowMissing = opts.allowMissingEnv ?? process.env.CONVERGE_ALLOW_MISSING_ENV === "1";
139888
+ const interpolated = interpolateEnv4(
139889
+ parsed,
139890
+ this.paths.project,
139891
+ allowMissing
139892
+ );
139893
+ return ProjectConfigSchema4.parse(interpolated);
139743
139894
  }
139744
139895
  /**
139745
139896
  * Write project configuration
@@ -140055,7 +140206,7 @@ ${message}
140055
140206
  };
140056
140207
  }
140057
140208
  });
140058
- async function loadProjectMdConfig3(configPath) {
140209
+ async function loadProjectMdConfig3(configPath, opts = {}) {
140059
140210
  let raw;
140060
140211
  try {
140061
140212
  raw = await readFile(configPath, "utf-8");
@@ -140085,11 +140236,16 @@ See PROJECT.md format reference.`
140085
140236
  See PROJECT.md format reference.`
140086
140237
  );
140087
140238
  }
140239
+ frontmatter = substituteEnvVarsInTree3(
140240
+ frontmatter,
140241
+ configPath,
140242
+ opts.allowMissingEnv ?? false
140243
+ );
140088
140244
  const body = raw.slice(match2[0].length).trim();
140089
140245
  const projectDir = dirname(dirname(configPath));
140090
140246
  return buildConfigFromData3(frontmatter, projectDir, body);
140091
140247
  }
140092
- async function loadProjectYamlConfig3(configPath) {
140248
+ async function loadProjectYamlConfig3(configPath, opts = {}) {
140093
140249
  let raw;
140094
140250
  try {
140095
140251
  raw = await readFile(configPath, "utf-8");
@@ -140099,18 +140255,6 @@ async function loadProjectYamlConfig3(configPath) {
140099
140255
  ${err.message}`
140100
140256
  );
140101
140257
  }
140102
- raw = raw.replace(
140103
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
140104
- (_m, dollar, name, fallback) => {
140105
- if (dollar) return "$";
140106
- const value = process.env[name];
140107
- if (value !== void 0) return value;
140108
- if (fallback !== void 0) return fallback;
140109
- throw new Error(
140110
- `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
140111
- );
140112
- }
140113
- );
140114
140258
  let data;
140115
140259
  try {
140116
140260
  data = parse(raw);
@@ -140124,9 +140268,44 @@ async function loadProjectYamlConfig3(configPath) {
140124
140268
  See project.yaml format reference.`
140125
140269
  );
140126
140270
  }
140271
+ data = substituteEnvVarsInTree3(
140272
+ data,
140273
+ configPath,
140274
+ opts.allowMissingEnv ?? false
140275
+ );
140127
140276
  const projectDir = dirname(dirname(configPath));
140128
140277
  return buildConfigFromData3(data, projectDir);
140129
140278
  }
140279
+ function substituteEnvVarsInTree3(value, configPath, allowMissingEnv) {
140280
+ if (typeof value === "string") {
140281
+ return value.replace(
140282
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
140283
+ (_m, dollar, name, fallback) => {
140284
+ if (dollar) return "$";
140285
+ const envValue = process.env[name];
140286
+ if (envValue !== void 0) return envValue;
140287
+ if (fallback !== void 0) return fallback;
140288
+ if (allowMissingEnv) return "";
140289
+ throw new Error(
140290
+ `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
140291
+ );
140292
+ }
140293
+ );
140294
+ }
140295
+ if (Array.isArray(value)) {
140296
+ return value.map(
140297
+ (v22) => substituteEnvVarsInTree3(v22, configPath, allowMissingEnv)
140298
+ );
140299
+ }
140300
+ if (value && typeof value === "object") {
140301
+ const out = {};
140302
+ for (const [k102, v22] of Object.entries(value)) {
140303
+ out[k102] = substituteEnvVarsInTree3(v22, configPath, allowMissingEnv);
140304
+ }
140305
+ return out;
140306
+ }
140307
+ return value;
140308
+ }
140130
140309
  function buildConfigFromData3(data, projectDir, body) {
140131
140310
  const config = {
140132
140311
  name: data.name,
@@ -140153,12 +140332,12 @@ function buildConfigFromData3(data, projectDir, body) {
140153
140332
  }
140154
140333
  return config;
140155
140334
  }
140156
- async function loadConvergeConfig3(configPath, type2) {
140335
+ async function loadConvergeConfig3(configPath, type2, opts = {}) {
140157
140336
  const configType = configPath.endsWith("project.yaml") || configPath.endsWith("project.yml") ? "project.yaml" : "PROJECT.md";
140158
140337
  if (configType === "project.yaml") {
140159
- return loadProjectYamlConfig3(configPath);
140338
+ return loadProjectYamlConfig3(configPath, opts);
140160
140339
  } else {
140161
- return loadProjectMdConfig3(configPath);
140340
+ return loadProjectMdConfig3(configPath, opts);
140162
140341
  }
140163
140342
  }
140164
140343
  function convertScriptHooks3(hooks, projectDir) {
@@ -158632,7 +158811,7 @@ async function findConvergeConfig2(startDir) {
158632
158811
  }
158633
158812
  }
158634
158813
  var FRONTMATTER_RE4 = /^---\r?\n([\s\S]*?)\r?\n---/;
158635
- async function loadProjectMdConfig4(configPath) {
158814
+ async function loadProjectMdConfig4(configPath, opts = {}) {
158636
158815
  let raw;
158637
158816
  try {
158638
158817
  raw = await readFile(configPath, "utf-8");
@@ -158662,11 +158841,16 @@ See PROJECT.md format reference.`
158662
158841
  See PROJECT.md format reference.`
158663
158842
  );
158664
158843
  }
158844
+ frontmatter = substituteEnvVarsInTree4(
158845
+ frontmatter,
158846
+ configPath,
158847
+ opts.allowMissingEnv ?? false
158848
+ );
158665
158849
  const body = raw.slice(match2[0].length).trim();
158666
158850
  const projectDir = dirname(dirname(configPath));
158667
158851
  return buildConfigFromData4(frontmatter, projectDir, body);
158668
158852
  }
158669
- async function loadProjectYamlConfig4(configPath) {
158853
+ async function loadProjectYamlConfig4(configPath, opts = {}) {
158670
158854
  let raw;
158671
158855
  try {
158672
158856
  raw = await readFile(configPath, "utf-8");
@@ -158676,18 +158860,6 @@ async function loadProjectYamlConfig4(configPath) {
158676
158860
  ${err.message}`
158677
158861
  );
158678
158862
  }
158679
- raw = raw.replace(
158680
- /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
158681
- (_m, dollar, name, fallback) => {
158682
- if (dollar) return "$";
158683
- const value = process.env[name];
158684
- if (value !== void 0) return value;
158685
- if (fallback !== void 0) return fallback;
158686
- throw new Error(
158687
- `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
158688
- );
158689
- }
158690
- );
158691
158863
  let data;
158692
158864
  try {
158693
158865
  data = parse(raw);
@@ -158701,9 +158873,44 @@ async function loadProjectYamlConfig4(configPath) {
158701
158873
  See project.yaml format reference.`
158702
158874
  );
158703
158875
  }
158876
+ data = substituteEnvVarsInTree4(
158877
+ data,
158878
+ configPath,
158879
+ opts.allowMissingEnv ?? false
158880
+ );
158704
158881
  const projectDir = dirname(dirname(configPath));
158705
158882
  return buildConfigFromData4(data, projectDir);
158706
158883
  }
158884
+ function substituteEnvVarsInTree4(value, configPath, allowMissingEnv) {
158885
+ if (typeof value === "string") {
158886
+ return value.replace(
158887
+ /\$(\$)|\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/g,
158888
+ (_m, dollar, name, fallback) => {
158889
+ if (dollar) return "$";
158890
+ const envValue = process.env[name];
158891
+ if (envValue !== void 0) return envValue;
158892
+ if (fallback !== void 0) return fallback;
158893
+ if (allowMissingEnv) return "";
158894
+ throw new Error(
158895
+ `${configPath}: ${name} is not set. Add it to .env.local or export it before running the CLI.`
158896
+ );
158897
+ }
158898
+ );
158899
+ }
158900
+ if (Array.isArray(value)) {
158901
+ return value.map(
158902
+ (v22) => substituteEnvVarsInTree4(v22, configPath, allowMissingEnv)
158903
+ );
158904
+ }
158905
+ if (value && typeof value === "object") {
158906
+ const out = {};
158907
+ for (const [k102, v22] of Object.entries(value)) {
158908
+ out[k102] = substituteEnvVarsInTree4(v22, configPath, allowMissingEnv);
158909
+ }
158910
+ return out;
158911
+ }
158912
+ return value;
158913
+ }
158707
158914
  function buildConfigFromData4(data, projectDir, body) {
158708
158915
  const config = {
158709
158916
  name: data.name,
@@ -158730,12 +158937,12 @@ function buildConfigFromData4(data, projectDir, body) {
158730
158937
  }
158731
158938
  return config;
158732
158939
  }
158733
- async function loadConvergeConfig4(configPath, type2) {
158940
+ async function loadConvergeConfig4(configPath, type2, opts = {}) {
158734
158941
  const configType = type2 || (configPath.endsWith("project.yaml") || configPath.endsWith("project.yml") ? "project.yaml" : "PROJECT.md");
158735
158942
  if (configType === "project.yaml") {
158736
- return loadProjectYamlConfig4(configPath);
158943
+ return loadProjectYamlConfig4(configPath, opts);
158737
158944
  } else {
158738
- return loadProjectMdConfig4(configPath);
158945
+ return loadProjectMdConfig4(configPath, opts);
158739
158946
  }
158740
158947
  }
158741
158948
  var HOOK_TIMEOUT_MS4 = 3e4;
@@ -158793,10 +159000,13 @@ function buildSafePayload4(event, payload) {
158793
159000
  }
158794
159001
  return safe;
158795
159002
  }
158796
- async function resolveConvergeConfig2(startDir) {
159003
+ async function resolveConvergeConfig2(startDir, opts = {}) {
158797
159004
  const result = await findConvergeConfig2(startDir);
158798
159005
  if (!result) return null;
158799
- const config = await loadConvergeConfig4(result.path, result.type);
159006
+ const effective = {
159007
+ allowMissingEnv: opts.allowMissingEnv ?? process.env.CONVERGE_ALLOW_MISSING_ENV === "1"
159008
+ };
159009
+ const config = await loadConvergeConfig4(result.path, result.type, effective);
158800
159010
  return { config, configPath: result.path, type: result.type };
158801
159011
  }
158802
159012
  init_esm_shims5();
@@ -191722,8 +191932,26 @@ async function doctorCommand({
191722
191932
  contradictoryFindings: [],
191723
191933
  staleSentinels: [],
191724
191934
  trippedCircuits: [],
191725
- malformedSkills: []
191935
+ malformedSkills: [],
191936
+ configErrors: []
191726
191937
  };
191938
+ try {
191939
+ await resolveConvergeConfig2(workspace, { allowMissingEnv: true });
191940
+ } catch (err) {
191941
+ const msg = err?.message ?? String(err);
191942
+ const idx = msg.indexOf(": ");
191943
+ if (idx > 0 && msg.slice(0, idx).includes(".converge/project")) {
191944
+ report.configErrors.push({
191945
+ path: msg.slice(0, idx),
191946
+ reason: msg.slice(idx + 2).trim()
191947
+ });
191948
+ } else {
191949
+ report.configErrors.push({
191950
+ path: join(workspace, ".converge", "project.yaml"),
191951
+ reason: msg
191952
+ });
191953
+ }
191954
+ }
191727
191955
  const defGaps = await findDefinitionGaps2(workspace, playbook);
191728
191956
  for (const d16 of defGaps) {
191729
191957
  report.definitionGaps.push({
@@ -191807,7 +192035,7 @@ async function doctorCommand({
191807
192035
  }
191808
192036
  }
191809
192037
  }
191810
- const totalFindings = report.definitionGaps.length + report.phantomWorkItems.length + report.contradictoryFindings.length + report.staleSentinels.length + report.trippedCircuits.length + report.malformedSkills.length;
192038
+ const totalFindings = report.definitionGaps.length + report.phantomWorkItems.length + report.contradictoryFindings.length + report.staleSentinels.length + report.trippedCircuits.length + report.malformedSkills.length + report.configErrors.length;
191811
192039
  if (jsonOut) {
191812
192040
  console.log(
191813
192041
  JSON.stringify({ totalFindings, ...report, fixed: fix }, null, 2)
@@ -191878,6 +192106,17 @@ function printHumanReport(report, total, fixed) {
191878
192106
  if (fixed) console.log(` (re-armed by --fix)`);
191879
192107
  console.log();
191880
192108
  }
192109
+ if (report.configErrors.length > 0) {
192110
+ console.log(
192111
+ `\u25CF ${report.configErrors.length} project config error(s) \u2014 .converge/project.yaml will fail to load:`
192112
+ );
192113
+ for (const c of report.configErrors) {
192114
+ console.log(` ${c.path}`);
192115
+ console.log(` reason: ${c.reason.split("\n")[0].slice(0, 200)}`);
192116
+ }
192117
+ console.log(` (--fix does NOT touch project.yaml \u2014 edit it yourself)`);
192118
+ console.log();
192119
+ }
191881
192120
  if (report.malformedSkills.length > 0) {
191882
192121
  console.log(
191883
192122
  `\u25CF ${report.malformedSkills.length} malformed SKILL.md file(s) \u2014 skill directories silently skipped by the catalog loader:`
@@ -209958,7 +210197,7 @@ function renderProjectYaml(args) {
209958
210197
  if (args.description) lines.push(`description: ${yamlEscape(args.description)}`);
209959
210198
  lines.push("");
209960
210199
  lines.push("# AI provider configuration \u2014 one default, multiple enabled.");
209961
- lines.push("# Replace ${ENV_VAR} placeholders with real keys or export them in your shell.");
210200
+ lines.push("# Replace $VAR placeholders with real keys or export them in your shell.");
209962
210201
  lines.push("ai:");
209963
210202
  lines.push(` default: ${args.defaultAgent}`);
209964
210203
  lines.push(" providers:");
@@ -211478,7 +211717,13 @@ async function main() {
211478
211717
  const localProjectYaml = join(localConvergeDir, "project.yaml");
211479
211718
  const localProjectMd = join(localConvergeDir, "PROJECT.md");
211480
211719
  const hasLocalProject = existsSync(localProjectYml) || existsSync(localProjectYaml) || existsSync(localProjectMd);
211481
- let resolved = await resolveConvergeConfig2(searchDir);
211720
+ const isDryRunEarly = !!(options.dry || options.plan);
211721
+ if (isDryRunEarly) {
211722
+ process.env.CONVERGE_ALLOW_MISSING_ENV = "1";
211723
+ }
211724
+ let resolved = await resolveConvergeConfig2(searchDir, {
211725
+ allowMissingEnv: isDryRunEarly
211726
+ });
211482
211727
  if (resolved && !hasLocalProject) {
211483
211728
  const resolvedDir = dirname(resolved.configPath);
211484
211729
  const resolvedIsParent = resolvedDir !== searchDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openplaybooks/converge",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "CLI for Converge - A build system for AI agents",
@@ -31,14 +31,14 @@
31
31
  "dependencies": {
32
32
  "@anthropic-ai/claude-agent-sdk": "^0.2.0",
33
33
  "@clack/prompts": "^0.9.1",
34
- "glob": "^11.1.0",
34
+ "glob": "^13.0.0",
35
35
  "yaml": "^2.8.3",
36
- "@openplaybooks/claudefn": "0.1.0",
37
36
  "@openplaybooks/codexfn": "0.1.0",
37
+ "@openplaybooks/openfn": "^0.1.0",
38
+ "@openplaybooks/converge-core": "0.2.3",
38
39
  "@openplaybooks/agentfn": "0.2.1",
39
40
  "@openplaybooks/deepcodefn": "0.1.0",
40
- "@openplaybooks/openfn": "^0.1.0",
41
- "@openplaybooks/converge-core": "0.2.1"
41
+ "@openplaybooks/claudefn": "0.1.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.0.0",