@cleocode/cleo 2026.4.127 → 2026.4.129

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/index.js CHANGED
@@ -7669,6 +7669,32 @@ var init_registry = __esm({
7669
7669
  }
7670
7670
  ]
7671
7671
  },
7672
+ // ── playbook.validate (T1261 PSYCHE E4) ──────────────────────────────────
7673
+ {
7674
+ gateway: "query",
7675
+ domain: "playbook",
7676
+ operation: "validate",
7677
+ description: "playbook.validate (query) \u2014 parse and validate a .cantbook file without executing it; exit 0 on success, exit 70 on parse error",
7678
+ tier: 1,
7679
+ idempotent: true,
7680
+ sessionRequired: false,
7681
+ requiredParams: [],
7682
+ params: [
7683
+ {
7684
+ name: "file",
7685
+ type: "string",
7686
+ required: false,
7687
+ description: "Absolute or relative path to the .cantbook file",
7688
+ cli: { positional: true }
7689
+ },
7690
+ {
7691
+ name: "name",
7692
+ type: "string",
7693
+ required: false,
7694
+ description: "Playbook name (resolved through the standard search path)"
7695
+ }
7696
+ ]
7697
+ },
7672
7698
  // ── orchestrate.{approve,reject,pending} HITL gate decisions (T935) ──────
7673
7699
  {
7674
7700
  gateway: "mutate",
@@ -9782,7 +9808,7 @@ async function applyCantBodySubstitution(payload, cwd, ctx) {
9782
9808
  if (!cantPath) {
9783
9809
  return { ...empty, reason: "resolved agent has no cantPath" };
9784
9810
  }
9785
- const [{ existsSync: existsSync12, readFileSync: readFileSync15 }, { substituteCantAgentBody }] = await Promise.all([
9811
+ const [{ existsSync: existsSync12, readFileSync: readFileSync16 }, { substituteCantAgentBody }] = await Promise.all([
9786
9812
  import("node:fs"),
9787
9813
  import("@cleocode/core/internal")
9788
9814
  ]);
@@ -9791,7 +9817,7 @@ async function applyCantBodySubstitution(payload, cwd, ctx) {
9791
9817
  }
9792
9818
  let body;
9793
9819
  try {
9794
- body = readFileSync15(cantPath, "utf-8");
9820
+ body = readFileSync16(cantPath, "utf-8");
9795
9821
  } catch (err) {
9796
9822
  return {
9797
9823
  ...empty,
@@ -24527,13 +24553,15 @@ function parseAgenticNode(raw, base, index) {
24527
24553
  }
24528
24554
  inputs = acc;
24529
24555
  }
24556
+ const context_files = parseStringArray(raw.context_files, `nodes[${index}].context_files`);
24530
24557
  return {
24531
24558
  ...base,
24532
24559
  type: "agentic",
24533
24560
  skill,
24534
24561
  agent,
24535
24562
  role,
24536
- inputs
24563
+ inputs,
24564
+ ...context_files !== void 0 ? { context_files } : {}
24537
24565
  };
24538
24566
  }
24539
24567
  function parseDeterministicNode(raw, base, index) {
@@ -24893,6 +24921,105 @@ var init_parser = __esm({
24893
24921
  }
24894
24922
  });
24895
24923
 
24924
+ // packages/playbooks/src/migrate-e4.ts
24925
+ import { readFileSync as readFileSync5, writeFileSync } from "node:fs";
24926
+ function validatePlaybookCompliance(source) {
24927
+ let parsed;
24928
+ try {
24929
+ parsed = parsePlaybook(source);
24930
+ } catch (err) {
24931
+ return {
24932
+ parses: false,
24933
+ parseError: err instanceof PlaybookParseError ? err.message : String(err),
24934
+ hasErrorHandlers: false,
24935
+ nodesMissingRequires: 0,
24936
+ nodesMissingEnsures: 0,
24937
+ nodes: [],
24938
+ compliant: false
24939
+ };
24940
+ }
24941
+ const { definition } = parsed;
24942
+ const hasErrorHandlers = (definition.error_handlers?.length ?? 0) > 0;
24943
+ const nodeEntries = definition.nodes.map((n) => ({
24944
+ id: n.id,
24945
+ type: n.type,
24946
+ hasRequires: n.requires !== void 0,
24947
+ hasEnsures: n.ensures !== void 0
24948
+ }));
24949
+ const workNodes = nodeEntries.filter((e) => e.type !== "approval");
24950
+ const nodesMissingRequires = workNodes.filter((e) => !e.hasRequires).length;
24951
+ const nodesMissingEnsures = workNodes.filter((e) => !e.hasEnsures).length;
24952
+ const compliant = hasErrorHandlers && nodesMissingRequires === 0 && nodesMissingEnsures === 0;
24953
+ return {
24954
+ parses: true,
24955
+ hasErrorHandlers,
24956
+ nodesMissingRequires,
24957
+ nodesMissingEnsures,
24958
+ nodes: nodeEntries,
24959
+ compliant
24960
+ };
24961
+ }
24962
+ function migratePlaybook(source) {
24963
+ parsePlaybook(source);
24964
+ const raw = load(source);
24965
+ if (!Array.isArray(raw.error_handlers) || raw.error_handlers.length === 0) {
24966
+ raw.error_handlers = [
24967
+ {
24968
+ on: "iteration_cap_exceeded",
24969
+ action: "hitl_escalate",
24970
+ message: "Stage exhausted retries \u2014 escalate to human for direction."
24971
+ },
24972
+ {
24973
+ on: "contract_violation",
24974
+ action: "inject_hint",
24975
+ message: "Contract violated at stage boundary \u2014 check requires/ensures fields."
24976
+ }
24977
+ ];
24978
+ }
24979
+ if (Array.isArray(raw.nodes)) {
24980
+ raw.nodes = raw.nodes.map((node) => {
24981
+ if (node.type === "approval") return node;
24982
+ const patched = { ...node };
24983
+ if (!patched.requires) patched.requires = {};
24984
+ if (!patched.ensures) patched.ensures = {};
24985
+ return patched;
24986
+ });
24987
+ }
24988
+ return dump(raw, { lineWidth: 100, noRefs: true });
24989
+ }
24990
+ function migratePlaybookFile(filePath, dryRun = false) {
24991
+ const source = readFileSync5(filePath, "utf8");
24992
+ const before = validatePlaybookCompliance(source);
24993
+ if (!before.parses) {
24994
+ return {
24995
+ filePath,
24996
+ before,
24997
+ after: before,
24998
+ written: false,
24999
+ migratedSource: source
25000
+ };
25001
+ }
25002
+ const migratedSource = migratePlaybook(source);
25003
+ const after = validatePlaybookCompliance(migratedSource);
25004
+ if (!dryRun && !before.compliant) {
25005
+ writeFileSync(filePath, migratedSource, "utf8");
25006
+ }
25007
+ return {
25008
+ filePath,
25009
+ before,
25010
+ after,
25011
+ written: !dryRun && !before.compliant,
25012
+ migratedSource
25013
+ };
25014
+ }
25015
+ var init_migrate_e4 = __esm({
25016
+ "packages/playbooks/src/migrate-e4.ts"() {
25017
+ "use strict";
25018
+ init_js_yaml();
25019
+ init_parser();
25020
+ }
25021
+ });
25022
+
24896
25023
  // packages/playbooks/src/policy.ts
24897
25024
  function evaluatePolicy(command, rules = DEFAULT_POLICY_RULES) {
24898
25025
  for (const rule of rules) {
@@ -25364,6 +25491,39 @@ function executeApprovalNode(node, runId, context, db, secret) {
25364
25491
  });
25365
25492
  return { kind: "awaiting_approval", token: gate.token, approvalId: gate.approvalId };
25366
25493
  }
25494
+ function checkRequires(fields, from, context) {
25495
+ for (const key of fields) {
25496
+ if (!(key in context)) {
25497
+ const source = from !== void 0 ? ` (from node '${from}')` : "";
25498
+ return `requires.fields['${key}']${source} not present in context`;
25499
+ }
25500
+ }
25501
+ return null;
25502
+ }
25503
+ function resolveEdge(fromId, toId, edges) {
25504
+ return edges.find((e) => e.from === fromId && e.to === toId);
25505
+ }
25506
+ function auditContractViolation(projectRoot, runId, nodeId, field, key, playbookName) {
25507
+ if (!projectRoot) return;
25508
+ try {
25509
+ void import("@cleocode/core").then(({ appendContractViolation }) => {
25510
+ appendContractViolation(projectRoot, {
25511
+ runId,
25512
+ nodeId,
25513
+ field,
25514
+ key,
25515
+ message: `contract_violation: ${field}['${key}'] check failed on node '${nodeId}'`,
25516
+ playbookName
25517
+ });
25518
+ });
25519
+ } catch {
25520
+ }
25521
+ }
25522
+ function handleContractErrorHandler(playbook, trigger, _message) {
25523
+ if (!playbook.error_handlers) return null;
25524
+ const handler = playbook.error_handlers.find((h) => h.on === trigger);
25525
+ return handler?.action ?? null;
25526
+ }
25367
25527
  function iterationCapFor(node, runtimeDefault) {
25368
25528
  const cap = node.on_failure?.max_iterations;
25369
25529
  if (typeof cap === "number" && Number.isFinite(cap) && cap >= 0) return cap;
@@ -25372,6 +25532,7 @@ function iterationCapFor(node, runtimeDefault) {
25372
25532
  async function runFromNode(args) {
25373
25533
  const {
25374
25534
  db,
25535
+ playbook,
25375
25536
  run,
25376
25537
  startNodeId,
25377
25538
  nodeIndex,
@@ -25397,6 +25558,35 @@ async function runFromNode(args) {
25397
25558
  currentNode: node.id,
25398
25559
  iterationCounts: { ...iterationCounts }
25399
25560
  });
25561
+ if (node.requires?.fields) {
25562
+ const violation = checkRequires(node.requires.fields, node.requires.from, context);
25563
+ if (violation !== null) {
25564
+ const contractFailure = `contract_violation: ${violation}`;
25565
+ auditContractViolation(
25566
+ args.projectRoot,
25567
+ run.runId,
25568
+ node.id,
25569
+ "requires",
25570
+ violation,
25571
+ playbook.name
25572
+ );
25573
+ const handled = handleContractErrorHandler(playbook, "contract_violation", contractFailure);
25574
+ if (handled === "abort") {
25575
+ failedNodeId = node.id;
25576
+ lastError = contractFailure;
25577
+ break;
25578
+ }
25579
+ if (handled !== null) {
25580
+ context["__lastError"] = contractFailure;
25581
+ context["__lastFailedNode"] = node.id;
25582
+ context["__contractViolation"] = violation;
25583
+ if (handled === "hitl_escalate") {
25584
+ exceededNodeId = node.id;
25585
+ break;
25586
+ }
25587
+ }
25588
+ }
25589
+ }
25400
25590
  let outcome;
25401
25591
  if (node.type === "agentic") {
25402
25592
  outcome = await executeAgenticNode(node, run.runId, context, attempt, dispatcher);
@@ -25420,7 +25610,51 @@ async function runFromNode(args) {
25420
25610
  if (outcome.kind === "success") {
25421
25611
  Object.assign(context, outcome.output);
25422
25612
  updatePlaybookRun(db, run.runId, { bindings: { ...context } });
25423
- currentId = resolveNextNodeId(node.id, edgeIndex);
25613
+ if (node.ensures?.outputFiles) {
25614
+ for (const key of node.ensures.outputFiles) {
25615
+ if (!(key in context)) {
25616
+ const violation = `ensures.outputFiles[${key}] not present in context after ${node.id}`;
25617
+ auditContractViolation(
25618
+ args.projectRoot,
25619
+ run.runId,
25620
+ node.id,
25621
+ "ensures",
25622
+ key,
25623
+ playbook.name
25624
+ );
25625
+ handleContractErrorHandler(playbook, "contract_violation", violation);
25626
+ context["__ensuresViolation"] = violation;
25627
+ }
25628
+ }
25629
+ }
25630
+ const nextId = resolveNextNodeId(node.id, edgeIndex);
25631
+ if (nextId !== null) {
25632
+ const edge = resolveEdge(node.id, nextId, playbook.edges);
25633
+ if (edge?.contract?.requires) {
25634
+ for (const key of edge.contract.requires) {
25635
+ if (!(key in context)) {
25636
+ const violation = `edge.contract.requires[${key}] missing when crossing ${node.id} \u2192 ${nextId}`;
25637
+ auditContractViolation(
25638
+ args.projectRoot,
25639
+ run.runId,
25640
+ node.id,
25641
+ "requires",
25642
+ key,
25643
+ playbook.name
25644
+ );
25645
+ const handled = handleContractErrorHandler(playbook, "contract_violation", violation);
25646
+ if (handled === "abort") {
25647
+ failedNodeId = node.id;
25648
+ lastError = violation;
25649
+ break;
25650
+ }
25651
+ context["__contractViolation"] = violation;
25652
+ }
25653
+ }
25654
+ if (failedNodeId !== void 0) break;
25655
+ }
25656
+ }
25657
+ currentId = nextId;
25424
25658
  continue;
25425
25659
  }
25426
25660
  if (outcome.kind === "awaiting_approval") {
@@ -25560,7 +25794,8 @@ async function executePlaybook(options) {
25560
25794
  deterministicRunner: options.deterministicRunner,
25561
25795
  approvalSecret,
25562
25796
  maxIterationsDefault,
25563
- now
25797
+ now,
25798
+ projectRoot: options.projectRoot
25564
25799
  };
25565
25800
  return runFromNode(runArgs);
25566
25801
  }
@@ -25660,7 +25895,8 @@ async function resumePlaybook(options) {
25660
25895
  deterministicRunner: options.deterministicRunner,
25661
25896
  approvalSecret,
25662
25897
  maxIterationsDefault,
25663
- now
25898
+ now,
25899
+ projectRoot: options.projectRoot
25664
25900
  };
25665
25901
  return runFromNode(runArgs);
25666
25902
  }
@@ -25699,26 +25935,30 @@ __export(src_exports, {
25699
25935
  getPlaybookSecret: () => getPlaybookSecret,
25700
25936
  listPlaybookApprovals: () => listPlaybookApprovals,
25701
25937
  listPlaybookRuns: () => listPlaybookRuns,
25938
+ migratePlaybook: () => migratePlaybook,
25939
+ migratePlaybookFile: () => migratePlaybookFile,
25702
25940
  parsePlaybook: () => parsePlaybook,
25703
25941
  rejectGate: () => rejectGate,
25704
25942
  resumePlaybook: () => resumePlaybook,
25705
25943
  updatePlaybookApproval: () => updatePlaybookApproval,
25706
- updatePlaybookRun: () => updatePlaybookRun
25944
+ updatePlaybookRun: () => updatePlaybookRun,
25945
+ validatePlaybookCompliance: () => validatePlaybookCompliance
25707
25946
  });
25708
25947
  var PLAYBOOKS_PACKAGE_VERSION;
25709
25948
  var init_src2 = __esm({
25710
25949
  "packages/playbooks/src/index.ts"() {
25711
25950
  init_approval();
25951
+ init_migrate_e4();
25712
25952
  init_parser();
25713
25953
  init_policy();
25714
25954
  init_runtime();
25715
25955
  init_state();
25716
- PLAYBOOKS_PACKAGE_VERSION = "2026.4.85";
25956
+ PLAYBOOKS_PACKAGE_VERSION = "2026.4.129";
25717
25957
  }
25718
25958
  });
25719
25959
 
25720
25960
  // packages/cleo/src/dispatch/domains/playbook.ts
25721
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
25961
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
25722
25962
  import { homedir } from "node:os";
25723
25963
  import { dirname as dirname2, join as join4, resolve as resolvePath2 } from "node:path";
25724
25964
  import { fileURLToPath } from "node:url";
@@ -25760,7 +26000,7 @@ function loadPlaybookByName(name) {
25760
26000
  for (const dir of candidates) {
25761
26001
  const full = join4(dir, fileName);
25762
26002
  if (existsSync3(full)) {
25763
- return { sourcePath: full, source: readFileSync5(full, "utf8") };
26003
+ return { sourcePath: full, source: readFileSync6(full, "utf8") };
25764
26004
  }
25765
26005
  }
25766
26006
  return null;
@@ -25874,7 +26114,7 @@ var init_playbook = __esm({
25874
26114
  __playbookRuntimeOverrides = {};
25875
26115
  PlaybookHandler = class {
25876
26116
  /**
25877
- * Query gateway — `status` and `list`.
26117
+ * Query gateway — `status`, `list`, and `validate`.
25878
26118
  */
25879
26119
  async query(operation, params) {
25880
26120
  const startTime = Date.now();
@@ -25884,6 +26124,8 @@ var init_playbook = __esm({
25884
26124
  return this.handleStatus(params, startTime);
25885
26125
  case "list":
25886
26126
  return this.handleList(params, startTime);
26127
+ case "validate":
26128
+ return this.handleValidate(params, startTime);
25887
26129
  default:
25888
26130
  return errorResult(
25889
26131
  "query",
@@ -25928,7 +26170,7 @@ var init_playbook = __esm({
25928
26170
  */
25929
26171
  getSupportedOperations() {
25930
26172
  return {
25931
- query: ["status", "list"],
26173
+ query: ["status", "list", "validate"],
25932
26174
  mutate: ["run", "resume"]
25933
26175
  };
25934
26176
  }
@@ -25988,6 +26230,104 @@ var init_playbook = __esm({
25988
26230
  data: envelope
25989
26231
  };
25990
26232
  }
26233
+ /**
26234
+ * Validate a `.cantbook` file at an absolute path or a playbook name.
26235
+ *
26236
+ * Accepts either:
26237
+ * - `file` — absolute or relative path to a `.cantbook` file on disk.
26238
+ * - `name` — playbook name resolved through the standard search path.
26239
+ *
26240
+ * Returns a LAFS envelope with `valid: true` on success, or an error
26241
+ * envelope with `E_PLAYBOOK_PARSE` and the field/message on failure.
26242
+ * Exit code 70 is passed through from {@link PlaybookParseError}.
26243
+ *
26244
+ * @task T1261 PSYCHE E4
26245
+ */
26246
+ async handleValidate(params, startTime) {
26247
+ const file = params?.file;
26248
+ const name = params?.name;
26249
+ if (!file && !name) {
26250
+ return errorResult(
26251
+ "query",
26252
+ "playbook",
26253
+ "validate",
26254
+ "E_INVALID_INPUT",
26255
+ "Either file (path) or name (playbook name) is required",
26256
+ startTime
26257
+ );
26258
+ }
26259
+ let source;
26260
+ let sourcePath;
26261
+ if (file) {
26262
+ const { existsSync: existsSync12, readFileSync: readFileSync16 } = await import("node:fs");
26263
+ const { resolve: resolvePath3 } = await import("node:path");
26264
+ const resolved = resolvePath3(file);
26265
+ if (!existsSync12(resolved)) {
26266
+ return errorResult(
26267
+ "query",
26268
+ "playbook",
26269
+ "validate",
26270
+ "E_NOT_FOUND",
26271
+ `playbook file not found: ${resolved}`,
26272
+ startTime
26273
+ );
26274
+ }
26275
+ sourcePath = resolved;
26276
+ source = readFileSync16(resolved, "utf8");
26277
+ } else {
26278
+ const loaded = loadPlaybookByName(name);
26279
+ if (loaded === null) {
26280
+ return errorResult(
26281
+ "query",
26282
+ "playbook",
26283
+ "validate",
26284
+ "E_NOT_FOUND",
26285
+ `playbook "${name}" not found in any search path`,
26286
+ startTime
26287
+ );
26288
+ }
26289
+ sourcePath = loaded.sourcePath;
26290
+ source = loaded.source;
26291
+ }
26292
+ try {
26293
+ const { definition, sourceHash } = parsePlaybook(source);
26294
+ return {
26295
+ meta: dispatchMeta("query", "playbook", "validate", startTime),
26296
+ success: true,
26297
+ data: {
26298
+ valid: true,
26299
+ sourcePath,
26300
+ sourceHash,
26301
+ name: definition.name,
26302
+ version: definition.version,
26303
+ nodeCount: definition.nodes.length,
26304
+ edgeCount: definition.edges.length,
26305
+ hasRequires: definition.nodes.some((n) => n.requires !== void 0),
26306
+ hasEnsures: definition.nodes.some((n) => n.ensures !== void 0),
26307
+ hasErrorHandlers: (definition.error_handlers?.length ?? 0) > 0
26308
+ }
26309
+ };
26310
+ } catch (err) {
26311
+ if (err instanceof PlaybookParseError) {
26312
+ return errorResult(
26313
+ "query",
26314
+ "playbook",
26315
+ "validate",
26316
+ err.code,
26317
+ `${err.message}${err.field ? ` [field=${err.field}]` : ""}`,
26318
+ startTime
26319
+ );
26320
+ }
26321
+ return errorResult(
26322
+ "query",
26323
+ "playbook",
26324
+ "validate",
26325
+ "E_PLAYBOOK_PARSE",
26326
+ err instanceof Error ? err.message : String(err),
26327
+ startTime
26328
+ );
26329
+ }
26330
+ }
25991
26331
  async handleRun(params, startTime) {
25992
26332
  const name = params?.name;
25993
26333
  if (!name) {
@@ -26051,12 +26391,14 @@ var init_playbook = __esm({
26051
26391
  const dispatcher = await buildDefaultDispatcher();
26052
26392
  let result;
26053
26393
  try {
26394
+ const { getProjectRoot: getProjectRoot29 } = await import("@cleocode/core/internal");
26054
26395
  const opts = {
26055
26396
  db,
26056
26397
  playbook: parsed.definition,
26057
26398
  playbookHash: parsed.sourceHash,
26058
26399
  initialContext,
26059
- dispatcher
26400
+ dispatcher,
26401
+ projectRoot: getProjectRoot29()
26060
26402
  };
26061
26403
  if (__playbookRuntimeOverrides.approvalSecret !== void 0) {
26062
26404
  opts.approvalSecret = __playbookRuntimeOverrides.approvalSecret;
@@ -26219,7 +26561,7 @@ import {
26219
26561
  async function orchestrateClassify(request, context, projectRoot) {
26220
26562
  try {
26221
26563
  const { getCleoCantWorkflowsDir: getCleoCantWorkflowsDir2 } = await import("@cleocode/core/internal");
26222
- const { readFileSync: readFileSync15, readdirSync: readdirSync4, existsSync: existsSync12 } = await import("node:fs");
26564
+ const { readFileSync: readFileSync16, readdirSync: readdirSync4, existsSync: existsSync12 } = await import("node:fs");
26223
26565
  const { join: join22 } = await import("node:path");
26224
26566
  const workflowsDir = getCleoCantWorkflowsDir2();
26225
26567
  const combined = `${request} ${context ?? ""}`.toLowerCase();
@@ -26228,7 +26570,7 @@ async function orchestrateClassify(request, context, projectRoot) {
26228
26570
  const files = readdirSync4(workflowsDir).filter((f) => f.endsWith(".cant"));
26229
26571
  for (const file of files) {
26230
26572
  try {
26231
- const src = readFileSync15(join22(workflowsDir, file), "utf-8");
26573
+ const src = readFileSync16(join22(workflowsDir, file), "utf-8");
26232
26574
  const teamMatch = /^team\s+(\S+):/m.exec(src);
26233
26575
  if (!teamMatch) continue;
26234
26576
  const teamName = teamMatch[1];
@@ -26248,7 +26590,7 @@ async function orchestrateClassify(request, context, projectRoot) {
26248
26590
  const files = readdirSync4(localCantDir).filter((f) => f.endsWith(".cant"));
26249
26591
  for (const file of files) {
26250
26592
  try {
26251
- const src = readFileSync15(join22(localCantDir, file), "utf-8");
26593
+ const src = readFileSync16(join22(localCantDir, file), "utf-8");
26252
26594
  const teamMatch = /^team\s+(\S+):/m.exec(src);
26253
26595
  if (!teamMatch) continue;
26254
26596
  const teamName = teamMatch[1];
@@ -31185,7 +31527,7 @@ var init_defaults = __esm({
31185
31527
  });
31186
31528
 
31187
31529
  // packages/cleo/src/dispatch/lib/config-loader.ts
31188
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
31530
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
31189
31531
  import { join as join7 } from "path";
31190
31532
  function loadFromEnv(key) {
31191
31533
  const envKey = `${ENV_PREFIX}${key.toUpperCase()}`;
@@ -31212,7 +31554,7 @@ function loadFromFile(projectRoot) {
31212
31554
  return {};
31213
31555
  }
31214
31556
  try {
31215
- const content = readFileSync6(configPath, "utf-8");
31557
+ const content = readFileSync7(configPath, "utf-8");
31216
31558
  const config = JSON.parse(content);
31217
31559
  const result = {};
31218
31560
  if (config.lifecycleEnforcement) {
@@ -31921,7 +32263,7 @@ var init_cli = __esm({
31921
32263
  });
31922
32264
 
31923
32265
  // packages/cleo/src/cli/index.ts
31924
- import { readFileSync as readFileSync14 } from "node:fs";
32266
+ import { readFileSync as readFileSync15 } from "node:fs";
31925
32267
  import { dirname as dirname9, join as join21 } from "node:path";
31926
32268
  import { fileURLToPath as fileURLToPath3 } from "node:url";
31927
32269
  import {
@@ -32882,7 +33224,7 @@ var addCommand = defineCommand({
32882
33224
  });
32883
33225
 
32884
33226
  // packages/cleo/src/cli/commands/add-batch.ts
32885
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
33227
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
32886
33228
  init_cli();
32887
33229
  init_renderers();
32888
33230
  var addBatchCommand = defineCommand({
@@ -32939,7 +33281,7 @@ var addBatchCommand = defineCommand({
32939
33281
  process.exit(2);
32940
33282
  return;
32941
33283
  }
32942
- raw = readFileSync7(filePath, "utf-8");
33284
+ raw = readFileSync8(filePath, "utf-8");
32943
33285
  }
32944
33286
  let tasks;
32945
33287
  try {
@@ -33549,7 +33891,7 @@ var registerCommand = defineCommand({
33549
33891
  transportConfig: {},
33550
33892
  isActive: true
33551
33893
  });
33552
- const { existsSync: existsSync12, mkdirSync: mkdirSync4, writeFileSync: writeFileSync4 } = await import("node:fs");
33894
+ const { existsSync: existsSync12, mkdirSync: mkdirSync4, writeFileSync: writeFileSync5 } = await import("node:fs");
33553
33895
  const { join: join22 } = await import("node:path");
33554
33896
  const cantDir = join22(CLEO_DIR_NAME, AGENTS_SUBDIR);
33555
33897
  const cantPath = join22(cantDir, `${agentId}.cant`);
@@ -33605,7 +33947,7 @@ agent ${agentId}:
33605
33947
  enforcement:
33606
33948
  1: TODO \u2014 what does this agent push back on?
33607
33949
  `;
33608
- writeFileSync4(cantPath, cantContent, "utf-8");
33950
+ writeFileSync5(cantPath, cantContent, "utf-8");
33609
33951
  cantScaffolded = true;
33610
33952
  }
33611
33953
  cliOutput(
@@ -33733,7 +34075,7 @@ var startCommand = defineCommand({
33733
34075
  try {
33734
34076
  const { AgentRegistryAccessor, getDb: getDb3 } = await import("@cleocode/core/internal");
33735
34077
  const { createRuntime } = await import("@cleocode/runtime");
33736
- const { existsSync: existsSync12, readFileSync: readFileSync15 } = await import("node:fs");
34078
+ const { existsSync: existsSync12, readFileSync: readFileSync16 } = await import("node:fs");
33737
34079
  const { join: join22 } = await import("node:path");
33738
34080
  await getDb3();
33739
34081
  const registry = new AgentRegistryAccessor(process.cwd());
@@ -33756,7 +34098,7 @@ var startCommand = defineCommand({
33756
34098
  let cantValidation = null;
33757
34099
  const cantPath = args.cant ?? join22(CLEO_DIR_NAME, AGENTS_SUBDIR, `${args.agentId}.cant`);
33758
34100
  if (existsSync12(cantPath)) {
33759
- profile = readFileSync15(cantPath, "utf-8");
34101
+ profile = readFileSync16(cantPath, "utf-8");
33760
34102
  try {
33761
34103
  const cantModule = await import("@cleocode/cant");
33762
34104
  const validate = "validate" in cantModule ? cantModule.validate : null;
@@ -35514,7 +35856,7 @@ var createCommand = defineCommand({
35514
35856
  },
35515
35857
  async run({ args }) {
35516
35858
  try {
35517
- const { existsSync: existsSync12, mkdirSync: mkdirSync4, writeFileSync: writeFileSync4 } = await import("node:fs");
35859
+ const { existsSync: existsSync12, mkdirSync: mkdirSync4, writeFileSync: writeFileSync5 } = await import("node:fs");
35518
35860
  const { join: join22 } = await import("node:path");
35519
35861
  const { homedir: homedir6 } = await import("node:os");
35520
35862
  const name = args.name;
@@ -35605,9 +35947,9 @@ var createCommand = defineCommand({
35605
35947
  domain,
35606
35948
  parent
35607
35949
  });
35608
- writeFileSync4(join22(agentDir, "persona.cant"), personaContent, "utf-8");
35950
+ writeFileSync5(join22(agentDir, "persona.cant"), personaContent, "utf-8");
35609
35951
  const manifest = generateManifest({ name, role, tier, domain });
35610
- writeFileSync4(
35952
+ writeFileSync5(
35611
35953
  join22(agentDir, "manifest.json"),
35612
35954
  `${JSON.stringify(manifest, null, 2)}
35613
35955
  `,
@@ -35619,14 +35961,14 @@ var createCommand = defineCommand({
35619
35961
  ];
35620
35962
  if (team) {
35621
35963
  const teamConfigContent = generateTeamConfig(name, role, team);
35622
- writeFileSync4(join22(agentDir, "team-config.cant"), teamConfigContent, "utf-8");
35964
+ writeFileSync5(join22(agentDir, "team-config.cant"), teamConfigContent, "utf-8");
35623
35965
  createdFiles.push(join22(agentDir, "team-config.cant"));
35624
35966
  }
35625
35967
  if (seedBrain) {
35626
35968
  const expertiseDir = join22(agentDir, "expertise");
35627
35969
  mkdirSync4(expertiseDir, { recursive: true });
35628
35970
  const seedContent = generateMentalModelSeed(name, role, domain);
35629
- writeFileSync4(join22(expertiseDir, "mental-model-seed.md"), seedContent, "utf-8");
35971
+ writeFileSync5(join22(expertiseDir, "mental-model-seed.md"), seedContent, "utf-8");
35630
35972
  createdFiles.push(join22(expertiseDir, "mental-model-seed.md"));
35631
35973
  try {
35632
35974
  const { execFile: execFile2 } = await import("node:child_process");
@@ -35725,7 +36067,7 @@ var mintCommand = defineCommand({
35725
36067
  },
35726
36068
  async run({ args }) {
35727
36069
  try {
35728
- const { existsSync: existsSync12, readFileSync: readFileSync15, mkdirSync: mkdirSync4 } = await import("node:fs");
36070
+ const { existsSync: existsSync12, readFileSync: readFileSync16, mkdirSync: mkdirSync4 } = await import("node:fs");
35729
36071
  const { resolve: resolve5, join: join22 } = await import("node:path");
35730
36072
  const specPath = resolve5(args.spec);
35731
36073
  if (!existsSync12(specPath)) {
@@ -35743,7 +36085,7 @@ var mintCommand = defineCommand({
35743
36085
  process.exitCode = 4;
35744
36086
  return;
35745
36087
  }
35746
- const specContent = readFileSync15(specPath, "utf-8");
36088
+ const specContent = readFileSync16(specPath, "utf-8");
35747
36089
  const projectRoot = process.cwd();
35748
36090
  const outputDir = args["output-dir"] ? resolve5(args["output-dir"]) : join22(projectRoot, ".cleo", "cant", "agents");
35749
36091
  mkdirSync4(outputDir, { recursive: true });
@@ -37919,7 +38261,7 @@ var cancelCommand = defineCommand({
37919
38261
  });
37920
38262
 
37921
38263
  // packages/cleo/src/cli/commands/cant.ts
37922
- import { existsSync as existsSync8, mkdirSync, readFileSync as readFileSync8, writeFileSync } from "node:fs";
38264
+ import { existsSync as existsSync8, mkdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
37923
38265
  import { dirname as dirname5, isAbsolute, join as join10, resolve as resolve3 } from "node:path";
37924
38266
  init_renderers();
37925
38267
  function resolveFilePath(file) {
@@ -38070,7 +38412,7 @@ var cantMigrateCommand = defineCommand({
38070
38412
  if (!ensureExists(filePath, "cant.migrate")) return;
38071
38413
  try {
38072
38414
  const mod = await loadMigrateEngine();
38073
- const content = readFileSync8(filePath, "utf-8");
38415
+ const content = readFileSync9(filePath, "utf-8");
38074
38416
  const result = mod.migrateMarkdown(content, filePath, {
38075
38417
  write: isWrite,
38076
38418
  verbose: isVerbose,
@@ -38082,7 +38424,7 @@ var cantMigrateCommand = defineCommand({
38082
38424
  for (const outputFile of result.outputFiles) {
38083
38425
  const outputPath = isAbsolute(outputFile.path) ? outputFile.path : join10(projectRoot, outputFile.path);
38084
38426
  mkdirSync(dirname5(outputPath), { recursive: true });
38085
- writeFileSync(outputPath, outputFile.content, "utf-8");
38427
+ writeFileSync2(outputPath, outputFile.content, "utf-8");
38086
38428
  written++;
38087
38429
  }
38088
38430
  cliOutput(
@@ -38130,7 +38472,7 @@ var cantCommand = defineCommand({
38130
38472
  });
38131
38473
 
38132
38474
  // packages/cleo/src/cli/commands/chain.ts
38133
- import { readFileSync as readFileSync9 } from "node:fs";
38475
+ import { readFileSync as readFileSync10 } from "node:fs";
38134
38476
  init_cli();
38135
38477
  var showCommand3 = defineCommand({
38136
38478
  meta: { name: "show", description: "Show details for a WarpChain definition" },
@@ -38167,7 +38509,7 @@ var addCommand3 = defineCommand({
38167
38509
  }
38168
38510
  },
38169
38511
  async run({ args }) {
38170
- const chainJson = JSON.parse(readFileSync9(args.file, "utf-8"));
38512
+ const chainJson = JSON.parse(readFileSync10(args.file, "utf-8"));
38171
38513
  await dispatchFromCli(
38172
38514
  "mutate",
38173
38515
  "pipeline",
@@ -38336,10 +38678,10 @@ var checkChainValidateCommand = defineCommand({
38336
38678
  }
38337
38679
  },
38338
38680
  async run({ args }) {
38339
- const { readFileSync: readFileSync15 } = await import("node:fs");
38681
+ const { readFileSync: readFileSync16 } = await import("node:fs");
38340
38682
  let chain;
38341
38683
  try {
38342
- chain = JSON.parse(readFileSync15(args.file, "utf8"));
38684
+ chain = JSON.parse(readFileSync16(args.file, "utf8"));
38343
38685
  } catch (err) {
38344
38686
  const message = err instanceof Error ? err.message : String(err);
38345
38687
  console.error(`Failed to read or parse chain file: ${message}`);
@@ -40353,7 +40695,7 @@ var detectCommand2 = defineCommand({
40353
40695
 
40354
40696
  // packages/cleo/src/cli/commands/detect-drift.ts
40355
40697
  init_src();
40356
- import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "node:fs";
40698
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "node:fs";
40357
40699
  import { dirname as dirname6, join as join12 } from "node:path";
40358
40700
  init_paths();
40359
40701
  init_renderers();
@@ -40380,7 +40722,7 @@ var detectDriftCommand = defineCommand({
40380
40722
  const cleoSrcRoot = isCleoRepo ? join12(projectRoot, "packages", "cleo", "src") : join12(projectRoot, "src");
40381
40723
  const safeRead = (filePath) => {
40382
40724
  try {
40383
- return readFileSync10(filePath, "utf-8");
40725
+ return readFileSync11(filePath, "utf-8");
40384
40726
  } catch {
40385
40727
  return "";
40386
40728
  }
@@ -42396,14 +42738,14 @@ var gcCommand = defineCommand({
42396
42738
  // packages/cleo/src/cli/commands/generate-changelog.ts
42397
42739
  init_src();
42398
42740
  import { execFileSync as execFileSync3 } from "node:child_process";
42399
- import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
42741
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
42400
42742
  import { dirname as dirname8, join as join15 } from "node:path";
42401
42743
  import { CleoError as CleoError4, formatError as formatError6, getConfigPath as getConfigPath2, getProjectRoot as getProjectRoot21 } from "@cleocode/core";
42402
42744
  init_renderers();
42403
42745
  function getChangelogSource(cwd) {
42404
42746
  const configPath = getConfigPath2(cwd);
42405
42747
  try {
42406
- const config = JSON.parse(readFileSync11(configPath, "utf-8"));
42748
+ const config = JSON.parse(readFileSync12(configPath, "utf-8"));
42407
42749
  return config?.release?.changelog?.source ?? "CHANGELOG.md";
42408
42750
  } catch {
42409
42751
  return "CHANGELOG.md";
@@ -42412,7 +42754,7 @@ function getChangelogSource(cwd) {
42412
42754
  function getEnabledPlatforms(cwd) {
42413
42755
  const configPath = getConfigPath2(cwd);
42414
42756
  try {
42415
- const config = JSON.parse(readFileSync11(configPath, "utf-8"));
42757
+ const config = JSON.parse(readFileSync12(configPath, "utf-8"));
42416
42758
  const outputs = config?.release?.changelog?.outputs ?? [];
42417
42759
  return outputs.filter((o) => o.enabled);
42418
42760
  } catch {
@@ -42560,7 +42902,7 @@ var generateChangelogCommand = defineCommand({
42560
42902
  if (!existsSync10(sourcePath)) {
42561
42903
  throw new CleoError4(4 /* NOT_FOUND */, `Changelog source not found: ${sourcePath}`);
42562
42904
  }
42563
- const sourceContent = readFileSync11(sourcePath, "utf-8");
42905
+ const sourceContent = readFileSync12(sourcePath, "utf-8");
42564
42906
  const repoSlug = getGitHubRepoSlug();
42565
42907
  const results = [];
42566
42908
  if (targetPlatform) {
@@ -42571,7 +42913,7 @@ var generateChangelogCommand = defineCommand({
42571
42913
  if (!dryRun) {
42572
42914
  const fullPath = join15(getProjectRoot21(), outputPath);
42573
42915
  mkdirSync2(dirname8(fullPath), { recursive: true });
42574
- writeFileSync2(fullPath, content, "utf-8");
42916
+ writeFileSync3(fullPath, content, "utf-8");
42575
42917
  }
42576
42918
  results.push({ platform: targetPlatform, path: outputPath, written: !dryRun });
42577
42919
  } else {
@@ -42592,7 +42934,7 @@ var generateChangelogCommand = defineCommand({
42592
42934
  if (!dryRun) {
42593
42935
  const fullPath = join15(getProjectRoot21(), platformConfig.path);
42594
42936
  mkdirSync2(dirname8(fullPath), { recursive: true });
42595
- writeFileSync2(fullPath, content, "utf-8");
42937
+ writeFileSync3(fullPath, content, "utf-8");
42596
42938
  }
42597
42939
  results.push({
42598
42940
  platform: platformConfig.platform,
@@ -44000,7 +44342,7 @@ var mapCommand = defineCommand({
44000
44342
 
44001
44343
  // packages/cleo/src/cli/commands/memory.ts
44002
44344
  import { createHash as createHash3 } from "node:crypto";
44003
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
44345
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "node:fs";
44004
44346
  import { homedir as homedir4 } from "node:os";
44005
44347
  import { join as join16 } from "node:path";
44006
44348
  import {
@@ -44046,7 +44388,7 @@ ${body}`).digest("hex").slice(0, 16);
44046
44388
  function loadImportHashes(stateFile) {
44047
44389
  try {
44048
44390
  if (!existsSync11(stateFile)) return /* @__PURE__ */ new Set();
44049
- const raw = readFileSync12(stateFile, "utf-8");
44391
+ const raw = readFileSync13(stateFile, "utf-8");
44050
44392
  const parsed = JSON.parse(raw);
44051
44393
  return new Set(parsed.hashes);
44052
44394
  } catch {
@@ -44056,7 +44398,7 @@ function loadImportHashes(stateFile) {
44056
44398
  function saveImportHashes(stateFile, hashes) {
44057
44399
  const dir = stateFile.slice(0, stateFile.lastIndexOf("/"));
44058
44400
  if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
44059
- writeFileSync3(stateFile, JSON.stringify({ hashes: [...hashes] }, null, 2), "utf-8");
44401
+ writeFileSync4(stateFile, JSON.stringify({ hashes: [...hashes] }, null, 2), "utf-8");
44060
44402
  }
44061
44403
  var storeCommand = defineCommand({
44062
44404
  meta: { name: "store", description: "Store a pattern or learning to BRAIN memory" },
@@ -45329,7 +45671,7 @@ var importCommand3 = defineCommand({
45329
45671
  for (const filePath of files) {
45330
45672
  const fileName = filePath.split("/").pop() ?? filePath;
45331
45673
  try {
45332
- const raw = readFileSync12(filePath, "utf-8");
45674
+ const raw = readFileSync13(filePath, "utf-8");
45333
45675
  if (!raw.trim()) {
45334
45676
  stats.skipped++;
45335
45677
  skippedEntries.push({ file: fileName, reason: "empty file" });
@@ -49213,8 +49555,8 @@ var exportCommand6 = defineCommand({
49213
49555
  return;
49214
49556
  }
49215
49557
  if (outputFile) {
49216
- const { writeFileSync: writeFileSync4 } = await import("node:fs");
49217
- writeFileSync4(outputFile, output, "utf-8");
49558
+ const { writeFileSync: writeFileSync5 } = await import("node:fs");
49559
+ writeFileSync5(outputFile, output, "utf-8");
49218
49560
  process.stdout.write(
49219
49561
  `[nexus] Exported to ${outputFile} (${nodes.length} nodes, ${relations.length} edges)
49220
49562
  `
@@ -52440,17 +52782,41 @@ var listCommand13 = defineCommand({
52440
52782
  );
52441
52783
  }
52442
52784
  });
52785
+ var validateCommand6 = defineCommand({
52786
+ meta: {
52787
+ name: "validate",
52788
+ description: "Parse and validate a .cantbook file \u2014 exit 0 on success, exit 70 on parse error (T1261)"
52789
+ },
52790
+ args: {
52791
+ file: {
52792
+ type: "positional",
52793
+ description: "Path to .cantbook file OR playbook name to resolve via search path",
52794
+ required: true
52795
+ }
52796
+ },
52797
+ async run({ args }) {
52798
+ const isPath = args.file.includes("/") || args.file.includes("\\") || args.file.endsWith(".cantbook");
52799
+ await dispatchFromCli(
52800
+ "query",
52801
+ "playbook",
52802
+ "validate",
52803
+ isPath ? { file: args.file } : { name: args.file },
52804
+ { command: "playbook" }
52805
+ );
52806
+ }
52807
+ });
52443
52808
  var playbookCommand = defineCommand({
52444
52809
  meta: {
52445
52810
  name: "playbook",
52446
- description: "Playbook runtime operations (run, status, resume, list, create)"
52811
+ description: "Playbook runtime operations (run, status, resume, list, create, validate)"
52447
52812
  },
52448
52813
  subCommands: {
52449
52814
  run: runCommand3,
52450
52815
  status: statusCommand10,
52451
52816
  resume: resumeCommand,
52452
52817
  list: listCommand13,
52453
- create: createCommand2
52818
+ create: createCommand2,
52819
+ validate: validateCommand6
52454
52820
  },
52455
52821
  async run({ cmd, rawArgs }) {
52456
52822
  const firstArg = rawArgs?.find((a) => !a.startsWith("-"));
@@ -56364,7 +56730,7 @@ var searchCommand3 = defineCommand({
56364
56730
  );
56365
56731
  }
56366
56732
  });
56367
- var validateCommand6 = defineCommand({
56733
+ var validateCommand7 = defineCommand({
56368
56734
  meta: { name: "validate", description: "Validate skill against protocol" },
56369
56735
  args: {
56370
56736
  "skill-name": {
@@ -56631,7 +56997,7 @@ var skillsCommand2 = defineCommand({
56631
56997
  subCommands: {
56632
56998
  list: listCommand20,
56633
56999
  search: searchCommand3,
56634
- validate: validateCommand6,
57000
+ validate: validateCommand7,
56635
57001
  info: infoCommand,
56636
57002
  install: installCommand2,
56637
57003
  uninstall: uninstallCommand,
@@ -57212,10 +57578,10 @@ var reconcileCommand2 = defineCommand({
57212
57578
  }
57213
57579
  },
57214
57580
  async run({ args }) {
57215
- const { readFileSync: readFileSync15 } = await import("node:fs");
57581
+ const { readFileSync: readFileSync16 } = await import("node:fs");
57216
57582
  let externalTasks;
57217
57583
  try {
57218
- externalTasks = JSON.parse(readFileSync15(args.file, "utf8"));
57584
+ externalTasks = JSON.parse(readFileSync16(args.file, "utf8"));
57219
57585
  } catch (err) {
57220
57586
  const message = err instanceof Error ? err.message : String(err);
57221
57587
  console.error(`Failed to read or parse external tasks file: ${message}`);
@@ -57253,7 +57619,7 @@ var syncCommand5 = defineCommand({
57253
57619
 
57254
57620
  // packages/cleo/src/cli/commands/testing.ts
57255
57621
  init_cli();
57256
- var validateCommand7 = defineCommand({
57622
+ var validateCommand8 = defineCommand({
57257
57623
  meta: { name: "validate", description: "Validate testing protocol compliance for a task" },
57258
57624
  args: {
57259
57625
  taskId: {
@@ -57353,7 +57719,7 @@ var runCommand4 = defineCommand({
57353
57719
  var testingCommand = defineCommand({
57354
57720
  meta: { name: "testing", description: "Validate testing protocol compliance" },
57355
57721
  subCommands: {
57356
- validate: validateCommand7,
57722
+ validate: validateCommand8,
57357
57723
  check: checkCommand7,
57358
57724
  status: statusCommand12,
57359
57725
  coverage: coverageCommand,
@@ -57367,14 +57733,14 @@ var testingCommand = defineCommand({
57367
57733
  });
57368
57734
 
57369
57735
  // packages/cleo/src/cli/commands/token.ts
57370
- import { readFileSync as readFileSync13 } from "node:fs";
57736
+ import { readFileSync as readFileSync14 } from "node:fs";
57371
57737
  import { measureTokenExchange, recordTokenExchange as recordTokenExchange2 } from "@cleocode/core/internal";
57372
57738
  init_cli();
57373
57739
  init_renderers();
57374
57740
  function readPayload(args, textKey, fileKey) {
57375
57741
  const text = args[textKey];
57376
57742
  const file = args[fileKey];
57377
- if (file) return readFileSync13(file, "utf-8");
57743
+ if (file) return readFileSync14(file, "utf-8");
57378
57744
  return text;
57379
57745
  }
57380
57746
  var filterArgs = {
@@ -58765,7 +59131,7 @@ Or via NodeSource: https://github.com/nodesource/distributions
58765
59131
  }
58766
59132
  function getPackageVersion() {
58767
59133
  const pkgPath = join21(dirname9(fileURLToPath3(import.meta.url)), "../../package.json");
58768
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
59134
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
58769
59135
  return pkg.version;
58770
59136
  }
58771
59137
  var CLI_VERSION = getPackageVersion();